From 7923850e94c1ad542d109b141bc50b7c479419c6 Mon Sep 17 00:00:00 2001 From: Christophe Bliard Date: Mon, 16 Dec 2024 11:25:10 +0100 Subject: [PATCH] [59539] Update seeding data to be compatible with new automatic scheduling mode --- app/models/relation.rb | 17 +- app/seeders/demo_data/work_package_seeder.rb | 11 +- app/seeders/standard.yml | 161 ++++++++-------- app/services/work_packages/shared/all_days.rb | 7 + .../work_packages/shared/working_days.rb | 12 +- modules/bim/app/seeders/bim.yml | 177 +++++++++++------- .../seeders/root_seeder_bim_edition_spec.rb | 5 + .../demo_data/work_package_seeder_spec.rb | 49 ++++- spec/seeders/root_seeder_shared_examples.rb | 55 ++++++ .../root_seeder_standard_edition_spec.rb | 5 + .../work_packages/shared/all_days_spec.rb | 2 + .../shared/shared_examples_days.rb | 77 ++++++++ .../work_packages/shared/working_days_spec.rb | 2 + 13 files changed, 427 insertions(+), 153 deletions(-) diff --git a/app/models/relation.rb b/app/models/relation.rb index 947312b1c213..f96967d3c8fa 100644 --- a/app/models/relation.rb +++ b/app/models/relation.rb @@ -134,10 +134,23 @@ def label_for(work_package) TYPES[relation_type] ? TYPES[relation_type][key] : :unknown end + def predecessor = to + def predecessor_id = to_id + def successor = from + def successor_id = from_id + + def predecessor_date + predecessor.due_date || predecessor.start_date + end + + def successor_date + successor.start_date || successor.due_date + end + def successor_soonest_start - if follows? && (to.start_date || to.due_date) + if follows? && predecessor_date days = WorkPackages::Shared::Days.for(from) - relation_start_date = (to.due_date || to.start_date) + 1.day + relation_start_date = predecessor_date + 1.day days.soonest_working_day(relation_start_date, lag:) end end diff --git a/app/seeders/demo_data/work_package_seeder.rb b/app/seeders/demo_data/work_package_seeder.rb index efee28ae1cdf..93006c89c365 100644 --- a/app/seeders/demo_data/work_package_seeder.rb +++ b/app/seeders/demo_data/work_package_seeder.rb @@ -199,6 +199,7 @@ def work_package_attributes due_date:, duration:, ignore_non_working_days:, + schedule_manually:, estimated_hours: } end @@ -213,7 +214,7 @@ def initialize(attributes) def start_date days_ahead = attributes["start"] || 0 - Time.zone.today.monday + days_ahead.days + Date.current.monday + days_ahead.days end def due_date @@ -230,6 +231,14 @@ def ignore_non_working_days .any? { |date| working_days.non_working?(date) } end + def schedule_manually + if attributes["schedule_manually"].nil? + WorkPackage.column_defaults["schedule_manually"] + else + ActiveModel::Type::Boolean.new.cast(attributes["schedule_manually"]) + end + end + def estimated_hours attributes["estimated_hours"]&.to_i end diff --git a/app/seeders/standard.yml b/app/seeders/standard.yml index 0e00dea26de8..eeabbfbc046e 100644 --- a/app/seeders/standard.yml +++ b/app/seeders/standard.yml @@ -243,7 +243,7 @@ projects: options: t_name: 'Milestones' queryId: '##query.id:demo_project__query__milestones' - # For all dates, the reference is the monday of the current week + # For all dates, the reference is the Monday of the current week # So # * start: 0 references Monday # * start: 4 references Friday @@ -255,13 +255,15 @@ projects: status: :default_status_closed type: :default_type_milestone duration: 1 - schedule_manually: false + schedule_manually: true - start: 0 t_subject: Organize open source conference reference: :organize_open_source_conference description: '' status: :default_status_in_progress type: :default_type_phase + duration: 15 + schedule_manually: false children: - start: 0 t_subject: Set date and location of conference @@ -269,6 +271,8 @@ projects: description: '' status: :default_status_in_progress type: :default_type_task + duration: 4 + schedule_manually: false children: - start: 0 t_subject: Send invitation to speakers @@ -288,8 +292,6 @@ projects: status: :default_status_new type: :default_type_task duration: 4 - duration: 4 - schedule_manually: false - start: 4 t_subject: Invite attendees to conference reference: :invite_attendees_to_conference @@ -297,10 +299,10 @@ projects: status: :default_status_new type: :default_type_task duration: 1 + schedule_manually: false relations: - to: :set_date_and_location_of_conference type: follows - schedule_manually: false - start: 4 t_subject: Setup conference website reference: :setup_conference_website @@ -308,12 +310,10 @@ projects: status: :default_status_new type: :default_type_task duration: 11 + schedule_manually: false relations: - to: :set_date_and_location_of_conference type: follows - schedule_manually: false - duration: 15 - schedule_manually: true - start: 15 t_subject: Conference description: '' @@ -330,6 +330,8 @@ projects: description: '' status: :default_status_to_be_scheduled type: :default_type_phase + duration: 11 + schedule_manually: false children: - start: 21 t_subject: Upload presentations to website @@ -338,7 +340,6 @@ projects: status: :default_status_new type: :default_type_task duration: 10 - schedule_manually: false - start: 31 t_subject: Party for conference supporters :-) t_description: |- @@ -349,19 +350,16 @@ projects: status: :default_status_new type: :default_type_task duration: 1 - schedule_manually: false - duration: 11 - schedule_manually: false - start: 32 t_subject: End of project description: status: :default_status_new type: :default_type_milestone duration: 1 + schedule_manually: false relations: - to: :follow_up_tasks type: follows - schedule_manually: false t_wiki: | _In this wiki you can collaboratively create and edit pages and sub-pages to create a project wiki._ @@ -619,6 +617,7 @@ projects: type: :default_type_epic start: 0 duration: 29 + schedule_manually: false children: - t_subject: Newsletter registration form status: :default_status_in_progress @@ -638,6 +637,7 @@ projects: story_points: 3 start: 28 duration: 1 + schedule_manually: false children: - t_subject: Create wireframes for new landing page status: :default_status_in_progress @@ -659,6 +659,7 @@ projects: version: :scrum_project__version__sprint_1 position: 3 story_points: 5 + schedule_manually: false children: - t_subject: Make screenshots for feature tour status: :default_status_closed @@ -705,6 +706,7 @@ projects: story_points: 3 start: 25 duration: 1 + schedule_manually: false children: - t_subject: Set up navigation concept for website. status: :default_status_in_specification @@ -724,12 +726,13 @@ projects: status: :default_status_in_progress type: :default_type_phase start: 14 - duration: 3 + duration: 4 - t_subject: Release v1.0 status: :default_status_new type: :default_type_milestone start: 18 duration: 1 + schedule_manually: false relations: - to: :develop_v1_0 type: follows @@ -738,12 +741,13 @@ projects: status: :default_status_new type: :default_type_phase start: 21 - duration: 3 + duration: 4 - t_subject: Release v1.1 status: :default_status_new type: :default_type_milestone start: 25 duration: 1 + schedule_manually: false relations: - to: :develop_v1_1 type: follows @@ -752,12 +756,13 @@ projects: status: :default_status_new type: :default_type_phase start: 28 - duration: 3 + duration: 4 - t_subject: Release v2.0 status: :default_status_new type: :default_type_milestone start: 32 duration: 1 + schedule_manually: false relations: - to: :develop_v2_0 type: follows @@ -940,80 +945,80 @@ types: workflows: - type: :default_type_task statuses: - - :default_status_new - - :default_status_in_progress - - :default_status_on_hold - - :default_status_rejected - - :default_status_closed + - :default_status_new + - :default_status_in_progress + - :default_status_on_hold + - :default_status_rejected + - :default_status_closed - type: :default_type_milestone statuses: - - :default_status_new - - :default_status_to_be_scheduled - - :default_status_scheduled - - :default_status_in_progress - - :default_status_on_hold - - :default_status_rejected - - :default_status_closed + - :default_status_new + - :default_status_to_be_scheduled + - :default_status_scheduled + - :default_status_in_progress + - :default_status_on_hold + - :default_status_rejected + - :default_status_closed - type: :default_type_phase statuses: - - :default_status_new - - :default_status_to_be_scheduled - - :default_status_scheduled - - :default_status_in_progress - - :default_status_on_hold - - :default_status_rejected - - :default_status_closed + - :default_status_new + - :default_status_to_be_scheduled + - :default_status_scheduled + - :default_status_in_progress + - :default_status_on_hold + - :default_status_rejected + - :default_status_closed - type: :default_type_feature statuses: - - :default_status_new - - :default_status_in_specification - - :default_status_specified - - :default_status_in_progress - - :default_status_developed - - :default_status_in_testing - - :default_status_tested - - :default_status_test_failed - - :default_status_on_hold - - :default_status_rejected - - :default_status_closed + - :default_status_new + - :default_status_in_specification + - :default_status_specified + - :default_status_in_progress + - :default_status_developed + - :default_status_in_testing + - :default_status_tested + - :default_status_test_failed + - :default_status_on_hold + - :default_status_rejected + - :default_status_closed - type: :default_type_epic statuses: - - :default_status_new - - :default_status_in_specification - - :default_status_specified - - :default_status_in_progress - - :default_status_developed - - :default_status_in_testing - - :default_status_tested - - :default_status_test_failed - - :default_status_on_hold - - :default_status_rejected - - :default_status_closed + - :default_status_new + - :default_status_in_specification + - :default_status_specified + - :default_status_in_progress + - :default_status_developed + - :default_status_in_testing + - :default_status_tested + - :default_status_test_failed + - :default_status_on_hold + - :default_status_rejected + - :default_status_closed - type: :default_type_user_story statuses: - - :default_status_new - - :default_status_in_specification - - :default_status_specified - - :default_status_in_progress - - :default_status_developed - - :default_status_in_testing - - :default_status_tested - - :default_status_test_failed - - :default_status_on_hold - - :default_status_rejected - - :default_status_closed + - :default_status_new + - :default_status_in_specification + - :default_status_specified + - :default_status_in_progress + - :default_status_developed + - :default_status_in_testing + - :default_status_tested + - :default_status_test_failed + - :default_status_on_hold + - :default_status_rejected + - :default_status_closed - type: :default_type_bug statuses: - - :default_status_new - - :default_status_confirmed - - :default_status_in_progress - - :default_status_developed - - :default_status_in_testing - - :default_status_tested - - :default_status_test_failed - - :default_status_on_hold - - :default_status_rejected - - :default_status_closed + - :default_status_new + - :default_status_confirmed + - :default_status_in_progress + - :default_status_developed + - :default_status_in_testing + - :default_status_tested + - :default_status_test_failed + - :default_status_on_hold + - :default_status_rejected + - :default_status_closed welcome: t_title: "Welcome to OpenProject!" diff --git a/app/services/work_packages/shared/all_days.rb b/app/services/work_packages/shared/all_days.rb index bcceae1539f9..0824e79c354c 100644 --- a/app/services/work_packages/shared/all_days.rb +++ b/app/services/work_packages/shared/all_days.rb @@ -36,6 +36,13 @@ def duration(start_date, due_date) (start_date..due_date).count end + # Returns the number of working days between a predecessor date and + # successor date, exclusive. + def lag(predecessor_date, successor_date) + # lag is *always* excluding non-working days (at least for now) + WorkingDays.new.lag(predecessor_date, successor_date) + end + def start_date(due_date, duration) return nil unless due_date && duration raise ArgumentError, "duration must be strictly positive" if duration.is_a?(Integer) && duration <= 0 diff --git a/app/services/work_packages/shared/working_days.rb b/app/services/work_packages/shared/working_days.rb index 5bc2179e01b4..cdf007b285d5 100644 --- a/app/services/work_packages/shared/working_days.rb +++ b/app/services/work_packages/shared/working_days.rb @@ -35,14 +35,22 @@ def clear_cache end end - # Returns number of working days between two dates, excluding weekend days - # and non working days. + # Returns number of working days between two dates, inclusive, excluding + # weekend days and non working days. def duration(start_date, due_date) return nil unless start_date && due_date (start_date..due_date).count { working?(_1) } end + # Returns the number of working days between a predecessor date and + # successor date, exclusive. + def lag(predecessor_date, successor_date) + return nil unless predecessor_date && successor_date + + duration(predecessor_date + 1.day, successor_date - 1.day) + end + def start_date(due_date, duration) assert_strictly_positive_duration(duration) return nil unless due_date && duration diff --git a/modules/bim/app/seeders/bim.yml b/modules/bim/app/seeders/bim.yml index 904432c6487a..c4b0e419ff5f 100644 --- a/modules/bim/app/seeders/bim.yml +++ b/modules/bim/app/seeders/bim.yml @@ -538,6 +538,12 @@ projects: and "Request", thus represents a general note. :status: :default_status_new :type: :default_type_phase + :assigned_to: :group__planners + :duration: 15 + :schedule_manually: false + :relations: + - :to: :project_kick_off_construction_project + :type: follows :children: - :start: 7 :t_subject: Gathering first project information @@ -556,7 +562,7 @@ projects: :status: :default_status_new :type: :default_type_task :assigned_to: :group__planners - :duration: 4 + :duration: 5 - :start: 14 :t_subject: Summarize the results :reference: :summarize_the_results @@ -576,7 +582,8 @@ projects: :status: :default_status_new :type: :default_type_task :assigned_to: :group__planners - :duration: 4 + :duration: 5 + :schedule_manually: false :relations: - :to: :gathering_first_project_information :type: follows @@ -589,20 +596,19 @@ projects: :type: :default_type_milestone :assigned_to: :group__planners :duration: 1 + :schedule_manually: false :relations: - :to: :summarize_the_results :type: follows - :assigned_to: :group__planners - :duration: 14 - :relations: - - :to: :project_kick_off_construction_project - :type: follows - :start: 22 :t_subject: Preliminary planning :t_description: This type is hierarchically a parent of the types "Clash" and "Request", thus represents a general note. :status: :default_status_new :type: :default_type_phase + :assigned_to: :group__planners + :duration: 18 + :schedule_manually: false :children: - :start: 22 :t_subject: Developing first draft @@ -612,7 +618,7 @@ projects: * Create a useful overview of the results * Check what has been done and summarize the results - * Communicate all the relevant results with the customer + * Communicate all the relevant results with the customer * Identify the fundamental boundary conditions of the project ## Description @@ -623,7 +629,8 @@ projects: :status: :default_status_new :type: :default_type_task :assigned_to: :group__planners - :duration: 10 + :duration: 11 + :schedule_manually: false :relations: - :to: :end_of_basic_evaluation :type: follows @@ -635,7 +642,7 @@ projects: * Create a useful overview of the results * Check what has been done and summarize the results - * Communicate all the relevant results with the customer + * Communicate all the relevant results with the customer * Identify the fundamental boundary conditions of the project ## Description @@ -646,12 +653,11 @@ projects: :status: :default_status_new :type: :default_type_task :assigned_to: :group__planners - :duration: 4 + :duration: 5 + :schedule_manually: false :relations: - :to: :developing_first_draft :type: follows - :assigned_to: :group__planners - :duration: 17 - :start: 42 :t_subject: Passing of preliminary planning :reference: :passing_of_preliminary_planning @@ -661,17 +667,21 @@ projects: :type: :default_type_milestone :assigned_to: :group__planners :duration: 1 + :schedule_manually: false :relations: - :to: :summarize_results :type: follows - - :start: 44 + - :start: 43 :t_subject: Design planning :t_description: This type is hierarchically a parent of the types "Clash" and "Request", thus represents a general note. :status: :default_status_new :type: :default_type_phase + :assigned_to: :group__planners + :duration: 46 + :schedule_manually: false :children: - - :start: 44 + - :start: 43 :t_subject: Finishing design :reference: :finishing_design :t_description: |- @@ -690,10 +700,11 @@ projects: :type: :default_type_task :assigned_to: :group__planners :duration: 45 + :schedule_manually: false :relations: - :to: :passing_of_preliminary_planning :type: follows - - :start: 90 + - :start: 88 :t_subject: Design freeze :reference: :design_freeze :t_description: This type is hierarchically a parent of the types "Clash" @@ -702,18 +713,23 @@ projects: :type: :default_type_milestone :assigned_to: :group__planners :duration: 1 + :schedule_manually: false :relations: - :to: :finishing_design :type: follows - :assigned_to: :group__planners - :duration: 46 - - :start: 98 + - :start: 91 :t_subject: Construction phase :description: '' :status: :default_status_new :type: :default_type_phase + :assigned_to: :group__planners + :duration: 118 + :schedule_manually: false + :relations: + - :to: :design_freeze + :type: follows :children: - - :start: 98 + - :start: 91 :t_subject: Start constructing :reference: :start_constructing :t_description: |- @@ -732,7 +748,7 @@ projects: :type: :default_type_task :assigned_to: :group__planners :duration: 31 - - :start: 130 + - :start: 122 :t_subject: Foundation :t_description: |- ## Goal @@ -749,6 +765,7 @@ projects: :type: :default_type_task :assigned_to: :group__planners :duration: 25 + :schedule_manually: false :relations: - :to: :start_constructing :type: follows @@ -825,7 +842,7 @@ projects: :status: :default_status_new :type: :default_type_task :assigned_to: :group__planners - :duration: 18 + :duration: 19 - :start: 208 :t_subject: House warming party :t_description: |- @@ -841,14 +858,10 @@ projects: :status: :default_status_new :type: :default_type_milestone :duration: 1 + :schedule_manually: false :relations: - :to: :final_touches :type: follows - :assigned_to: :group__planners - :duration: 110 - :relations: - - :to: :design_freeze - :type: follows demo-bim-project: t_name: (Demo) BIM project identifier: demo-bim-project @@ -1010,6 +1023,8 @@ projects: and "Request", thus represents a general note. :status: :default_status_new :type: :default_type_phase + :duration: 38 + :schedule_manually: false :children: - :start: 15 :t_subject: Gathering the project specific data and information for the BIM model @@ -1031,11 +1046,12 @@ projects: :status: :default_status_new :type: :default_type_task :assigned_to: :group__planners - :duration: 14 + :duration: 11 + :schedule_manually: false :relations: - :to: :project_kick_off_creating_bim_model :type: follows - - :start: 33 + - :start: 28 :t_subject: Creating the BIM execution plan :reference: :creating_bim_execution_plan :t_description: |- @@ -1052,24 +1068,24 @@ projects: :status: :default_status_new :type: :default_type_task :assigned_to: :group__lead_bim_coordinators - :duration: 18 + :duration: 24 + :schedule_manually: false :relations: - :to: :gathering_project_data_and_info :type: follows - :start: 52 :t_subject: Completion of the BIM execution plan - :reference: :completion_of_bim_execution_plan :t_description: This type is hierarchically a parent of the types "Clash" and "Request", thus represents a general note. :status: :default_status_resolved :type: :default_type_milestone :assigned_to: :group__bim_coordinators :duration: 1 + :schedule_manually: false :relations: - :to: :creating_bim_execution_plan :type: follows :assigned_to: :group__bim_managers - :duration: 37 - :start: 53 :t_subject: End of preparation phase :reference: :end_of_preparation_phase @@ -1079,17 +1095,24 @@ projects: :type: :default_type_milestone :assigned_to: :group__bim_managers :duration: 1 + :schedule_manually: false :relations: - :to: :project_preparation :type: follows - - :start: 54 + - :start: 56 :t_subject: Creating initial BIM model :t_description: This type is hierarchically a parent of the types "Clash" and "Request", thus represents a general note. :status: :default_status_new :type: :default_type_phase + :assigned_to: :group__architects + :duration: 10 + :schedule_manually: false + :relations: + - :to: :end_of_preparation_phase + :type: follows :children: - - :start: 54 + - :start: 56 :t_subject: Modelling initial BIM model :reference: :modelling_initial_bim_model :t_description: |- @@ -1106,11 +1129,12 @@ projects: :status: :default_status_new :type: :default_type_task :assigned_to: :group__bim_modellers - :duration: 7 + :duration: 5 + :schedule_manually: false :relations: - :to: :end_of_preparation_phase :type: follows - - :start: 62 + - :start: 63 :t_subject: Initial, internal model check and revising :reference: :initial_internal_model_check_and_revising :t_description: |- @@ -1126,6 +1150,7 @@ projects: :type: :default_type_task :assigned_to: :group__lead_bim_coordinators :duration: 2 + :schedule_manually: false :relations: - :to: :modelling_initial_bim_model :type: follows @@ -1138,20 +1163,22 @@ projects: :type: :default_type_milestone :assigned_to: :group__bim_modellers :duration: 1 + :schedule_manually: false :relations: - :to: :initial_internal_model_check_and_revising :type: follows - :assigned_to: :group__architects - :duration: 11 - :relations: - - :to: :completion_of_bim_execution_plan - :type: follows - :start: 66 :t_subject: Modelling, first cycle :t_description: This type is hierarchically a parent of the types "Clash" and "Request", thus represents a general note. :status: :default_status_new :type: :default_type_phase + :assigned_to: :group__bim_coordinators + :duration: 9 + :schedule_manually: false + :relations: + - :to: :submitting_initial_bim_model + :type: follows :children: - :start: 66 :t_subject: Referencing external BIM models @@ -1170,6 +1197,7 @@ projects: :type: :default_type_task :assigned_to: :group__bim_modellers :duration: 1 + :schedule_manually: false :relations: - :to: :submitting_initial_bim_model :type: follows @@ -1190,10 +1218,11 @@ projects: :type: :default_type_task :assigned_to: :group__bim_modellers :duration: 6 + :schedule_manually: false :relations: - :to: :referencing_external_bim_models :type: follows - - :start: 74 + - :start: 73 :t_subject: First Cycle, internal model check and revising :reference: :first_cycle_internal_model_check_and_revising :t_description: |- @@ -1209,10 +1238,11 @@ projects: :type: :default_type_task :assigned_to: :group__bim_coordinators :duration: 1 + :schedule_manually: false :relations: - :to: :modelling_of_the_bim_model :type: follows - - :start: 76 + - :start: 74 :t_subject: Submitting BIM model :reference: :submitting_bim_model :t_description: This type is hierarchically a parent of the types "Clash" @@ -1221,20 +1251,22 @@ projects: :type: :default_type_milestone :assigned_to: :group__bim_modellers :duration: 1 + :schedule_manually: false :relations: - :to: :first_cycle_internal_model_check_and_revising :type: follows - :assigned_to: :group__bim_coordinators - :duration: 10 - :relations: - - :to: :submitting_initial_bim_model - :type: follows - :start: 77 :t_subject: Coordination, first cycle :t_description: This type is hierarchically a parent of the types "Clash" and "Request", thus represents a general note. :status: :default_status_new :type: :default_type_phase + :assigned_to: :group__lead_bim_coordinators + :duration: 5 + :schedule_manually: false + :relations: + - :to: :submitting_bim_model + :type: follows :children: - :start: 77 :t_subject: Coordinate the different BIM models @@ -1261,7 +1293,8 @@ projects: :type: :default_type_phase :assigned_to: :group__bim_coordinators :duration: 4 - - :start: 82 + :schedule_manually: false + - :start: 81 :t_subject: Finishing coordination, first cycle :reference: :finishing_coordination_first_cycle :t_description: This type is hierarchically a parent of the types "Clash" @@ -1270,15 +1303,11 @@ projects: :type: :default_type_milestone :assigned_to: :group__lead_bim_coordinators :duration: 1 + :schedule_manually: false :relations: - :to: :issue_management_first_cycle :type: follows - :assigned_to: :group__lead_bim_coordinators - :duration: 5 - :relations: - - :to: :submitting_bim_model - :type: follows - - :start: 83 + - :start: 84 :t_subject: Modelling & coordinating, second cycle :reference: :modelling_and_coordinating_second_cycle :t_description: "## Goal\r\n\r\n* ...\r\n\r\n## Description\r\n\r\n* ..." @@ -1286,10 +1315,11 @@ projects: :type: :default_type_phase :assigned_to: :group__lead_bim_coordinators :duration: 1 + :schedule_manually: false :relations: - :to: :finishing_coordination_first_cycle :type: follows - - :start: 84 + - :start: 85 :t_subject: Modelling & coordinating, ... cycle :reference: :modelling_and_coordinating_another_cycle :t_description: This type is hierarchically a parent of the types "Clash" @@ -1298,10 +1328,11 @@ projects: :type: :default_type_phase :assigned_to: :group__lead_bim_coordinators :duration: 1 + :schedule_manually: false :relations: - :to: :modelling_and_coordinating_second_cycle :type: follows - - :start: 85 + - :start: 86 :t_subject: Modelling & coordinating, (n-th minus 1) cycle :reference: :modelling_and_coordinating_penultimate_cycle :t_description: "## Goal\r\n\r\n* ...\r\n\r\n## Description\r\n\r\n* ..." @@ -1309,10 +1340,11 @@ projects: :type: :default_type_phase :assigned_to: :group__lead_bim_coordinators :duration: 1 + :schedule_manually: false :relations: - :to: :modelling_and_coordinating_another_cycle :type: follows - - :start: 86 + - :start: 87 :t_subject: Modelling & coordinating n-th cycle :reference: :modelling_and_coordinating_last_cycle :t_description: This type is hierarchically a parent of the types "Clash" @@ -1321,10 +1353,11 @@ projects: :type: :default_type_phase :assigned_to: :group__bim_coordinators :duration: 1 + :schedule_manually: false :relations: - :to: :modelling_and_coordinating_penultimate_cycle :type: follows - - :start: 87 + - :start: 88 :t_subject: Finishing modelling & coordinating, n-th cycle :reference: :finishing_modelling_and_coordinating :t_description: This type is hierarchically a parent of the types "Clash" @@ -1333,16 +1366,23 @@ projects: :type: :default_type_milestone :assigned_to: :group__bim_coordinators :duration: 1 + :schedule_manually: false :relations: - :to: :modelling_and_coordinating_last_cycle :type: follows - - :start: 88 + - :start: 91 :t_subject: Use model for construction phase :description: '' :status: :default_status_new :type: :default_type_phase + :assigned_to: :group__bim_coordinators + :duration: 110 + :schedule_manually: false + :relations: + - :to: :finishing_modelling_and_coordinating + :type: follows :children: - - :start: 88 + - :start: 91 :t_subject: Handover model for construction crew :t_description: |- ## Goal @@ -1377,7 +1417,7 @@ projects: :status: :default_status_new :type: :default_type_task :assigned_to: :group__bim_coordinators - :duration: 101 + :duration: 102 - :start: 200 :t_subject: Finish construction :reference: :finish_construction @@ -1386,14 +1426,10 @@ projects: :type: :default_type_milestone :assigned_to: :group__bim_coordinators :duration: 1 + :schedule_manually: false :relations: - :to: :construct_building :type: follows - :assigned_to: :group__bim_coordinators - :duration: 112 - :relations: - - :to: :finishing_modelling_and_coordinating - :type: follows - :start: 98 :t_subject: Issue management, construction phase :reference: :issue_management_construction_phase @@ -1402,7 +1438,8 @@ projects: :type: :default_type_phase :assigned_to: :group__planners :duration: 88 - - :start: 210 + :schedule_manually: false + - :start: 203 :t_subject: Handover for Facility Management :reference: :handover_for_facility_management :t_description: |- @@ -1421,15 +1458,17 @@ projects: :type: :default_type_milestone :assigned_to: :group__lead_bim_coordinators :duration: 1 + :schedule_manually: false :relations: - :to: :finish_construction :type: follows - - :start: 211 + - :start: 204 :t_subject: Asset Management :t_description: Enjoy your building :) :status: :default_status_new :type: :default_type_phase :duration: 1 + :schedule_manually: false :relations: - :to: :handover_for_facility_management :type: follows diff --git a/modules/bim/spec/seeders/root_seeder_bim_edition_spec.rb b/modules/bim/spec/seeders/root_seeder_bim_edition_spec.rb index 892c1ce4ebce..2a62148f5672 100644 --- a/modules/bim/spec/seeders/root_seeder_bim_edition_spec.rb +++ b/modules/bim/spec/seeders/root_seeder_bim_edition_spec.rb @@ -36,6 +36,10 @@ with_config: { edition: "bim" } do include RootSeederTestHelpers + before_all do + week_with_saturday_and_sunday_as_weekend + end + shared_examples "creates BIM demo data" do def group_name(reference) root_seeder.seed_data.find_reference(reference)["name"] @@ -104,6 +108,7 @@ def group_name(reference) include_examples "it creates records", model: Status, expected_count: 4 include_examples "it creates records", model: TimeEntryActivity, expected_count: 3 include_examples "it creates records", model: Workflow, expected_count: 273 + include_examples "it is compatible with the automatic scheduling mode" end describe "demo data" do diff --git a/spec/seeders/demo_data/work_package_seeder_spec.rb b/spec/seeders/demo_data/work_package_seeder_spec.rb index b4c4be56a677..1887407ebe97 100644 --- a/spec/seeders/demo_data/work_package_seeder_spec.rb +++ b/spec/seeders/demo_data/work_package_seeder_spec.rb @@ -194,6 +194,32 @@ def work_package_data(**attributes) end end + context "with work package data with schedule_manually" do + let(:work_packages_data) do + [ + work_package_data(schedule_manually: false), + work_package_data(schedule_manually: true) + ] + end + + it "sets schedule_manually to the given value" do + expect(WorkPackage.first.schedule_manually).to be(false) + expect(WorkPackage.second.schedule_manually).to be(true) + end + end + + context "with work package data without schedule_manually" do + let(:work_packages_data) do + [ + work_package_data(schedule_manually: nil) + ] + end + + it "sets schedule_manually to true, its default value" do + expect(WorkPackage.first.schedule_manually).to be(true) + end + end + context "with a parent relation by reference" do let(:work_packages_data) do [ @@ -204,7 +230,8 @@ def work_package_data(**attributes) it "creates a parent-child relation between work packages" do expect(WorkPackage.count).to eq(2) - expect(WorkPackage.second.parent).to eq(WorkPackage.first) + parent, child = WorkPackage.order(:id).to_a + expect(child.parent).to eq(parent) end end @@ -244,6 +271,26 @@ def work_package_data(**attributes) end end + context "with a relations array" do + let(:work_packages_data) do + [ + work_package_data(subject: "predecessor", reference: :predecessor), + work_package_data(subject: "related", reference: :related), + work_package_data(subject: "successor", relations: [{ to: :predecessor, type: "follows" }, + { to: :related, type: "relates" }]) + ] + end + + it "creates relations between work packages" do + expect(WorkPackage.count).to eq(3) + predecessor, related, successor = WorkPackage.order(:id).to_a + expect(successor.relations.pluck(:relation_type, :to_id)).to contain_exactly( + ["follows", predecessor.id], + ["relates", related.id] + ) + end + end + context "with a work package description referencing a work package with ##wp:ref notation" do let(:work_packages_data) do [ diff --git a/spec/seeders/root_seeder_shared_examples.rb b/spec/seeders/root_seeder_shared_examples.rb index 0b025afe9f52..4fdf7b29ae3d 100644 --- a/spec/seeders/root_seeder_shared_examples.rb +++ b/spec/seeders/root_seeder_shared_examples.rb @@ -28,6 +28,8 @@ # See COPYRIGHT and LICENSE files for more details. #++ +Date::DATE_FORMATS[:wday_iso_date] = "%a %Y-%m-%d" # Fri 2022-08-05 + RSpec.shared_examples "no email deliveries" do it "does not perform any email deliveries" do perform_enqueued_jobs @@ -42,3 +44,56 @@ expect(model.count).to eq(expected_count) end end + +RSpec.shared_examples "it is compatible with the automatic scheduling mode" do + # rubocop:disable Layout/LineContinuationLeadingSpace + it "has successors in automatic mode with dates matching closest predecessor's dates and relation lag", :aggregate_failures do + days = WorkPackages::Shared::WorkingDays.new + relations = Relation.follows.includes(:from, :to).to_a + .sort_by!(&:predecessor_date) + .reverse! + .uniq(&:successor_id) + relations.each do |relation| + predecessor = relation.to + successor = relation.from + pred_date = relation.predecessor_date + succ_date = relation.successor_date + next if pred_date.nil? && succ_date.nil? + + expect(successor).to be_schedule_automatically, + "Expected successor '#{successor.subject}' to be scheduled automatically" + + expected_lag = days.lag(pred_date, succ_date) + relation_lag = relation.lag || 0 + message = + "Relation from predecessor '#{predecessor.subject}' (date: #{pred_date.to_fs(:wday_iso_date)})\n" \ + " to successor '#{successor.subject}' (date: #{succ_date.to_fs(:wday_iso_date)})\n" \ + "has invalid lag #{relation_lag} (expected #{expected_lag})\n" \ + "Try the following:\n" \ + "- Adjust successor dates to start #{(succ_date - days.soonest_working_day(pred_date + 1)).to_i} day(s) earlier\n" \ + "- Increase predecessor duration by #{expected_lag - relation_lag}\n" + expect(expected_lag).to eq(relation_lag), message + end + end + + it "has parents in automatic mode with dates matching children's dates", :aggregate_failures do + parents = WorkPackage.where(id: WorkPackage.select(:parent_id).distinct).includes(:children) + parents.each do |parent| + expected_start_date = parent.children.minimum(:start_date) + if expected_start_date + expect(parent.start_date) + .to eq(expected_start_date), + "Expected parent '#{parent.subject}' to have start date #{expected_start_date.to_fs(:wday_iso_date)}" + end + expected_due_date = parent.children.maximum(:due_date) + if expected_due_date + expect(parent.due_date) + .to eq(expected_due_date), + "Expected parent '#{parent.subject}' to have due date #{expected_due_date.to_fs(:wday_iso_date)}" + end + expect(parent).to be_schedule_automatically, + "Expected parent '#{parent.subject}' to be scheduled automatically" + end + end + # rubocop:enable Layout/LineContinuationLeadingSpace +end diff --git a/spec/seeders/root_seeder_standard_edition_spec.rb b/spec/seeders/root_seeder_standard_edition_spec.rb index 9f416a03afe6..828db18f39de 100644 --- a/spec/seeders/root_seeder_standard_edition_spec.rb +++ b/spec/seeders/root_seeder_standard_edition_spec.rb @@ -36,6 +36,10 @@ with_config: { edition: "standard" } do include RootSeederTestHelpers + before_all do + week_with_saturday_and_sunday_as_weekend + end + shared_examples "creates standard demo data" do it "creates the system user" do expect(SystemUser.where(admin: true).count).to eq 1 @@ -137,6 +141,7 @@ include_examples "it creates records", model: TimeEntryActivity, expected_count: 6 include_examples "it creates records", model: Workflow, expected_count: 1758 include_examples "it creates records", model: Meeting, expected_count: 1 + include_examples "it is compatible with the automatic scheduling mode" end describe "demo data" do diff --git a/spec/services/work_packages/shared/all_days_spec.rb b/spec/services/work_packages/shared/all_days_spec.rb index 1be534fc3f54..26e3a8a8e861 100644 --- a/spec/services/work_packages/shared/all_days_spec.rb +++ b/spec/services/work_packages/shared/all_days_spec.rb @@ -67,6 +67,8 @@ end end + include_examples "lag computation excluding non-working days" + describe "#start_date" do it "returns the start date for a due date and a duration" do expect(subject.start_date(sunday_2022_07_31, 1)).to eq(sunday_2022_07_31) diff --git a/spec/services/work_packages/shared/shared_examples_days.rb b/spec/services/work_packages/shared/shared_examples_days.rb index 5317c5805902..513107a5fdce 100644 --- a/spec/services/work_packages/shared/shared_examples_days.rb +++ b/spec/services/work_packages/shared/shared_examples_days.rb @@ -71,6 +71,19 @@ end end +RSpec.shared_examples "it returns lag" do |expected_lag, predecessor_date, successor_date| + from_date_format = "%a %-d" + from_date_format += " %b" if [predecessor_date.month, predecessor_date.year] != [successor_date.month, successor_date.year] + from_date_format += " %Y" if predecessor_date.year != successor_date.year + + it "from predecessor date #{predecessor_date.strftime(from_date_format)} " \ + "to successor date #{successor_date.to_fs(:wday_date)} " \ + "=> #{expected_lag}" \ + do + expect(subject.lag(predecessor_date, successor_date)).to eq(expected_lag) + end +end + RSpec.shared_examples "start_date" do |due_date:, duration:, expected:| it "start_date(#{due_date.to_fs(:wday_date)}, #{duration}) => #{expected.to_fs(:wday_date)}" do expect(subject.start_date(due_date, duration)).to eq(expected) @@ -94,3 +107,67 @@ expect(subject.soonest_working_day(date, lag:)).to eq(expected) end end + +RSpec.shared_examples "lag computation excluding non-working days" do + describe "#lag" do + sunday_2022_07_31 = Date.new(2022, 7, 31) + monday_2022_08_01 = Date.new(2022, 8, 1) + wednesday_2022_08_03 = Date.new(2022, 8, 3) + + it "returns the working days between a predecessor date and successor date" do + expect(subject.lag(sunday_2022_07_31, sunday_2022_07_31 + 6)).to eq(5) + end + + it "can't be negative" do + expect(subject.lag(sunday_2022_07_31, sunday_2022_07_31 + 1)).to eq(0) + expect(subject.lag(sunday_2022_07_31, sunday_2022_07_31)).to eq(0) + expect(subject.lag(sunday_2022_07_31, sunday_2022_07_31 - 1)).to eq(0) + end + + context "without any week days created" do + it "considers all days as working days and returns the number of days between two dates, exclusive" do + expect(subject.lag(sunday_2022_07_31, sunday_2022_07_31 + 6)).to eq(5) + expect(subject.lag(sunday_2022_07_31, sunday_2022_07_31 + 50)).to eq(49) + end + end + + context "with weekend days (Saturday and Sunday)", :weekend_saturday_sunday do + include_examples "it returns lag", 0, sunday_2022_07_31, monday_2022_08_01 + include_examples "it returns lag", 4, sunday_2022_07_31, Date.new(2022, 8, 5) # Friday + include_examples "it returns lag", 5, sunday_2022_07_31, Date.new(2022, 8, 6) # Saturday + include_examples "it returns lag", 5, sunday_2022_07_31, Date.new(2022, 8, 7) # Sunday + include_examples "it returns lag", 5, sunday_2022_07_31, Date.new(2022, 8, 8) # Monday + include_examples "it returns lag", 6, sunday_2022_07_31, Date.new(2022, 8, 9) # Tuesday + + include_examples "it returns lag", 3, monday_2022_08_01, Date.new(2022, 8, 5) # Friday + include_examples "it returns lag", 4, monday_2022_08_01, Date.new(2022, 8, 6) # Saturday + include_examples "it returns lag", 4, monday_2022_08_01, Date.new(2022, 8, 7) # Sunday + include_examples "it returns lag", 4, monday_2022_08_01, Date.new(2022, 8, 8) # Monday + include_examples "it returns lag", 5, monday_2022_08_01, Date.new(2022, 8, 9) # Tuesday + + include_examples "it returns lag", 1, wednesday_2022_08_03, Date.new(2022, 8, 5) # Friday + include_examples "it returns lag", 2, wednesday_2022_08_03, Date.new(2022, 8, 6) # Saturday + include_examples "it returns lag", 2, wednesday_2022_08_03, Date.new(2022, 8, 7) # Sunday + include_examples "it returns lag", 2, wednesday_2022_08_03, Date.new(2022, 8, 8) # Monday + include_examples "it returns lag", 3, wednesday_2022_08_03, Date.new(2022, 8, 9) # Tuesday + end + + context "with some non working days (Christmas 2022-12-25 and new year's day 2023-01-01)", :christmas_2022_new_year_2023 do + include_examples "it returns lag", 0, Date.new(2022, 12, 24), Date.new(2022, 12, 26) + include_examples "it returns lag", 1, Date.new(2022, 12, 24), Date.new(2022, 12, 27) + include_examples "it returns lag", 6, Date.new(2022, 12, 24), Date.new(2023, 1, 2) + end + + context "without predecessor date" do + it "returns nil" do + expect(subject.lag(nil, sunday_2022_07_31)).to be_nil + end + end + + context "without successor date" do + it "returns nil" do + expect(subject.lag(sunday_2022_07_31, nil)).to be_nil + end + end + end +end diff --git a/spec/services/work_packages/shared/working_days_spec.rb b/spec/services/work_packages/shared/working_days_spec.rb index d8132903a99d..e3c2d5e1feb5 100644 --- a/spec/services/work_packages/shared/working_days_spec.rb +++ b/spec/services/work_packages/shared/working_days_spec.rb @@ -91,6 +91,8 @@ end end + include_examples "lag computation excluding non-working days" + describe "#start_date" do it "returns the start date for a due date and a duration" do expect(subject.start_date(monday_2022_08_01, 1)).to eq(monday_2022_08_01)