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

[#59914] Add times for labor costs to the cost report and export #17450

Draft
wants to merge 2 commits into
base: dev
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
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
22 changes: 22 additions & 0 deletions modules/reporting/app/helpers/reporting_helper.rb
Original file line number Diff line number Diff line change
Expand Up @@ -141,6 +141,28 @@ def field_representation_map(key, value)
end
# rubocop:enable Metrics/AbcSize

def spent_on_time_representation(start_timestamp, hours)
return "" if start_timestamp.nil?

result = format_time(start_timestamp, include_date: false)
return result if hours.nil? || hours.zero?

end_timestamp = start_timestamp + hours.hours
days_between_suffix = days_between_representation(start_timestamp, end_timestamp)
"#{result} - #{format_time(end_timestamp, include_date: false)}#{days_between_suffix}"
end

def days_between_representation(start_timestamp, end_timestamp)
return "" if start_timestamp.nil? || end_timestamp.nil?

days_between = (end_timestamp.to_date - start_timestamp.to_date).to_i
if days_between.positive?
" (+#{WorkPackage::Exports::Formatters::Days.new(nil)
.format_value(days_between, nil)
.delete(' ')})"
end
end

def custom_value(cf_identifier, value)
cf_id = cf_identifier.gsub("custom_field", "").to_i

Expand Down
23 changes: 23 additions & 0 deletions modules/reporting/app/models/cost_query/result.rb
Original file line number Diff line number Diff line change
Expand Up @@ -44,17 +44,40 @@ class Base < Report::Result::Base

class DirectResult < Report::Result::DirectResult
include BaseAdditions

def display_costs
self["display_costs"].to_i
end

def real_costs
(self["real_costs"] || 0).to_d if display_costs? # FIXME: default value here?
end

def start_timestamp
return nil if self["start_time"].blank? || self["time_zone"].blank? || self["spent_on"].blank?

timestamp(Date.parse(self["spent_on"]), self["start_time"], self["time_zone"])
end

def end_timestamp
return nil if self["units"].blank?

start = start_timestamp
return nil if start.nil?

start + self["units"].to_i.hours
end

private

def timestamp(date, time, time_zone)
ActiveSupport::TimeZone[time_zone].local(date.year, date.month, date.day, time / 60, time % 60)
end
end

class WrappedResult < Report::Result::WrappedResult
include BaseAdditions

def display_costs
(sum_for :display_costs) >= 1 ? 1 : 0
end
Expand Down
6 changes: 4 additions & 2 deletions modules/reporting/app/models/cost_query/sql_statement.rb
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,8 @@ def to_s
# cost_type_id | -1 | cost_type_id
# type | "TimeEntry" | "CostEntry"
# count | 1 | 1
# start_time | start_time | nil
# time_zone | time_zone | nil
#
# Also: This _should_ handle joining activities and cost_types, as the logic differs for time_entries
# and cost_entries.
Expand Down Expand Up @@ -102,7 +104,7 @@ def self.unified_entry(model)
#
# @param [CostQuery::SqlStatement] query The statement to adjust
def self.unify_time_entries(query)
query.select :activity_id, :logged_by_id, units: :hours, cost_type_id: -1
query.select :activity_id, :logged_by_id, :start_time, :time_zone, units: :hours, cost_type_id: -1
query.select cost_type: quoted_label(:caption_labor)
end

Expand All @@ -111,7 +113,7 @@ def self.unify_time_entries(query)
#
# @param [CostQuery::SqlStatement] query The statement to adjust
def self.unify_cost_entries(query)
query.select :units, :cost_type_id, :logged_by_id, activity_id: -1
query.select :units, :cost_type_id, :logged_by_id, activity_id: -1, start_time: nil, time_zone: nil
query.select cost_type: "cost_types.name"
query.join CostType
end
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ class CostQuery::PDF::TimesheetGenerator
include WorkPackage::PDFExport::Common::Logo
include WorkPackage::PDFExport::Export::Page
include WorkPackage::PDFExport::Export::Style
include ReportingHelper

H1_FONT_SIZE = 26
H1_MARGIN_BOTTOM = 2
Expand Down Expand Up @@ -347,26 +348,7 @@ def format_hours(hours)
end

