mirror of
https://github.com/openfoodfoundation/openfoodnetwork
synced 2026-02-15 23:57:48 +00:00
Reports: Format cells for html, pdf, and spreadsheet
Currency, number format, dates
This commit is contained in:
committed by
Jean-Baptiste Bellet
parent
3b01c44eae
commit
8a943f50ef
@@ -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.
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
|
||||
126
lib/reporting/report_row_builder.rb
Normal file
126
lib/reporting/report_row_builder.rb
Normal 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
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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" },
|
||||
|
||||
@@ -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),
|
||||
|
||||
Reference in New Issue
Block a user