diff --git a/app/services/work_packages/update_ancestors/loader.rb b/app/services/work_packages/update_ancestors/loader.rb index 966a04fd4620..dcf4ae002740 100644 --- a/app/services/work_packages/update_ancestors/loader.rb +++ b/app/services/work_packages/update_ancestors/loader.rb @@ -30,6 +30,8 @@ class WorkPackages::UpdateAncestors::Loader parent_id: "parent_id", estimated_hours: "estimated_hours", remaining_hours: "remaining_hours", + done_ratio: "done_ratio", + derived_done_ratio: "derived_done_ratio", status_excluded_from_totals: "statuses.excluded_from_totals", schedule_manually: "schedule_manually", ignore_non_working_days: "ignore_non_working_days" diff --git a/app/services/work_packages/update_ancestors_service.rb b/app/services/work_packages/update_ancestors_service.rb index b6f0c3e0d78f..b08e756a90f5 100644 --- a/app/services/work_packages/update_ancestors_service.rb +++ b/app/services/work_packages/update_ancestors_service.rb @@ -120,13 +120,27 @@ def derive_done_ratio(ancestor, loader) end def compute_derived_done_ratio(work_package, loader) - return if work_package.derived_estimated_hours.nil? || work_package.derived_remaining_hours.nil? - return if work_package.derived_estimated_hours.zero? return if no_children?(work_package, loader) - work_done = (work_package.derived_estimated_hours - work_package.derived_remaining_hours) - progress = (work_done.to_f / work_package.derived_estimated_hours) * 100 - progress.round + if Setting.total_percent_complete_mode == "work_weighted_average" + return if work_package.derived_estimated_hours.nil? || work_package.derived_remaining_hours.nil? + return if work_package.derived_estimated_hours.zero? + + work_done = (work_package.derived_estimated_hours - work_package.derived_remaining_hours) + progress = (work_done.to_f / work_package.derived_estimated_hours) * 100 + progress.round + elsif Setting.total_percent_complete_mode == "simple_average" + all_done_ratios = loader + .children_of(work_package) + .map { |child| child.done_ratio || 0 } + + if work_package.done_ratio.present? + all_done_ratios << work_package.done_ratio + end + + progress = all_done_ratios.sum.to_f / all_done_ratios.count + progress.round + end end # Sets the ignore_non_working_days to true if any descendant has its value set to true. diff --git a/spec/services/work_packages/update_ancestors_service_spec.rb b/spec/services/work_packages/update_ancestors_service_spec.rb index 98f6fb1ec3fc..dbf020659f53 100644 --- a/spec/services/work_packages/update_ancestors_service_spec.rb +++ b/spec/services/work_packages/update_ancestors_service_spec.rb @@ -28,7 +28,8 @@ require "spec_helper" -RSpec.describe WorkPackages::UpdateAncestorsService, type: :model do +RSpec.describe WorkPackages::UpdateAncestorsService, + type: :model do shared_association_default(:author, factory_name: :user) { create(:user) } shared_association_default(:project_with_types) { create(:project_with_types) } shared_association_default(:priority) { create(:priority) } @@ -795,6 +796,190 @@ def call_update_ancestors_service(work_package) end end + describe "simple average mode for total % complete calculation", + with_settings: { total_percent_complete_mode: "simple_average" } do + subject(:call_result) do + described_class.new(user:, work_package: parent) + .call(%i(remaining_hours)) + end + + context "with parent and all children all values set" do + let_work_packages(<<~TABLE) + hierarchy | work | ∑ work | remaining work | ∑ remaining work | % complete | ∑ % complete + parent | 10h | 40h | 7.5h | 22.5h | 25% | + child1 | 15h | | 10h | | 33% | + child2 | 5h | | 2.5h | | 50% | + child3 | 10h | | 2.5h | | 75% | + TABLE + + it "sets the total % complete solely based on % complete values of children and parent" do + expect(call_result).to be_success + updated_work_packages = call_result.all_results + expect_work_packages(updated_work_packages, <<~TABLE) + subject | total % complete + parent | 46% + TABLE + end + end + + context "with parent having no values and children having all values set" do + let_work_packages(<<~TABLE) + hierarchy | work | ∑ work | remaining work | ∑ remaining work | % complete | ∑ % complete + parent | | 30h | | 15h | | + child1 | 15h | | 10h | | 33% | + child2 | 5h | | 2.5h | | 50% | + child3 | 10h | | 2.5h | | 75% | + TABLE + + it "sets the total % complete solely based on % complete values of children" do + expect(call_result).to be_success + updated_work_packages = call_result.all_results + expect_work_packages(updated_work_packages, <<~TABLE) + subject | total % complete + parent | 53% + TABLE + end + end + + context "with parent having no values set " \ + "and some children having work and remaining work set " \ + "and all children having % complete set" do + let_work_packages(<<~TABLE) + hierarchy | work | ∑ work | remaining work | ∑ remaining work | % complete | ∑ % complete + parent | | 25h | | 12.5h | | + child1 | 15h | | 10h | | 33% | + child2 | | | | | 75% | + child3 | 10h | | 2.5h | | 75% | + TABLE + + it "sets the total % complete solely based on % complete values of children" do + expect(call_result).to be_success + updated_work_packages = call_result.all_results + expect_work_packages(updated_work_packages, <<~TABLE) + subject | total % complete + parent | 61% + TABLE + end + end + + context "with parent having no values set " \ + "and no children having work and remaining work set " \ + "and all children having % complete set" do + let_work_packages(<<~TABLE) + hierarchy | work | ∑ work | remaining work | ∑ remaining work | % complete | ∑ % complete + parent | | | | | | + child1 | | | | | 100% | + child2 | | | | | 100% | + child3 | | | | | 75% | + TABLE + + it "sets the total % complete solely based on % complete values of children" do + expect(call_result).to be_success + updated_work_packages = call_result.all_results + expect_work_packages(updated_work_packages, <<~TABLE) + subject | total % complete + parent | 92% + TABLE + end + end + + context "with parent having no values set " \ + "and no children having work and remaining work set " \ + "and some children having % complete set" do + let_work_packages(<<~TABLE) + hierarchy | work | ∑ work | remaining work | ∑ remaining work | % complete | ∑ % complete + parent | | | | | | + child1 | | | | | 100% | + child2 | | | | | | + child3 | | | | | 75% | + TABLE + + it "sets the total % complete solely based on % complete values of children " \ + "and accounts unset values in children as 0" do + expect(call_result).to be_success + updated_work_packages = call_result.all_results + expect_work_packages(updated_work_packages, <<~TABLE) + subject | total % complete + parent | 58% + TABLE + end + end + + context "with parent having % complete set " \ + "and no children having work and remaining work set " \ + "and some children having % complete set" do + let_work_packages(<<~TABLE) + hierarchy | work | ∑ work | remaining work | ∑ remaining work | % complete | ∑ % complete + parent | | | | | 10% | + child1 | | | | | 100% | + child2 | | | | | | + child3 | | | | | 75% | + TABLE + + it "sets the total % complete based on % complete values of children and parent " \ + "and accounts unset values in children as 0" do + expect(call_result).to be_success + updated_work_packages = call_result.all_results + expect_work_packages(updated_work_packages, <<~TABLE) + subject | total % complete + parent | 46% + TABLE + end + end + + context "with parent having no values set " \ + "and a multi-level children hierarchy with all values set" do + let_work_packages(<<~TABLE) + hierarchy | work | ∑ work | remaining work | ∑ remaining work | % complete | ∑ % complete + parent | | 44h | | 21h | | 52% + child1 | 15h | 23h | 10h | 14h | 33% | 43% + grandchild1 | 5h | | 3h | | 40% | + grandchild2 | 3h | | 1h | | 67% | + child2 | 5h | 7h | 2.5h | 3.5h | 50% | 50% + grandchild3 | 2h | | 1h | | 50% | + child3 | 10h | 14h | 2.5h | 3.5h | 75% | 75% + grandchild4 | 4h | | 1h | | 75% | + TABLE + + it "sets the total % complete solely based on % complete values of direct children" do + expect(call_result).to be_success + updated_work_packages = call_result.all_results + expect_work_packages(updated_work_packages, <<~TABLE) + subject | total % complete + parent | 53% + TABLE + end + end + end + + describe "work weighted average mode for total % complete calculation", + with_settings: { total_percent_complete_mode: "work_weighted_average" } do + subject(:call_result) do + described_class.new(user:, work_package: parent) + .call(%i(remaining_hours)) + end + + context "with parent and all children all values set" do + let_work_packages(<<~TABLE) + hierarchy | work | remaining work | % complete + parent | 10h | 7.5h | 25% + child1 | 15h | 10h | 33% + child2 | 5h | 2.5h | 50% + child3 | 10h | 2.5h | 75% + TABLE + + it "sets the total % complete accounting for work values as weights" do + pending "TODO" + expect(call_result).to be_success + updated_work_packages = call_result.all_results + expect_work_packages(updated_work_packages, <<~TABLE) + subject | total % complete + parent | 46% + TABLE + end + end + end + describe "ignore_non_working_days propagation" do shared_let(:grandgrandparent) do create(:work_package,