Fix/wp sums in db (#8580)
* remove apparently unused methods * have specs for wp query sums * grouped sums in sql * sql for total sums * alter interface of all_grouped_sums Since we now fetch all sums in one sql statement it no longer makes sense to fetch the group sums individually * remove now unused method * extract method * Add material_costs to summing * add labor_costs to group sums * add overall costs to sum * fix sum grouping descision * fix summable? check * remove work_package_list_summable_columns setting Now all summable columns are always summed. The user no longer needs to select summing up in the settings. Selecting the column to be displayed and activiating sums will suffice * fix flickering specpull/8583/head
parent
b8942af11b
commit
38d2707946
@ -0,0 +1,128 @@ |
||||
#-- encoding: UTF-8 |
||||
|
||||
#-- copyright |
||||
# OpenProject is an open source project management software. |
||||
# Copyright (C) 2012-2020 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-2017 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 docs/COPYRIGHT.rdoc for more details. |
||||
#++ |
||||
|
||||
module ::Query::Results::Sums |
||||
include ActionView::Helpers::NumberHelper |
||||
|
||||
def all_total_sums |
||||
group_sums = sums_select |
||||
|
||||
query.summed_up_columns.inject({}) do |result, column| |
||||
value = group_sums.first |
||||
result[column] = value[column.name.to_s] unless value.nil? |
||||
result |
||||
end |
||||
end |
||||
|
||||
def all_group_sums |
||||
return nil unless query.grouped? |
||||
|
||||
sums_by_id = sums_select(true).inject({}) do |result, group_sum| |
||||
result[group_sum['id']] = {} |
||||
|
||||
query.summed_up_columns.each do |column| |
||||
result[group_sum['id']][column] = group_sum[column.name.to_s] |
||||
end |
||||
|
||||
result |
||||
end |
||||
|
||||
transform_group_keys(sums_by_id) |
||||
end |
||||
|
||||
private |
||||
|
||||
def sums_select(grouped = false) |
||||
select = if grouped |
||||
["work_packages.id"] |
||||
else |
||||
[] |
||||
end |
||||
|
||||
select += query.summed_up_columns.map(&:summable_select) |
||||
|
||||
sql = <<~SQL |
||||
SELECT #{select.join(', ')} |
||||
FROM (#{sums_work_package_scope(grouped).to_sql}) work_packages |
||||
#{sums_callable_joins(grouped)} |
||||
SQL |
||||
|
||||
connection = ActiveRecord::Base.connection |
||||
|
||||
connection.uncached do |
||||
connection.select_all(sql) |
||||
end |
||||
end |
||||
|
||||
def sums_work_package_scope(grouped) |
||||
scope = WorkPackage |
||||
.where(id: work_packages) |
||||
.except(:order, :select) |
||||
.select(sums_work_package_scope_selects(grouped)) |
||||
|
||||
if grouped |
||||
scope.group(query.group_by_statement) |
||||
else |
||||
scope |
||||
end |
||||
end |
||||
|
||||
def sums_callable_joins(grouped) |
||||
callable_summed_up_columns |
||||
.map do |c| |
||||
join_condition = if grouped |
||||
"#{c.name}.id = work_packages.id OR #{c.name}.id IS NULL AND work_packages.id IS NULL" |
||||
else |
||||
"TRUE" |
||||
end |
||||
|
||||
"LEFT OUTER JOIN (#{c.summable.(query, grouped).to_sql}) #{c.name} ON #{join_condition}" |
||||
end |
||||
.join(' ') |
||||
end |
||||
|
||||
def sums_work_package_scope_selects(grouped) |
||||
select = if grouped |
||||
["#{query.group_by_statement} id"] |
||||
else |
||||
[] |
||||
end |
||||
|
||||
select + query.summed_up_columns.map(&:summable_work_packages_select).compact.map { |c| "SUM(#{c}) #{c}" } |
||||
end |
||||
|
||||
def callable_summed_up_columns |
||||
query.summed_up_columns.select { |column| column.summable.respond_to?(:call) } |
||||
end |
||||
|
||||
def non_callable_summed_up_columns |
||||
query.summed_up_columns.map { |column| column.summable.respond_to?(:call) } |
||||
end |
||||
end |
@ -1,147 +0,0 @@ |
||||
#-- encoding: UTF-8 |
||||
#-- copyright |
||||
# OpenProject is an open source project management software. |
||||
# Copyright (C) 2012-2020 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-2017 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 docs/COPYRIGHT.rdoc for more details. |
||||
#++ |
||||
|
||||
module ::Query::Sums |
||||
include ActionView::Helpers::NumberHelper |
||||
|
||||
def next_in_same_group?(issue = cached_issue) |
||||
caching_issue issue do |issue| |
||||
!last_issue? && |
||||
query.group_by_column.value(issue) == query.group_by_column.value(work_packages[issue_index + 1]) |
||||
end |
||||
end |
||||
|
||||
def last_issue?(issue = cached_issue) |
||||
caching_issue issue do |_issue| |
||||
issue_index == work_packages.size - 1 |
||||
end |
||||
end |
||||
|
||||
def issue_index(issue = cached_issue) |
||||
caching_issue issue do |issue| |
||||
work_packages.find_index(issue) |
||||
end |
||||
end |
||||
|
||||
def grouped_sum_of_issue(column, issue = cached_issue) |
||||
grouped_sum_of column, group_for_issue(issue) |
||||
end |
||||
|
||||
def grouped_sum_of(column, group) |
||||
sum_of column, group |
||||
end |
||||
|
||||
def grouped_sums(column) |
||||
work_packages |
||||
.map { |wp| query.group_by_column.value(wp) } |
||||
.uniq |
||||
.inject({}) do |group_sums, current_group| |
||||
work_packages_in_current_group = work_packages.select { |wp| query.group_by_column.value(wp) == current_group } |
||||
# TODO: sum_of only works fast when passing an AR::Relation |
||||
group_sums.merge current_group => sum_of(column, work_packages_in_current_group) |
||||
end |
||||
end |
||||
|
||||
def total_sum_of(column) |
||||
sum_of(column, work_packages) |
||||
end |
||||
|
||||
def sum_of(column, collection) |
||||
return nil unless should_be_summed_up?(column) |
||||
|
||||
sum = column.sum_of(collection) |
||||
|
||||
crunch(sum) |
||||
end |
||||
|
||||
def caching_issue(issue) |
||||
@cached_issue = issue unless @cached_issue == issue |
||||
block_given? ? yield(issue) : issue |
||||
end |
||||
|
||||
def cached_issue |
||||
@cached_issue |
||||
end |
||||
|
||||
def mapping_for(column) |
||||
if column.respond_to? :real_value |
||||
method(:number_to_currency) |
||||
else |
||||
# respond_to? :call, but do nothing |
||||
@nilproc ||= Proc.new { |val| val } |
||||
end |
||||
end |
||||
|
||||
def crunch(num) |
||||
return num if num.nil? || !num.respond_to?(:integer?) || num.integer? |
||||
|
||||
Float(format('%.2f', num.to_f)) |
||||
end |
||||
|
||||
def group_for_issue(issue = @current_issue) |
||||
caching_issue issue do |issue| |
||||
work_packages.select do |is| |
||||
query.group_by_column.value(issue) == query.group_by_column.value(is) |
||||
end |
||||
end |
||||
end |
||||
|
||||
def should_be_summed_up?(column) |
||||
column.summable? && Setting.work_package_list_summable_columns.include?(column.name.to_s) |
||||
end |
||||
|
||||
def column_total_sums |
||||
query.columns.map { |column| total_sum_of(column) } |
||||
end |
||||
|
||||
def all_total_sums |
||||
query.available_columns.select { |column| |
||||
should_be_summed_up?(column) |
||||
}.inject({}) { |result, column| |
||||
sum = total_sum_of(column) |
||||
result[column] = sum unless sum.nil? |
||||
result |
||||
} |
||||
end |
||||
|
||||
def all_sums_for_group(group) |
||||
return nil unless query.grouped? |
||||
|
||||
group_work_packages = work_packages.select { |wp| query.group_by_column.value(wp) == group } |
||||
query.available_columns.inject({}) do |result, column| |
||||
sum = sum_of(column, group_work_packages) |
||||
result[column] = sum unless sum.nil? |
||||
result |
||||
end |
||||
end |
||||
|
||||
def column_group_sums |
||||
query.group_by_column && query.columns.map { |column| grouped_sums(column) } |
||||
end |
||||
end |
@ -1,58 +0,0 @@ |
||||
#-- copyright |
||||
# OpenProject is an open source project management software. |
||||
# Copyright (C) 2012-2020 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-2017 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 docs/COPYRIGHT.rdoc for more details. |
||||
#++ |
||||
|
||||
module OpenProject::Backlogs |
||||
module Patches |
||||
module API |
||||
module WorkPackageSumsRepresenter |
||||
module_function |
||||
|
||||
def extension |
||||
->(*) do |
||||
property :story_points, |
||||
render_nil: true, |
||||
if: ->(*) { |
||||
::Setting.work_package_list_summable_columns.include?('story_points') |
||||
} |
||||
|
||||
property :remaining_time, |
||||
render_nil: true, |
||||
exec_context: :decorator, |
||||
getter: ->(*) { |
||||
datetime_formatter.format_duration_from_hours(represented.remaining_hours, |
||||
allow_nil: true) |
||||
}, |
||||
if: ->(*) { |
||||
::Setting.work_package_list_summable_columns.include?('remaining_hours') |
||||
} |
||||
end |
||||
end |
||||
end |
||||
end |
||||
end |
||||
end |
@ -1,57 +0,0 @@ |
||||
#-- copyright |
||||
# OpenProject is an open source project management software. |
||||
# Copyright (C) 2012-2020 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-2017 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 docs/COPYRIGHT.rdoc for more details. |
||||
#++ |
||||
|
||||
module OpenProject::Backlogs |
||||
module Patches |
||||
module API |
||||
module WorkPackageSumsSchemaRepresenter |
||||
module_function |
||||
|
||||
def extension |
||||
->(*) do |
||||
schema :story_points, |
||||
type: 'Integer', |
||||
required: false, |
||||
show_if: ->(*) { |
||||
::Setting.work_package_list_summable_columns.include?('story_points') |
||||
} |
||||
|
||||
schema :remaining_time, |
||||
type: 'Duration', |
||||
name_source: :remaining_hours, |
||||
required: false, |
||||
writable: false, |
||||
show_if: ->(*) { |
||||
::Setting.work_package_list_summable_columns.include?('remaining_hours') |
||||
} |
||||
end |
||||
end |
||||
end |
||||
end |
||||
end |
||||
end |
@ -1,88 +0,0 @@ |
||||
#-- copyright |
||||
# OpenProject is an open source project management software. |
||||
# Copyright (C) 2012-2020 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-2017 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 docs/COPYRIGHT.rdoc for more details. |
||||
#++ |
||||
|
||||
require 'spec_helper' |
||||
|
||||
describe ::API::V3::WorkPackages::Schema::WorkPackageSumsSchemaRepresenter do |
||||
let(:current_user) do |
||||
FactoryBot.build_stubbed(:user) |
||||
end |
||||
|
||||
let(:schema) { ::API::V3::WorkPackages::Schema::WorkPackageSumsSchema.new } |
||||
|
||||
let(:representer) { described_class.create(schema, current_user: current_user) } |
||||
subject { representer.to_json } |
||||
|
||||
describe 'storyPoints' do |
||||
let(:setting) { ['story_points'] } |
||||
|
||||
before do |
||||
allow(Setting) |
||||
.to receive(:work_package_list_summable_columns) |
||||
.and_return(setting) |
||||
end |
||||
|
||||
it_behaves_like 'has basic schema properties' do |
||||
let(:path) { 'storyPoints' } |
||||
let(:type) { 'Integer' } |
||||
let(:name) { I18n.t('activerecord.attributes.work_package.story_points') } |
||||
let(:required) { false } |
||||
let(:writable) { false } |
||||
end |
||||
|
||||
context 'not marked as summable' do |
||||
let(:setting) { [] } |
||||
|
||||
it 'does not show story points' do |
||||
is_expected.to_not have_json_path('storyPoints') |
||||
end |
||||
end |
||||
end |
||||
|
||||
describe 'remainingTime' do |
||||
let(:setting) { ['remaining_time'] } |
||||
|
||||
shared_examples_for 'has schema for remainingTime' do |
||||
it_behaves_like 'has basic schema properties' do |
||||
let(:path) { 'remainingTime' } |
||||
let(:type) { 'Duration' } |
||||
let(:name) { I18n.t('activerecord.attributes.work_package.remaining_hours') } |
||||
let(:required) { false } |
||||
let(:writable) { true } |
||||
end |
||||
end |
||||
|
||||
context 'not marked as summable' do |
||||
let(:setting) { [] } |
||||
|
||||
it 'does not show remaining time' do |
||||
is_expected.to_not have_json_path('remaining time') |
||||
end |
||||
end |
||||
end |
||||
end |
@ -1,80 +0,0 @@ |
||||
#-- copyright |
||||
# OpenProject is an open source project management software. |
||||
# Copyright (C) 2012-2020 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-2017 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 docs/COPYRIGHT.rdoc for more details. |
||||
#++ |
||||
|
||||
require 'spec_helper' |
||||
|
||||
describe ::API::V3::WorkPackages::WorkPackageSumsRepresenter do |
||||
let(:sums) { double 'sums', story_points: 5, remaining_hours: 10 } |
||||
let(:schema) { double 'schema', available_custom_fields: [] } |
||||
let(:user) { FactoryBot.build_stubbed(:user) } |
||||
let(:representer) { |
||||
described_class.create_class(schema, user).new(sums) |
||||
} |
||||
let(:summable_columns) { [] } |
||||
|
||||
before do |
||||
allow(Setting) |
||||
.to receive(:work_package_list_summable_columns) |
||||
.and_return(summable_columns) |
||||
end |
||||
|
||||
subject { representer.to_json } |
||||
|
||||
context 'remainingTime' do |
||||
context 'with it being configured to be summable' do |
||||
let(:summable_columns) { ['remaining_hours'] } |
||||
|
||||
it 'is represented' do |
||||
expected = 'PT10H' |
||||
expect(subject).to be_json_eql(expected.to_json).at_path('remainingTime') |
||||
end |
||||
end |
||||
|
||||
context 'without it being configured to be summable' do |
||||
it 'is not represented when the summable setting does not list it' do |
||||
expect(subject).to_not have_json_path('remainingTime') |
||||
end |
||||
end |
||||
end |
||||
|
||||
context 'storyPoints' do |
||||
context 'with it being configured to be summable' do |
||||
let(:summable_columns) { ['story_points'] } |
||||
|
||||
it 'is represented' do |
||||
expect(subject).to be_json_eql(sums.story_points.to_json).at_path('storyPoints') |
||||
end |
||||
end |
||||
|
||||
context 'without it being configured to be summable' do |
||||
it 'is not represented when the summable setting does not list it' do |
||||
expect(subject).to_not have_json_path('storyPoints') |
||||
end |
||||
end |
||||
end |
||||
end |
@ -1,101 +0,0 @@ |
||||
#-- copyright |
||||
# OpenProject is an open source project management software. |
||||
# Copyright (C) 2012-2020 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-2017 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 docs/COPYRIGHT.rdoc for more details. |
||||
#++ |
||||
|
||||
require File.expand_path(File.dirname(__FILE__) + '/../spec_helper.rb') |
||||
|
||||
describe 'Work Package table cost sums', type: :feature, js: true do |
||||
let(:project) { work_package.project } |
||||
let(:user) { FactoryBot.create :user, |
||||
member_in_project: project, |
||||
member_through_role: role } |
||||
let(:role) { FactoryBot.create :role, permissions: [:view_own_hourly_rate, |
||||
:view_work_packages, |
||||
:view_work_packages, |
||||
:view_own_time_entries, |
||||
:view_own_cost_entries, |
||||
:view_cost_rates, |
||||
:log_costs] } |
||||
let(:work_package) {FactoryBot.create :work_package } |
||||
let(:hourly_rate) { FactoryBot.create :default_hourly_rate, user: user, |
||||
rate: 1.00 } |
||||
let!(:time_entry) { FactoryBot.create :time_entry, user: user, |
||||
work_package: work_package, |
||||
project: project, |
||||
hours: 1.50 } |
||||
let(:cost_type) { |
||||
type = FactoryBot.create :cost_type, name: 'Translations' |
||||
FactoryBot.create :cost_rate, cost_type: type, |
||||
rate: 1.00 |
||||
type |
||||
} |
||||
let!(:cost_entry) { FactoryBot.create :cost_entry, work_package: work_package, |
||||
project: project, |
||||
units: 2.50, |
||||
cost_type: cost_type, |
||||
user: user } |
||||
let(:wp_table) { ::Pages::WorkPackagesTable.new(project) } |
||||
let!(:query) do |
||||
query = FactoryBot.build(:query, user: user, project: project) |
||||
query.column_names = %w(subject overall_costs material_costs overall_costs) |
||||
|
||||
query.save! |
||||
query |
||||
end |
||||
|
||||
before do |
||||
login_as(user) |
||||
allow(Setting).to receive(:work_package_list_summable_columns).and_return(summable) |
||||
|
||||
wp_table.visit_query(query) |
||||
wp_table.expect_work_package_listed(work_package) |
||||
|
||||
# Trigger action from action menu dropdown |
||||
wp_table.click_setting_item 'Display sums' |
||||
expect(page).to have_selector('tr.sum.group.all') |
||||
end |
||||
|
||||
context 'when summing enabled' do |
||||
let(:summable) { %w(overall_costs labor_costs material_costs) } |
||||
|
||||
it 'shows the sums' do |
||||
within('tr.sum.group.all') do |
||||
expect(page).to have_selector('.inline-edit--display-field', text: '2.50 EUR', count: 3) |
||||
expect(page).to have_selector('.inline-edit--display-field', text: '-', count: 1) |
||||
end |
||||
end |
||||
end |
||||
context 'when summing disabled' do |
||||
let(:summable) { [] } |
||||
|
||||
it 'does not show the sums' do |
||||
within('tr.sum.group.all') do |
||||
expect(page).to have_selector('.inline-edit--display-field', text: '-', count: 4) |
||||
end |
||||
end |
||||
end |
||||
end |
@ -1,98 +0,0 @@ |
||||
#-- copyright |
||||
# OpenProject is an open source project management software. |
||||
# Copyright (C) 2012-2020 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-2017 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 docs/COPYRIGHT.rdoc for more details. |
||||
#++ |
||||
|
||||
require 'spec_helper' |
||||
|
||||
describe ::API::V3::WorkPackages::WorkPackageSumsRepresenter do |
||||
let(:sums) { double 'sums', material_costs: 5, labor_costs: 10, overall_costs: 15 } |
||||
let(:schema) { double 'schema', available_custom_fields: [] } |
||||
let(:user) { FactoryBot.build_stubbed(:user) } |
||||
let(:representer) do |
||||
described_class.create_class(schema, user).new(sums) |
||||
end |
||||
let(:summable_columns) { [] } |
||||
|
||||
before do |
||||
allow(Setting) |
||||
.to receive(:work_package_list_summable_columns) |
||||
.and_return(summable_columns) |
||||
end |
||||
|
||||
subject { representer.to_json } |
||||
|
||||
context 'materialCosts' do |
||||
context 'with it being configured to be summable' do |
||||
let(:summable_columns) { ['material_costs'] } |
||||
|
||||
it 'is represented' do |
||||
expected = "5.00 EUR" |
||||
expect(subject).to be_json_eql(expected.to_json).at_path('materialCosts') |
||||
end |
||||
end |
||||
|
||||
context 'without it being configured to be summable' do |
||||
it 'is not represented when the summable setting does not list it' do |
||||
expect(subject).to_not have_json_path('materialCosts') |
||||
end |
||||
end |
||||
end |
||||
|
||||
context 'laborCosts' do |
||||
context 'with it being configured to be summable' do |
||||
let(:summable_columns) { ['labor_costs'] } |
||||
|
||||
it 'is represented' do |
||||
expected = "10.00 EUR" |
||||
expect(subject).to be_json_eql(expected.to_json).at_path('laborCosts') |
||||
end |
||||
end |
||||
|
||||
context 'without it being configured to be summable' do |
||||
it 'is not represented when the summable setting does not list it' do |
||||
expect(subject).to_not have_json_path('laborCosts') |
||||
end |
||||
end |
||||
end |
||||
|
||||
context 'overallCosts' do |
||||
context 'with it being configured to be summable' do |
||||
let(:summable_columns) { ['overall_costs'] } |
||||
|
||||
it 'is represented' do |
||||
expected = "15.00 EUR" |
||||
expect(subject).to be_json_eql(expected.to_json).at_path('overallCosts') |
||||
end |
||||
end |
||||
|
||||
context 'without it being configured to be summable' do |
||||
it 'is not represented when the summable setting does not list it' do |
||||
expect(subject).to_not have_json_path('overallCosts') |
||||
end |
||||
end |
||||
end |
||||
end |
@ -0,0 +1,145 @@ |
||||
#-- copyright |
||||
# OpenProject is an open source project management software. |
||||
# Copyright (C) 2012-2020 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-2017 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 docs/COPYRIGHT.rdoc for more details. |
||||
#++ |
||||
|
||||
require 'spec_helper' |
||||
|
||||
describe Costs::QueryCurrencyColumn, type: :model do |
||||
let(:project) do |
||||
FactoryBot.build_stubbed(:project).tap do |p| |
||||
allow(p) |
||||
.to receive(:costs_enabled?) |
||||
.and_return(costs_enabled) |
||||
end |
||||
end |
||||
let(:instance) { described_class.instances(project).detect { |c| c.name == column_name } } |
||||
let(:costs_enabled) { true } |
||||
let(:column_name) { :material_costs } |
||||
|
||||
describe '.instances' do |
||||
subject { described_class.instances(project).map(&:name) } |
||||
|
||||
context 'with costs enabled' do |
||||
it 'returns the four costs columns' do |
||||
is_expected |
||||
.to match_array %i[budget material_costs labor_costs overall_costs] |
||||
end |
||||
end |
||||
|
||||
context 'with costs disabled' do |
||||
let(:costs_enabled) { false } |
||||
|
||||
it 'returns no columns' do |
||||
is_expected |
||||
.to be_empty |
||||
end |
||||
end |
||||
|
||||
context 'with no context' do |
||||
it 'returns the four costs columns' do |
||||
is_expected |
||||
.to match_array %i[budget material_costs labor_costs overall_costs] |
||||
end |
||||
end |
||||
end |
||||
|
||||
context 'material_costs' do |
||||
describe '#summable?' do |
||||
it 'is true' do |
||||
expect(instance) |
||||
.to be_summable |
||||
end |
||||
end |
||||
|
||||
describe '#summable' do |
||||
it 'is callable' do |
||||
expect(instance.summable) |
||||
.to respond_to(:call) |
||||
end |
||||
|
||||
# Not testing the results here, this is done by an integration test |
||||
it 'returns an AR scope that has an id and a material_costs column' do |
||||
query = double('query') |
||||
result = double('result') |
||||
|
||||
allow(query) |
||||
.to receive(:results) |
||||
.and_return result |
||||
|
||||
allow(result) |
||||
.to receive(:work_packages) |
||||
.and_return(WorkPackage.all) |
||||
|
||||
allow(query) |
||||
.to receive(:group_by_statement) |
||||
.and_return('author_id') |
||||
|
||||
expect(ActiveRecord::Base.connection.select_all(instance.summable.(query, true).to_sql).column_types.keys) |
||||
.to match_array %w(id material_costs) |
||||
end |
||||
end |
||||
end |
||||
|
||||
context 'labor_costs' do |
||||
let(:column_name) { :labor_costs } |
||||
|
||||
describe '#summable?' do |
||||
it 'is true' do |
||||
expect(instance) |
||||
.to be_summable |
||||
end |
||||
end |
||||
|
||||
describe '#summable' do |
||||
it 'is callable' do |
||||
expect(instance.summable) |
||||
.to respond_to(:call) |
||||
end |
||||
|
||||
# Not testing the results here, this is done by an integration test |
||||
it 'returns an AR scope that has an id and a labor_costs column' do |
||||
query = double('query') |
||||
result = double('result') |
||||
|
||||
allow(query) |
||||
.to receive(:results) |
||||
.and_return result |
||||
|
||||
allow(result) |
||||
.to receive(:work_packages) |
||||
.and_return(WorkPackage.all) |
||||
|
||||
allow(query) |
||||
.to receive(:group_by_statement) |
||||
.and_return('author_id') |
||||
|
||||
expect(ActiveRecord::Base.connection.select_all(instance.summable.(query, true).to_sql).column_types.keys) |
||||
.to match_array %w(id labor_costs) |
||||
end |
||||
end |
||||
end |
||||
end |
@ -0,0 +1,277 @@ |
||||
#-- copyright |
||||
# OpenProject is an open source project management software. |
||||
# Copyright (C) 2012-2020 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-2017 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 docs/COPYRIGHT.rdoc for more details. |
||||
#++ |
||||
|
||||
require 'spec_helper' |
||||
|
||||
describe ::Query::Results, 'sums', type: :model do |
||||
let(:project) do |
||||
FactoryBot.create(:project).tap do |p| |
||||
p.work_package_custom_fields << int_cf |
||||
p.work_package_custom_fields << float_cf |
||||
end |
||||
end |
||||
let(:other_project) do |
||||
FactoryBot.create(:project).tap do |p| |
||||
p.work_package_custom_fields << int_cf |
||||
p.work_package_custom_fields << float_cf |
||||
end |
||||
end |
||||
let!(:work_package1) do |
||||
FactoryBot.create(:work_package, |
||||
type: type, |
||||
project: project, |
||||
estimated_hours: 5, |
||||
done_ratio: 10, |
||||
"custom_field_#{int_cf.id}" => 10, |
||||
"custom_field_#{float_cf.id}" => 3.414, |
||||
remaining_hours: 3, |
||||
story_points: 7) |
||||
end |
||||
let!(:work_package2) do |
||||
FactoryBot.create(:work_package, |
||||
type: type, |
||||
project: project, |
||||
assigned_to: current_user, |
||||
done_ratio: 50, |
||||
estimated_hours: 5, |
||||
"custom_field_#{int_cf.id}" => 10, |
||||
"custom_field_#{float_cf.id}" => 3.414, |
||||
remaining_hours: 3, |
||||
story_points: 7) |
||||
end |
||||
let!(:work_package3) do |
||||
FactoryBot.create(:work_package, |
||||
type: type, |
||||
project: project, |
||||
assigned_to: current_user, |
||||
responsible: current_user, |
||||
done_ratio: 50, |
||||
estimated_hours: 5, |
||||
"custom_field_#{int_cf.id}" => 10, |
||||
"custom_field_#{float_cf.id}" => 3.414, |
||||
remaining_hours: 3, |
||||
story_points: 7) |
||||
end |
||||
let!(:invisible_work_package1) do |
||||
FactoryBot.create(:work_package, |
||||
type: type, |
||||
project: other_project, |
||||
estimated_hours: 5, |
||||
"custom_field_#{int_cf.id}" => 10, |
||||
"custom_field_#{float_cf.id}" => 3.414, |
||||
remaining_hours: 3, |
||||
story_points: 7) |
||||
end |
||||
let!(:cost_entry1) do |
||||
FactoryBot.create(:cost_entry, |
||||
project: project, |
||||
work_package: work_package1, |
||||
user: current_user, |
||||
overridden_costs: 200) |
||||
end |
||||
let!(:cost_entry2) do |
||||
FactoryBot.create(:cost_entry, |
||||
project: project, |
||||
work_package: work_package2, |
||||
user: current_user, |
||||
overridden_costs: 200) |
||||
end |
||||
let!(:time_entry1) do |
||||
FactoryBot.create(:time_entry, |
||||
project: project, |
||||
work_package: work_package1, |
||||
user: current_user, |
||||
overridden_costs: 300) |
||||
end |
||||
let!(:time_entry2) do |
||||
FactoryBot.create(:time_entry, |
||||
project: project, |
||||
work_package: work_package2, |
||||
user: current_user, |
||||
overridden_costs: 300) |
||||
end |
||||
let(:int_cf) do |
||||
FactoryBot.create(:int_wp_custom_field) |
||||
end |
||||
let(:float_cf) do |
||||
FactoryBot.create(:float_wp_custom_field) |
||||
end |
||||
let(:type) do |
||||
FactoryBot.create(:type).tap do |t| |
||||
t.custom_fields << int_cf |
||||
t.custom_fields << float_cf |
||||
end |
||||
end |
||||
let(:current_user) do |
||||
FactoryBot.create(:user, |
||||
member_in_project: project, |
||||
member_with_permissions: permissions) |
||||
end |
||||
let(:permissions) do |
||||
%i[view_work_packages view_cost_entries view_time_entries view_cost_rates view_hourly_rates] |
||||
end |
||||
let(:group_by) { nil } |
||||
let(:query) do |
||||
FactoryBot.build :query, |
||||
project: project, |
||||
group_by: group_by |
||||
end |
||||
let(:query_results) do |
||||
::Query::Results.new query |
||||
end |
||||
|
||||
before do |
||||
login_as(current_user) |
||||
end |
||||
let(:estimated_hours_column) { query.available_columns.detect { |c| c.name.to_s == 'estimated_hours' } } |
||||
let(:int_cf_column) { query.available_columns.detect { |c| c.name.to_s == "cf_#{int_cf.id}" } } |
||||
let(:float_cf_column) { query.available_columns.detect { |c| c.name.to_s == "cf_#{float_cf.id}" } } |
||||
let(:material_costs_column) { query.available_columns.detect { |c| c.name.to_s == "material_costs" } } |
||||
let(:labor_costs_column) { query.available_columns.detect { |c| c.name.to_s == "labor_costs" } } |
||||
let(:overall_costs_column) { query.available_columns.detect { |c| c.name.to_s == "overall_costs" } } |
||||
let(:remaining_hours_column) { query.available_columns.detect { |c| c.name.to_s == "remaining_hours" } } |
||||
let(:story_points_column) { query.available_columns.detect { |c| c.name.to_s == "story_points" } } |
||||
|
||||
describe '#all_total_sums' do |
||||
it 'is a hash of all summable columns' do |
||||
expect(query_results.all_total_sums) |
||||
.to eql(estimated_hours_column => 15.0, |
||||
int_cf_column => 30, |
||||
float_cf_column => 10.24, |
||||
material_costs_column => 400.0, |
||||
labor_costs_column => 600.0, |
||||
overall_costs_column => 1000.0, |
||||
remaining_hours_column => 9.0, |
||||
story_points_column => 21) |
||||
end |
||||
|
||||
context 'when filtering' do |
||||
before do |
||||
query.add_filter('assigned_to_id', '=', [current_user.id.to_s]) |
||||
end |
||||
|
||||
it 'is a hash of all summable columns and includes only the work packages matching the filter' do |
||||
expect(query_results.all_total_sums) |
||||
.to eql(estimated_hours_column => 10.0, |
||||
int_cf_column => 20, |
||||
float_cf_column => 6.83, |
||||
material_costs_column => 200.0, |
||||
labor_costs_column => 300.0, |
||||
overall_costs_column => 500.0, |
||||
remaining_hours_column => 6.0, |
||||
story_points_column => 14) |
||||
end |
||||
end |
||||
end |
||||
|
||||
describe '#all_sums_for_group' do |
||||
context 'grouped by assigned_to' do |
||||
let(:group_by) { :assigned_to } |
||||
|
||||
it 'is a hash of sums grouped by user values (and nil) and grouped columns' do |
||||
expect(query_results.all_group_sums) |
||||
.to eql(current_user => { estimated_hours_column => 10.0, |
||||
int_cf_column => 20, |
||||
float_cf_column => 6.83, |
||||
material_costs_column => 200.0, |
||||
labor_costs_column => 300.0, |
||||
overall_costs_column => 500.0, |
||||
remaining_hours_column => 6.0, |
||||
story_points_column => 14 }, |
||||
nil => { estimated_hours_column => 5.0, |
||||
int_cf_column => 10, |
||||
float_cf_column => 3.41, |
||||
material_costs_column => 200.0, |
||||
labor_costs_column => 300.0, |
||||
overall_costs_column => 500.0, |
||||
remaining_hours_column => 3.0, |
||||
story_points_column => 7 }) |
||||
end |
||||
|
||||
context 'when filtering' do |
||||
before do |
||||
query.add_filter('responsible_id', '=', [current_user.id.to_s]) |
||||
end |
||||
|
||||
it 'is a hash of sums grouped by user values and grouped columns' do |
||||
expect(query_results.all_group_sums) |
||||
.to eql(current_user => { estimated_hours_column => 5.0, |
||||
int_cf_column => 10, |
||||
float_cf_column => 3.41, |
||||
material_costs_column => 0.0, |
||||
labor_costs_column => 0.0, |
||||
overall_costs_column => 0.0, |
||||
story_points_column => 7, |
||||
remaining_hours_column => 3.0 }) |
||||
end |
||||
end |
||||
end |
||||
|
||||
context 'grouped by done_ratio' do |
||||
let(:group_by) { :done_ratio } |
||||
|
||||
it 'is a hash of sums grouped by done_ratio values and grouped columns' do |
||||
expect(query_results.all_group_sums) |
||||
.to eql(50 => { estimated_hours_column => 10.0, |
||||
int_cf_column => 20, |
||||
float_cf_column => 6.83, |
||||
material_costs_column => 200.0, |
||||
labor_costs_column => 300.0, |
||||
overall_costs_column => 500.0, |
||||
remaining_hours_column => 6.0, |
||||
story_points_column => 14 }, |
||||
10 => { estimated_hours_column => 5.0, |
||||
int_cf_column => 10, |
||||
float_cf_column => 3.41, |
||||
material_costs_column => 200.0, |
||||
labor_costs_column => 300.0, |
||||
overall_costs_column => 500.0, |
||||
remaining_hours_column => 3.0, |
||||
story_points_column => 7 }) |
||||
end |
||||
|
||||
context 'when filtering' do |
||||
before do |
||||
query.add_filter('responsible_id', '=', [current_user.id.to_s]) |
||||
end |
||||
|
||||
it 'is a hash of sums grouped by done_ratio values and grouped columns' do |
||||
expect(query_results.all_group_sums) |
||||
.to eql(50 => { estimated_hours_column => 5.0, |
||||
int_cf_column => 10, |
||||
float_cf_column => 3.41, |
||||
material_costs_column => 0.0, |
||||
labor_costs_column => 0.0, |
||||
overall_costs_column => 0.0, |
||||
story_points_column => 7, |
||||
remaining_hours_column => 3.0 }) |
||||
end |
||||
end |
||||
end |
||||
end |
||||
end |
Loading…
Reference in new issue