def format_spent_on_time(entry)
start_timestamp = entry.start_timestamp
return "" if start_timestamp.nil?

result = format_time(start_timestamp, include_date: false)
end_timestamp = entry.end_timestamp
return result if end_timestamp.nil?

days_between_suffix = format_days_between(start_timestamp, end_timestamp)
"#{result} - #{format_time(end_timestamp, include_date: false)}#{days_between_suffix}"
end

def format_days_between(start_timestamp, end_timestamp)
days_between = (end_timestamp.to_date - start_timestamp.to_date).to_i
if days_between.positive?
" (+#{days_formatter.format_value(days_between, nil).delete(' ')})"
end
end

def days_formatter
@days_formatter ||= WorkPackage::Exports::Formatters::Days.new(nil)
spent_on_time_representation(entry.start_timestamp, entry.hours)
end

def with_times_column?
Expand Down
2 changes: 2 additions & 0 deletions modules/reporting/config/locales/en.yml
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,8 @@ en:
time: "Time"
cost_reports:
title: "Your Cost Reports XLS export"
start_time: "Start time"
end_time: "End time"

reporting:
group_by:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -52,22 +52,48 @@ def format_columns
number_format: currency_format)
end

def cost_fields_columns(result)
cost_entry_attributes
.map { |field| show_field field, result.fields[field.to_s] }
end

def cost_main_times_columns(result)
[
format_time(result.start_timestamp, include_date: false),
cost_main_end_time_column(result.start_timestamp, result.end_timestamp)
]
end

def cost_main_end_time_column(start_timestamp, end_timestamp)
return "" if start_timestamp.nil? || end_timestamp.nil?

days_between = (end_timestamp.to_date - start_timestamp.to_date).to_i
day_prefix = days_between > 1 ? "#{end_timestamp.to_date.iso8601} " : ""
"#{day_prefix}#{format_time(end_timestamp, include_date: false)}"
end

def cost_main_columns(result)
main_cols = [show_field(:spent_on, result.fields[:spent_on.to_s])]
main_cols.concat cost_main_times_columns(result) if with_times_column?
main_cols
end

def cost_row(result)
current_cost_type_id = result.fields["cost_type_id"].to_i

