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 all 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
22 changes: 20 additions & 2 deletions app/services/settings/update_service.rb
Original file line number Diff line number Diff line change
Expand Up @@ -45,11 +45,29 @@ 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")

if name == :work_package_done_ratio
trigger_update_job_for_progress_mode_change(old_value, new_value)
elsif name == :total_percent_complete_mode
trigger_update_job_for_total_percent_complete_mode_change(old_value, new_value)
end
end

def trigger_update_job_for_progress_mode_change(old_value, new_value)
return if old_value == new_value
return if new_value != "status" # only trigger if changing to status-based

WorkPackages::Progress::ApplyStatusesChangeJob.perform_later(cause_type: "progress_mode_changed_to_status_based")
end

def trigger_update_job_for_total_percent_complete_mode_change(old_value, new_value)
return if old_value == new_value

WorkPackages::Progress::ApplyTotalPercentCompleteModeChangeJob
.perform_later(mode: new_value,
cause_type: "total_percent_complete_mode_changed_to_#{new_value}")
end

def derive_value(value)
case value
when Array, Hash
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
32 changes: 31 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,45 @@ 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

return if all_done_ratios.empty?

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 All @@ -64,6 +61,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,94 @@
#-- 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_total_percent_complete_in_work_weighted_average_mode
when "simple_average"
update_total_percent_complete_in_simple_average_mode
else
raise ArgumentError, "Invalid total percent complete mode: #{mode}"
end
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