diff --git a/app/models/work_package/exports/formatters/derived_remaining_hours.rb b/app/models/work_package/exports/formatters/derived_remaining_hours.rb new file mode 100644 index 000000000000..c1ca0a74a4be --- /dev/null +++ b/app/models/work_package/exports/formatters/derived_remaining_hours.rb @@ -0,0 +1,52 @@ +#-- copyright +# OpenProject is an open source project management software. +# Copyright (C) 2012-2023 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. +#++ +module WorkPackage::Exports + module Formatters + class DerivedRemainingHours < ::Exports::Formatters::Default + def self.apply?(name, _export_format) + %i[derived_remaining_time derived_remaining_hours].include?(name.to_sym) + end + + def format(work_package, **) + formatted_derived_hours(work_package) + end + + private + + def formatted_hours(value) + value.nil? ? nil : "#{value} #{I18n.t('export.units.hours')}" + end + + def formatted_derived_hours(work_package) + if (derived_estimated_value = work_package.derived_estimated_hours) + formatted_hours(derived_estimated_value) + end + end + end + end +end diff --git a/app/models/work_package/exports/formatters/spent_units.rb b/app/models/work_package/exports/formatters/spent_units.rb new file mode 100644 index 000000000000..00254ad8e3f2 --- /dev/null +++ b/app/models/work_package/exports/formatters/spent_units.rb @@ -0,0 +1,49 @@ +#-- copyright +# OpenProject is an open source project management software. +# Copyright (C) 2012-2023 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. +#++ +module WorkPackage::Exports + module Formatters + class SpentUnits < ::Exports::Formatters::Default + def self.apply?(name, _export_format) + %i[costs_by_type spent_units].include?(name.to_sym) + end + + def format(work_package, **) + cost_helper = ::Costs::AttributesHelper.new(work_package, User.current) + values = cost_helper.summarized_cost_entries.map do |kvp| + cost_type = kvp[0] + volume = kvp[1] + type_unit = volume.to_d == 1.0.to_d ? cost_type.unit : cost_type.unit_plural + "#{volume} #{type_unit}" + end + return nil if values.empty? + + values.join(', ') + end + end + end +end diff --git a/app/models/work_package/pdf_export/work_package_detail.rb b/app/models/work_package/pdf_export/work_package_detail.rb index 23cfa8107dda..8938ed3f4680 100644 --- a/app/models/work_package/pdf_export/work_package_detail.rb +++ b/app/models/work_package/pdf_export/work_package_detail.rb @@ -103,20 +103,15 @@ def attribute_table_rows(work_package) def attribute_data_list(work_package) list = if respond_to?(:column_objects) - attributes_list_by_columns + attributes_data_by_columns else - attributes_list_by_wp(work_package) + attributes_data_by_wp(work_package) end list - .map { |entry| entry.merge({ value: entry[:value] || get_column_value_cell(work_package, entry[:name]) }) } - .select { |attribute_data| can_show_attribute?(attribute_data) } + .map { |entry| entry.merge({ value: get_column_value_cell(work_package, entry[:name]) }) } end - def can_show_attribute?(attribute_data) - attribute_data[:value].present? - end - - def attributes_list_by_columns + def attributes_data_by_columns column_objects .reject { |column| column.name == :subject } .map do |column| @@ -124,28 +119,47 @@ def attributes_list_by_columns end end - def attributes_list_by_wp(work_package) - list = ::Query.available_columns(work_package.project) - .reject { |column| %i[subject project].include?(column.name) } - .map do |column| - { label: column.caption || '', name: column.name } - end - spent_units = costs_attribute_spent_units(work_package) - list << spent_units unless spent_units.nil? - list + def attributes_data_by_wp(work_package) + column_entries(%i[id type status]) + .concat(form_configuration_columns(work_package)) + end + + def form_configuration_columns(work_package) + work_package + .type.attribute_groups + .filter { |group| group.is_a?(Type::AttributeGroup) } + .map do |group| + group.attributes.map do |form_key| + form_key_to_column_entries(form_key.to_sym, work_package) + end + end.flatten end - def costs_attribute_spent_units(work_package) - cost_helper = ::Costs::AttributesHelper.new(work_package, User.current) - values = cost_helper.summarized_cost_entries.map do |kvp| - cost_type = kvp[0] - volume = kvp[1] - type_unit = volume.to_d == 1.0.to_d ? cost_type.unit : cost_type.unit_plural - "#{volume} #{type_unit}" + def form_key_to_column_entries(form_key, work_package) + if CustomField.custom_field_attribute? form_key + id = form_key.to_s.sub('custom_field_', '').to_i + cf = CustomField.find_by(id:) + return [] if cf.nil? || cf.formattable? + + return [{ label: cf.name || form_key, name: form_key }] + end + + if form_key == :date + column_entries(%i[start_date due_date duration]) + elsif form_key == :bcf_thumbnail + [] + else + column_name = ::API::Utilities::PropertyNameConverter.to_ar_name(form_key, context: work_package) + [column_entry(column_name)] end - return nil if values.empty? + end + + def column_entries(column_names) + column_names.map { |key| column_entry(key) } + end - { label: I18n.t('activerecord.attributes.work_package.spent_units'), name: :spent_units, value: values.join(', ') } + def column_entry(column_name) + { label: WorkPackage.human_attribute_name(column_name), name: column_name } end def build_columns_table_cells(attribute_data) diff --git a/config/initializers/export_formats.rb b/config/initializers/export_formats.rb index eb7447a1a39d..949498288ed3 100644 --- a/config/initializers/export_formats.rb +++ b/config/initializers/export_formats.rb @@ -7,6 +7,8 @@ single WorkPackage, WorkPackage::PDFExport::WorkPackageToPdf formatter WorkPackage, WorkPackage::Exports::Formatters::EstimatedHours + formatter WorkPackage, WorkPackage::Exports::Formatters::DerivedRemainingHours + formatter WorkPackage, WorkPackage::Exports::Formatters::SpentUnits formatter WorkPackage, WorkPackage::Exports::Formatters::Hours formatter WorkPackage, WorkPackage::Exports::Formatters::Days formatter WorkPackage, WorkPackage::Exports::Formatters::Currency diff --git a/spec/models/work_packages/pdf_export/work_package_to_pdf_spec.rb b/spec/models/work_packages/pdf_export/work_package_to_pdf_spec.rb index a97174792295..380ff7d8fe4d 100644 --- a/spec/models/work_packages/pdf_export/work_package_to_pdf_spec.rb +++ b/spec/models/work_packages/pdf_export/work_package_to_pdf_spec.rb @@ -31,8 +31,17 @@ RSpec.describe WorkPackage::PDFExport::WorkPackageToPdf do include Redmine::I18n include PDFExportSpecUtils - let(:type) { create(:type_bug) } - let(:project) { create(:project, name: 'Foo Bla. Report No. 4/2021 with/for Case 42', types: [type]) } + let(:type) do + create(:type_bug, custom_fields: [long_text_custom_field]).tap do |t| + t.attribute_groups.first.attributes.push(long_text_custom_field.attribute_name) + end + end + let(:project) do + create(:project, + name: 'Foo Bla. Report No. 4/2021 with/for Case 42', + types: [type], + work_package_custom_fields: [long_text_custom_field]) + end let(:user) do create(:user, member_with_permissions: { project => %w[view_work_packages export_work_packages] }) @@ -42,6 +51,7 @@ let(:image_path) { Rails.root.join("spec/fixtures/files/image.png") } let(:image_attachment) { Attachment.new author: user, file: File.open(image_path) } let(:attachments) { [image_attachment] } + let(:long_text_custom_field) { create(:issue_custom_field, :text, name: 'LongText') } let(:work_package) do description = <<~DESCRIPTION **Lorem** _ipsum_ ~~dolor~~ `sit` [amet](https://example.com/), consetetur sadipscing elitr. @@ -62,16 +72,20 @@ type:, subject: 'Work package 1', story_points: 1, - description:).tap do |wp| + description:, + custom_values: { long_text_custom_field.id => 'foo' }).tap do |wp| allow(wp) .to receive(:attachments) .and_return attachments end end let(:options) { {} } + let(:exporter) do + described_class.new(work_package, options) + end let(:export) do login_as(user) - described_class.new(work_package, options) + exporter end let(:export_pdf) do Timecop.freeze(export_time) do @@ -95,14 +109,15 @@ def get_column_value(column_name) describe 'with a request for a PDF' do it 'contains correct data' do - details = Query.available_columns(work_package.project) - .reject { |column| %i[subject project].include?(column.name) } - .flat_map do |column| - value = get_column_value(column.name) - value.blank? ? [] : [column.caption.upcase, value] + details = exporter.send(:attributes_data_by_wp, work_package) + .flat_map do |item| + value = get_column_value(item[:name]) + result = [item[:label].upcase] + result << value if value.present? + result end - - expect(pdf[:strings]).to eq([ + # Joining the results for comparison since word wrapping leads to a different array for the same content + expect(pdf[:strings].join(' ')).to eq([ "#{type.name} ##{work_package.id} - #{work_package.subject}", *details, label_title(:description), @@ -110,8 +125,10 @@ def get_column_value(column_name) 'amet', ', consetetur sadipscing elitr.', ' ', '@OpenProject Admin', 'Image Caption', 'Foo', - '1', export_time_formatted, project.name - ]) + '1', export_time_formatted, project.name, + 'LongText', 'foo', + '2', export_time_formatted, project.name + ].join(' ')) expect(pdf[:images].length).to eq(2) end end