Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[#57529] Add options for sum totals calculation mode in hierarchies #16649

Merged
Merged
Show file tree
Hide file tree
Changes from 11 commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
291b87b
Create setting for Calculation of % Complete hierarchy totals
aaron-contreras Sep 6, 2024
b92e9d1
Incorporate simple average mode into total % complete calculations
aaron-contreras Sep 10, 2024
53a3a72
Adjust selection of done ratio based on new rules for children
aaron-contreras Sep 16, 2024
793d758
Add radio group to progress tracking form
aaron-contreras Sep 16, 2024
06bd83f
Add feature spec for setting toggling
aaron-contreras Sep 17, 2024
503ad15
Add recalculation in job when switching to work weighted mode
aaron-contreras Sep 19, 2024
5e36ce2
Re-order radio group options according to design document
aaron-contreras Sep 20, 2024
6cbfdc8
Trigger the recalculation job when changing to work weighted mode
aaron-contreras Sep 20, 2024
2f2561c
Add in simple average calculation mode job
aaron-contreras Sep 23, 2024
1f12393
Account for status exclusion from total calculations in simple averag…
aaron-contreras Sep 24, 2024
1874fb3
Fix `status_excluded_from_totals` not existing for `WorkPackage`
cbliard Sep 25, 2024
f040848
Fix when all work packages of a hierarchy are excluded from totals
cbliard Sep 25, 2024
6d9a9c8
Removed flaky spec
cbliard Sep 25, 2024
447a6de
Fix rubocop warnings Metrics/AbcSize and Metrics/PerceivedComplexity
cbliard Sep 25, 2024
0f9be1b
Add a test when all hierarchy is excluded from totals
cbliard Sep 25, 2024
a574b2f
Add failing spec about handling simple average mode in a job
cbliard Sep 25, 2024
3f97171
Use work_package_hierarchies to create depth table
cbliard Sep 25, 2024
3250f51
Compute total % complete according to mode in update status job
cbliard Sep 25, 2024
946b57d
Merge branch 'dev' into implementation/57529-options-for-total-calcul…
aaron-contreras Sep 25, 2024
0edb100
Add extra spec specifically accounting for excluded statuses
aaron-contreras Sep 25, 2024
5b1dbb4
Test for unset total values
aaron-contreras Sep 25, 2024
95cda6d
Remove unnecessary update
aaron-contreras Sep 25, 2024
de86221
Extract common operations to sql_commands
aaron-contreras Sep 25, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions app/models/journal.rb
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,7 @@ class Journal < ApplicationRecord
progress_mode_changed_to_status_based
status_changed
system_update
total_percent_complete_mode_changed_to_work_weighted_average
work_package_children_changed_times
work_package_parent_changed_times
work_package_predecessor_changed_times
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
#-- copyright
# OpenProject is an open source project management software.
# Copyright (C) the OpenProject GmbH
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License version 3.
#
# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
# Copyright (C) 2006-2013 Jean-Philippe Lang
# Copyright (C) 2010-2013 the ChiliProject Team
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
#
# See COPYRIGHT and LICENSE files for more details.
#++

class Journal::CausedByTotalPercentCompleteModeChangedToSimpleAverage < CauseOfChange::Base
def initialize
super("total_percent_complete_mode_changed_to_simple_average")
end
end
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
#-- copyright
# OpenProject is an open source project management software.
# Copyright (C) the OpenProject GmbH
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License version 3.
#
# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
# Copyright (C) 2006-2013 Jean-Philippe Lang
# Copyright (C) 2010-2013 the ChiliProject Team
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
#
# See COPYRIGHT and LICENSE files for more details.
#++

class Journal::CausedByTotalPercentCompleteModeChangedToWorkWeightedAverage < CauseOfChange::Base
def initialize
super("total_percent_complete_mode_changed_to_work_weighted_average")
end
end
1 change: 1 addition & 0 deletions app/models/work_package.rb
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ class WorkPackage < ApplicationRecord
include OpenProject::Journal::AttachmentHelper

DONE_RATIO_OPTIONS = %w[field status].freeze
TOTAL_PERCENT_COMPLETE_MODE_OPTIONS = %w[work_weighted_average simple_average].freeze

belongs_to :project
belongs_to :type
Expand Down
8 changes: 8 additions & 0 deletions app/services/settings/update_service.rb
Original file line number Diff line number Diff line change
Expand Up @@ -41,14 +41,22 @@

private

def set_setting_value(name, value)
old_value = Setting[name]
new_value = derive_value(value)
Setting[name] = new_value
if name == :work_package_done_ratio && old_value != "status" && new_value == "status"
WorkPackages::Progress::ApplyStatusesChangeJob.perform_later(cause_type: "progress_mode_changed_to_status_based")
elsif name == :total_percent_complete_mode && old_value != "work_weighted_average" && new_value == "work_weighted_average"
WorkPackages::Progress::ApplyTotalPercentCompleteModeChangeJob
.perform_later(mode: new_value,
cause_type: "total_percent_complete_mode_changed_to_work_weighted_average")
cbliard marked this conversation as resolved.
Show resolved Hide resolved
elsif name == :total_percent_complete_mode && old_value != "simple_average" && new_value == "simple_average"
WorkPackages::Progress::ApplyTotalPercentCompleteModeChangeJob
.perform_later(mode: new_value,
cause_type: "total_percent_complete_mode_changed_to_simple_average")
end
end

Check notice on line 59 in app/services/settings/update_service.rb

View workflow job for this annotation

GitHub Actions / rubocop

[rubocop] app/services/settings/update_service.rb#L44-L59 <Metrics/AbcSize>

Assignment Branch Condition size for set_setting_value is too high. [<3, 6, 18> 19.21/17]
Raw output
app/services/settings/update_service.rb:44:3: C: Metrics/AbcSize: Assignment Branch Condition size for set_setting_value is too high. [<3, 6, 18> 19.21/17]

Check notice on line 59 in app/services/settings/update_service.rb

View workflow job for this annotation

GitHub Actions / rubocop

[rubocop] app/services/settings/update_service.rb#L44-L59 <Metrics/PerceivedComplexity>

Perceived complexity for set_setting_value is too high. [11/8]
Raw output
app/services/settings/update_service.rb:44:3: C: Metrics/PerceivedComplexity: Perceived complexity for set_setting_value is too high. [11/8]

def derive_value(value)
case value
Expand Down
2 changes: 2 additions & 0 deletions app/services/work_packages/update_ancestors/loader.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
30 changes: 29 additions & 1 deletion app/services/work_packages/update_ancestors_service.rb
Original file line number Diff line number Diff line change
Expand Up @@ -120,15 +120,43 @@ def derive_done_ratio(ancestor, loader)
end

def compute_derived_done_ratio(work_package, loader)
return if no_children?(work_package, loader)

case Setting.total_percent_complete_mode
when "work_weighted_average"
calculate_work_weighted_average_percent_complete(work_package)
when "simple_average"
calculate_simple_average_percent_complete(work_package, loader)
end
end

def calculate_work_weighted_average_percent_complete(work_package)
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
end

def calculate_simple_average_percent_complete(work_package, loader)
all_done_ratios = children_done_ratio_values(work_package, loader)

if work_package.done_ratio.present? && !work_package.status.excluded_from_totals
all_done_ratios << work_package.done_ratio
end

progress = all_done_ratios.sum.to_f / all_done_ratios.count
progress.round
cbliard marked this conversation as resolved.
Show resolved Hide resolved
end

def children_done_ratio_values(work_package, loader)
loader
.children_of(work_package)
.filter(&:included_in_totals_calculation?)
.map { |child| child.derived_done_ratio || child.done_ratio || 0 }
end

# Sets the ignore_non_working_days to true if any descendant has its value set to true.
# If there is no value returned from the descendants, that means that the work package in
# question no longer has a descendant. But since we are in the service going up the ancestor chain,
Expand Down
7 changes: 4 additions & 3 deletions app/views/admin/settings/progress_tracking/show.html.erb
Original file line number Diff line number Diff line change
Expand Up @@ -26,9 +26,7 @@ Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
See COPYRIGHT and LICENSE files for more details.

++#%>

<% html_title t(:label_administration), t(:label_work_package_plural), t(:label_progress_tracking) -%>

<%=
render Primer::OpenProject::PageHeader.new do |header|
header.with_title { t(:label_progress_tracking) }
Expand All @@ -37,7 +35,6 @@ See COPYRIGHT and LICENSE files for more details.
t(:label_progress_tracking)])
end
%>

