Reports: Format cells for html, pdf, and spreadsheet

Currency, number format, dates
This commit is contained in:
Sebastian Castro
2022-04-07 20:43:26 +02:00
committed by Jean-Baptiste Bellet
parent 3b01c44eae
commit 8a943f50ef
13 changed files with 159 additions and 81 deletions

View File

@@ -537,7 +537,6 @@ Metrics/ClassLength:
- 'lib/open_food_network/order_cycle_permissions.rb'
- 'lib/reporting/reports/payments/payments_report.rb'
- 'lib/reporting/reports/xero_invoices/base.rb'
- 'lib/reporting/report_rows_builder.rb'
# Offense count: 39
# Configuration parameters: IgnoredMethods, Max.

View File

@@ -29,16 +29,4 @@ module ReportsHelper
def currency_symbol
Spree::Money.currency_symbol
end
def format_cell(value)
return "" if value.nil?
if value.in? [true, false] # Boolean
value ? I18n.t(:yes) : I18n.t(:no)
elsif value.respond_to?(:strftime) # Date
value.to_datetime.in_time_zone.strftime "%Y-%m-%d %H:%M"
else
value
end
end
end

View File

@@ -13,10 +13,10 @@
= render partial: 'admin/reports/row_group', locals: { report: report, data: group_or_row[:data] }
/ Summary Row
- if group_or_row[:summary_row].present? && report.display_summary_row?
%tr.summary_row{ class: group_or_row[:summary_row_class] }
%tr.summary-row{ class: group_or_row[:summary_row_class] }
- group_or_row[:summary_row].to_h.each do |key, value|
%td= format_cell(value)
%td= value
- else
%tr
- group_or_row.row.to_h.each do |key, value|
%td= format_cell(value)
%td= value

View File

@@ -12,6 +12,10 @@ module Reporting
@report.params[:report_format].in?(['json', 'csv'])
end
def html_render?
@report.params[:report_format].in?(['', 'pdf'])
end
def display_header_row?
@report.params[:display_header_row].present? && !raw_render?
end

View File

@@ -0,0 +1,126 @@
# frozen_string_literal: true
module Reporting
class ReportRowBuilder
include ActionView::Helpers::NumberHelper
include ActionView::Helpers::TagHelper
attr_reader :report
def initialize(report)
@report = report
end
# Compute the query result item into a result row
# We use OpenStruct to it's easier to access the properties
# i.e. row.my_field, rows.sum(&:quantity)
def build_row(item)
OpenStruct.new(
report.columns.transform_values do |column_constructor|
if column_constructor.is_a?(Symbol)
report.__send__(column_constructor, item)
else
column_constructor.call(item)
end
end
)
end
def slice_and_format_row(row)
result = row.to_h.reject { |k, _v| k.in?(report.fields_to_hide) }
unless report.raw_render?
result = result.map { |k, v| [k, format_cell(v, k)] }.to_h
end
OpenStruct.new(result)
end
def build_header(rule, group_value, group_datas)
return if rule[:header].blank?
rule[:header].call(group_value, group_datas.map(&:item), group_datas.map(&:full_row))
end
def build_summary_row(rule, group_value, datas)
return if rule[:summary_row].blank?
proc_args = [group_value, datas.map(&:item), datas.map(&:full_row)]
row = rule[:summary_row].call(*proc_args)
row = slice_and_format_row(OpenStruct.new(row.reverse_merge!(blank_row)))
add_summary_row_label(row, rule, proc_args)
end
private
def add_summary_row_label(row, rule, proc_args)
previous_key = nil
label = rule[:summary_row_label]
label = label.call(*proc_args) if label.respond_to?(:call)
# Adds Total before first non empty column
row.each_pair do |key, value|
if value.present? && previous_key.present? && row[previous_key].blank?
row[previous_key] = label and break
end
previous_key = key
end
row
end
def blank_row
report.columns.transform_values { |_v| "" }
end
# rubocop:disable Metrics/CyclomaticComplexity
def format_cell(value, column = nil)
return "" if value.nil?
# Currency
if report.columns_format[column] == :currency || column.to_s.include?("price")
format_currency(value)
# Quantity
elsif report.columns_format[column] == :quantity && report.html_render?
format_quantity(value)
# Boolean
elsif value.in? [true, false]
format_boolean(value)
# Time
elsif value.is_a?(Time)
format_time(value)
# Date
elsif value.is_a?(Date)
format_date(value)
# Numeric
elsif value.is_a?(Numeric)
format_numeric(value)
# Default
else
value
end
end
# rubocop:enable Metrics/CyclomaticComplexity
def format_currency(value)
number_to_currency(value, unit: Spree::Money.currency_symbol)
end
def format_quantity(value)
content_tag(value > 1 ? :strong : :span, value)
end
def format_boolean(value)
value ? I18n.t(:yes) : I18n.t(:no)
end
def format_time(value)
value.to_datetime.in_time_zone.strftime "%Y-%m-%d %H:%M"
end
def format_date(value)
value.to_datetime.in_time_zone.strftime "%Y-%m-%d"
end
def format_numeric(value)
number_with_delimiter(value)
end
end
end