cost_entry_attributes
.map { |field| show_field field, result.fields[field.to_s] }
.concat(
[
show_result(result, current_cost_type_id), # units
cost_type_label(current_cost_type_id, @cost_type), # cost type
show_result(result, 0) # costs/currency
]
cost_main_columns(result)
.concat(cost_fields_columns(result))
.push(

show_result(result, current_cost_type_id), # units
cost_type_label(current_cost_type_id, @cost_type), # cost type
show_result(result, 0) # costs/currency

)
end

def build_footer
footer = [""] * cost_entry_attributes.size
footer = [""] * (cost_entry_attributes.size + main_headers.size)
footer += if show_result(query, 0) == show_result(query)
multiple_unit_types_footer
else
Expand All @@ -84,14 +110,22 @@ def multiple_unit_types_footer
["", "", show_result(query)]
end

def main_headers
main = [label_for(:spent_on)]
if with_times_column?
main.push I18n.t(:"export.cost_reports.start_time"), I18n.t(:"export.cost_reports.end_time")
end
main
end

def headers
cost_entry_attributes
.map { |field| label_for(field) }
.concat([CostEntry.human_attribute_name(:units), CostType.model_name.human, CostEntry.human_attribute_name(:costs)])
main_headers
.concat(cost_entry_attributes.map { |field| label_for(field) })
.push(CostEntry.human_attribute_name(:units), CostType.model_name.human, CostEntry.human_attribute_name(:costs))
end

def cost_entry_attributes
%i[spent_on user_id activity_id work_package_id comments project_id]
%i[user_id activity_id work_package_id comments project_id]
end

# Returns the results of the query sorted by date the time was spent on and by id
Expand All @@ -103,4 +137,12 @@ def sorted_results
.sort
.flat_map { |_, date_results| date_results.sort_by { |r| r.fields["id"] } }
end

def labour_query?
@unit_id == -1
end

def with_times_column?
Setting.allow_tracking_start_and_end_times && labour_query?
end
end
75 changes: 48 additions & 27 deletions modules/reporting/lib/widget/table/entry_table.rb
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,9 @@
#++

class Widget::Table::EntryTable < Widget::Table
FIELDS = %i[spent_on user_id activity_id work_package_id comments logged_by_id project_id].freeze
include ReportingHelper

FIELDS = %i[user_id activity_id work_package_id comments logged_by_id project_id].freeze

def render
content = content_tag :div, class: "generic-table--container -with-footer" do
Expand All @@ -47,6 +49,7 @@ def render

def colgroup
content_tag :colgroup do
concat content_tag(:col, "")
FIELDS.each do
concat content_tag(:col, "")
end
Expand All @@ -56,33 +59,31 @@ def colgroup
end
end

def head_column_field(field)
head_column(label_for(field))
end

def head_column(label)
content_tag(:th) do
content_tag(:div, class: "generic-table--sort-header-outer") do
content_tag(:div, class: "generic-table--sort-header") do
content_tag(:span, label)
end
end
end
end

# rubocop:disable Metrics/AbcSize
def head
content_tag :thead do
content_tag :tr do
concat head_column_field(:spent_on)
concat head_column(I18n.t("label_time")) if with_times_column?
FIELDS.map do |field|
concat content_tag(:th) {
content_tag(:div, class: "generic-table--sort-header-outer") do
content_tag(:div, class: "generic-table--sort-header") do
content_tag(:span, label_for(field))
end
end
}
concat head_column_field(field)
end
concat content_tag(:th) {
content_tag(:div, class: "generic-table--sort-header-outer") do
content_tag(:div, class: "generic-table--sort-header") do
content_tag(:span, cost_type.try(:unit_plural) || I18n.t(:units))
end
end
}
concat content_tag(:th) {
content_tag(:div, class: "generic-table--sort-header-outer") do
content_tag(:div, class: "generic-table--sort-header") do
content_tag(:span, CostEntry.human_attribute_name(:costs))
end
end
}
concat head_column(cost_type.try(:unit_plural) || I18n.t(:units))
concat head_column(CostEntry.human_attribute_name(:costs))
hit = false
@subject.each_direct_result do |result|
next if hit
Expand All @@ -101,15 +102,16 @@ def head
def foot
content_tag :tfoot do
content_tag :tr do
main_columns = with_times_column? ? 2 : 1
if show_result(@subject, 0) == show_result(@subject)
concat content_tag(:td, "", colspan: FIELDS.size + 1)
concat content_tag(:td, "", colspan: FIELDS.size + main_columns + 1)
concat content_tag(:td) {
concat content_tag(:div,
show_result(@subject),
class: "result generic-table--footer-outer")
}
else
concat content_tag(:td, "", colspan: FIELDS.size)
concat content_tag(:td, "", colspan: FIELDS.size + main_columns)
concat content_tag(:td) {
concat content_tag(:div,
show_result(@subject),
Expand All @@ -126,15 +128,25 @@ def foot
end
end

def body_column_field(field, result)
content_tag(:td, show_field(field, result.fields[field.to_s]),
"raw-data": raw_field(field, result.fields[field.to_s]),
class: "left")
end

def body
content_tag :tbody do
rows = "".html_safe
@subject.each_direct_result do |result|
rows << (content_tag(:tr) do
concat body_column_field(:spent_on, result)
if with_times_column?
concat content_tag :td, spent_on_time_representation(result.start_timestamp, result["units"].to_i),
class: "start_time right",
"raw-data": result.start_timestamp.to_s
end
FIELDS.each do |field|
concat content_tag(:td, show_field(field, result.fields[field.to_s]),
"raw-data": raw_field(field, result.fields[field.to_s]),
class: "left")
concat body_column_field(field, result)
end
concat content_tag :td, show_result(result, result.fields["cost_type_id"].to_i),
class: "units right",
Expand Down Expand Up @@ -177,4 +189,13 @@ def icons(result)
end

# rubocop:enable Metrics/AbcSize

def labour_query?
cost_type_filter = @subject.filters.detect { |f| f.is_a?(CostQuery::Filter::CostTypeId) }
cost_type_filter&.values&.first.to_i == -1
end

def with_times_column?
Setting.allow_tracking_start_and_end_times && labour_query?
end
end
Loading