<%=
primer_form_with(
scope: :settings, action: :update, method: :patch,
Expand Down Expand Up @@ -65,6 +62,10 @@ primer_form_with(
end
end
end
form.radio_button_group(
name: "total_percent_complete_mode",
values: WorkPackage::TOTAL_PERCENT_COMPLETE_MODE_OPTIONS
)
form.radio_button_group(
name: "percent_complete_on_status_closed",
disabled: WorkPackage.status_based_mode?,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,184 @@
#-- copyright
# OpenProject is an open source project management software.
# Copyright (C) the OpenProject GmbH
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License version 3.
#
# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
# Copyright (C) 2006-2013 Jean-Philippe Lang
# Copyright (C) 2010-2013 the ChiliProject Team
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
#
# See COPYRIGHT and LICENSE files for more details.
#++

class WorkPackages::Progress::ApplyTotalPercentCompleteModeChangeJob < WorkPackages::Progress::Job
VALID_CAUSE_TYPES = %w[
total_percent_complete_mode_changed_to_work_weighted_average
total_percent_complete_mode_changed_to_simple_average
].freeze

attr_reader :cause_type, :mode

# Updates the total % complete of all work packages after the total
# percent complete mode has been changed.
#
# It creates a journal entry with the System user describing the changes.
#
#
# Updates the total % complete of all work packages after the total
# percent complete mode has been changed.
#
# It creates a journal entry with the System user describing the changes.
#
# @param [String] cause_type The cause type of the change
# @param [String] mode The new total percent complete mode
# @return [void]
def perform(cause_type:, mode:)
@cause_type = cause_type
@mode = mode

with_temporary_total_percent_complete_table do
update_total_percent_complete
copy_total_percent_complete_values_to_work_packages_and_update_journals(journal_cause)
end
end

private

def update_total_percent_complete
case mode
when "work_weighted_average"
update_to_work_weighted_average
when "simple_average"
update_to_simple_average
else
raise ArgumentError, "Invalid total percent complete mode: #{mode}"
end
end

def update_to_work_weighted_average
execute(<<~SQL.squish)
UPDATE temp_wp_progress_values
SET total_p_complete = CASE
WHEN total_work IS NULL OR total_remaining_work IS NULL THEN NULL
WHEN total_work = 0 THEN NULL
ELSE ROUND(
((total_work - total_remaining_work)::float / total_work) * 100
)
END
WHERE id IN (
SELECT ancestor_id
FROM work_package_hierarchies
GROUP BY ancestor_id
HAVING MAX(generations) > 0
)
SQL
end

def update_to_simple_average
execute(<<~SQL.squish)
DO $$
DECLARE
min_depth INTEGER := 0;
max_depth INTEGER := (SELECT MAX(depth) FROM temp_work_package_depth);
current_depth INTEGER := min_depth;
BEGIN
/* Navigate work packages and perform updates bottom-up */
while current_depth <= max_depth loop
UPDATE temp_wp_progress_values wp
SET
total_p_complete = CASE
WHEN current_depth = min_depth THEN NULL
ELSE ROUND(
(
/* Exclude the current work package if it has a status excluded from totals */
CASE WHEN wp.status_excluded_from_totals
THEN 0
/* Otherwise, use the current work package's % complete value or 0 if unset */
ELSE COALESCE(wp.p_complete, 0)
END + (
SELECT
SUM(
COALESCE(child_wp.total_p_complete, child_wp.p_complete, 0)
)
FROM
temp_wp_progress_values child_wp
WHERE
child_wp.parent_id = wp.id
/* Exclude children with a status excluded from totals */
AND NOT child_wp.status_excluded_from_totals
)
) / (
/* Exclude the current work package if it has a status excluded from totals */
CASE WHEN wp.status_excluded_from_totals
THEN 0
/* Otherwise, count the current work package if it has a % complete value set */
ELSE(CASE WHEN wp.p_complete IS NOT NULL THEN 1 ELSE 0 END)
END + (
SELECT
COUNT(1)
FROM
temp_wp_progress_values child_wp
WHERE
child_wp.parent_id = wp.id
/* Exclude children with a status excluded from totals */
AND NOT child_wp.status_excluded_from_totals
)
)
)
END
/* Select only work packages at the curren depth */
WHERE
wp.id IN (
SELECT
id
FROM
temp_work_package_depth
WHERE
depth = current_depth
);

/* Go up a level from a child to a parent*/
current_depth := current_depth + 1;

END loop;
END $$;
SQL
end

def journal_cause
assert_valid_cause_type!

@journal_cause ||=
case cause_type
when "total_percent_complete_mode_changed_to_work_weighted_average"
Journal::CausedByTotalPercentCompleteModeChangedToWorkWeightedAverage.new
when "total_percent_complete_mode_changed_to_simple_average"
Journal::CausedByTotalPercentCompleteModeChangedToSimpleAverage.new
else
raise "Unable to handle cause type #{cause_type.inspect}"
end
end

def assert_valid_cause_type!
unless VALID_CAUSE_TYPES.include?(cause_type)
raise ArgumentError, "Invalid cause type #{cause_type.inspect}. " \
"Valid values are #{VALID_CAUSE_TYPES.inspect}"
end
end
end
Loading
Loading