View File

@@ -6,6 +6,7 @@ module Reporting
def initialize(report)
@report = report
@builder = ReportRowBuilder.new(report)
end
# Structured data by groups. This tree is used to render
@@ -38,8 +39,8 @@ module Reporting
def computed_data
@computed_data ||= report.query_result.map { |item|
row = build_row(item)
OpenStruct.new(item: item, full_row: row, row: slice_row_fields(row))
row = @builder.build_row(item)
OpenStruct.new(item: item, full_row: row, row: @builder.slice_and_format_row(row))
}
end
@@ -78,9 +79,9 @@ module Reporting
sorted_groups.each do |group_value, group_datas|
result << {
is_group: true,
header: build_header(rule, group_value, group_datas),
header: @builder.build_header(rule, group_value, group_datas),
header_class: rule[:header_class],
summary_row: build_summary_row(rule, group_value, group_datas),
summary_row: @builder.build_summary_row(rule, group_value, group_datas),
summary_row_class: rule[:summary_row_class],
data: build_tree(group_datas, remaining_rules)
}
@@ -109,56 +110,5 @@ module Reporting
end
end.to_h
end
def build_header(rule, group_value, group_datas)
return if rule[:header].blank?
rule[:header].call(group_value, group_datas.map(&:item), group_datas.map(&:full_row))
end
def build_summary_row(rule, group_value, datas)
return if rule[:summary_row].blank?
proc_args = [group_value, datas.map(&:item), datas.map(&:full_row)]
row = rule[:summary_row].call(*proc_args)
row = slice_row_fields(OpenStruct.new(row.reverse_merge!(blank_row)))
add_summary_row_label(row, rule, proc_args)
end
def add_summary_row_label(row, rule, proc_args)
previous_key = nil
label = rule[:summary_row_label]
label = label.call(*proc_args) if label.respond_to?(:call)
# Adds Total before first non empty column
row.each_pair do |key, value|
if value.present? && previous_key.present? && row[previous_key].blank?
row[previous_key] = label and break
end
previous_key = key
end
row
end
def blank_row
report.columns.transform_values { |_v| "" }
end
def slice_row_fields(row)
OpenStruct.new(row.to_h.reject { |k, _v| k.in?(report.fields_to_hide) })
end
# Compute the query result item into a result row
# We use OpenStruct to it's easier to access the properties
# i.e. row.my_field, rows.sum(&:quantity)
def build_row(item)
OpenStruct.new(report.columns.transform_values do |column_constructor|
if column_constructor.is_a?(Symbol)
report.__send__(column_constructor, item)
else
column_constructor.call(item)
end
end)
end
end
end

View File

@@ -6,7 +6,7 @@ module Reporting
attr_accessor :user, :params, :ransack_params
delegate :as_json, :as_arrays, :to_csv, :to_xlsx, :to_ods, :to_pdf, :to_json, to: :renderer
delegate :raw_render?, :display_header_row?, :display_summary_row?, to: :renderer
delegate :raw_render?, :html_render?, :display_header_row?, :display_summary_row?, to: :renderer
delegate :rows, :table_rows, :grouped_data, to: :rows_builder
delegate :available_headers, :table_headers, :fields_to_hide, to: :headers_builder
@@ -47,6 +47,11 @@ module Reporting
raise NotImplementedError
end
# Exple { total_price: :currency }
def columns_format
{}
end
# Headers are automatically translated with table_headers method
# You can customize some header name if needed
def custom_headers

View File

@@ -165,6 +165,7 @@ module Reporting
end
it 'returns rows with payment information' do
allow(subject).to receive(:raw_render?).and_return(true)
expect(subject.table_rows).to eq([[
order.billing_address.firstname,
order.billing_address.lastname,
@@ -191,6 +192,7 @@ module Reporting
end
it 'returns rows with delivery information' do
allow(subject).to receive(:raw_render?).and_return(true)
expect(subject.table_rows).to eq([[
order.ship_address.firstname,
order.ship_address.lastname,

View File

@@ -46,7 +46,7 @@ module Reporting
it 'should denormalise order and distributor details for display as csv' do
subject = Base.new create(:admin_user), {}
allow(subject).to receive(:raw_render?).and_return(true)
table = subject.table_rows
expect(table.size).to eq 1

View File

@@ -80,6 +80,7 @@ module Reporting
end
it "shows the correct payment fee amount for the order" do
allow(report).to receive(:raw_render?).and_return(true)
expect(report.rows.last.pay_fee_price).to eq completed_payment.adjustment.amount
end
end

View File

@@ -49,6 +49,7 @@ module Reporting
double(name: "taxon2")]
allow(variant).to receive_message_chain(:product, :group_buy_unit_size).and_return(21)
allow(subject).to receive(:query_result).and_return [variant]
allow(subject).to receive(:raw_render?).and_return(true)
expect(subject.table_rows).to eq([[
"Supplier",

View File

@@ -101,6 +101,7 @@ module Reporting
end
it "get correct data" do
allow(subject).to receive(:raw_render?).and_return(true)
@expected_table_rows = [
[5, "My Hub"],
[12, "My Other Hub"],
@@ -132,6 +133,7 @@ module Reporting
{ group_by: :customer, header: true }
]
allow(subject).to receive(:display_header_row?).and_return(true)
allow(subject).to receive(:raw_render?).and_return(true)
@expected_rows = [
{ header: "Hub 1" },
{ header: "Abby" },

View File

@@ -182,8 +182,8 @@ describe '
expect(page).to have_content order1.number.to_s
# And the totals and sales tax should be correct
expect(page).to have_content "1512.99" # items total
expect(page).to have_content "1500.45" # taxable items total
expect(page).to have_content "1,512.99" # items total
expect(page).to have_content "1,500.45" # taxable items total
expect(page).to have_content "250.08" # sales tax
expect(page).to have_content "20.0" # enterprise fee tax
@@ -310,17 +310,17 @@ describe '
expect(page).to have_table_row [product1.supplier.name, product1.supplier.address.city,
"Product Name",
product1.properties.map(&:presentation).join(", "),
product1.primary_taxon.name, "Test", "100.0",
product1.primary_taxon.name, "Test", "$100.00",
product1.group_buy_unit_size.to_s, "", "sku1"]
expect(page).to have_table_row [product1.supplier.name, product1.supplier.address.city,
"Product Name",
product1.properties.map(&:presentation).join(", "),
product1.primary_taxon.name, "Something", "80.0",
product1.primary_taxon.name, "Something", "$80.00",
product1.group_buy_unit_size.to_s, "", "sku2"]
expect(page).to have_table_row [product2.supplier.name, product1.supplier.address.city,
"Product 2",
product1.properties.map(&:presentation).join(", "),
product2.primary_taxon.name, "100g", "99.0",
product2.primary_taxon.name, "100g", "$99.00",
product1.group_buy_unit_size.to_s, "", "product_sku"]
end
@@ -332,7 +332,7 @@ describe '
expect(page).to have_table_row ['PRODUCT', 'Description', 'Qty', 'Pack Size', 'Unit',
'Unit Price', 'Total', 'GST incl.',
'Grower and growing method', 'Taxon'].map(&:upcase)
expect(page).to have_table_row ['Product 2', '100g', '', '100', 'g', '99.0', '', '0',
expect(page).to have_table_row ['Product 2', '100g', '', '100', 'g', '$99.00', '', '0',
'Supplier Name (Organic - NASAA 12345)', 'Taxon Name']
end
end
@@ -559,7 +559,7 @@ describe '
xero_invoice_header,
xero_invoice_summary_row('Total untaxable produce (no tax)', 12.54,
'GST Free Income'),
xero_invoice_summary_row('Total taxable produce (tax inclusive)', 1500.45,
xero_invoice_summary_row('Total taxable produce (tax inclusive)', '1,500.45',
'GST on Income'),
xero_invoice_summary_row('Total untaxable fees (no tax)', 10.0,
'GST Free Income'),
@@ -589,7 +589,7 @@ describe '
xero_invoice_header,
xero_invoice_summary_row('Total untaxable produce (no tax)', 12.54,
'GST Free Income', opts),
xero_invoice_summary_row('Total taxable produce (tax inclusive)', 1500.45,
xero_invoice_summary_row('Total taxable produce (tax inclusive)', '1,500.45',
'GST on Income', opts),
xero_invoice_summary_row('Total untaxable fees (no tax)', 10.0,
'GST Free Income', opts),