Merge pull request #12879 from chahmedejaz/task/12776-pay-suppliers-report

[Flower Farms] - Pay Suppliers Report
This commit is contained in:
Rachel Arnould
2024-11-15 11:36:29 +01:00
committed by GitHub
10 changed files with 667 additions and 91 deletions

View File

@@ -244,7 +244,7 @@ module Spree
can [:admin, :index, :show, :create], ::Admin::ReportsController
can [:admin, :show, :create, :customers, :orders_and_distributors, :group_buys, :payments,
:orders_and_fulfillment, :products_and_inventory, :order_cycle_management,
:packing, :enterprise_fee_summary, :bulk_coop], :report
:packing, :enterprise_fee_summary, :bulk_coop, :suppliers], :report
end
def add_order_cycle_management_abilities(user)

View File

@@ -0,0 +1,14 @@
= render 'admin/reports/date_range_form', f: f
.row
.alpha.two.columns= label_tag nil, t(:report_hubs)
.omega.fourteen.columns= f.collection_select(:distributor_id_in, @data.orders_distributors, :id, :name, {}, {class: "select2 fullwidth", multiple: true})
.row
.alpha.two.columns= label_tag nil, t(:report_producers)
.omega.fourteen.columns= select_tag(:supplier_id_in, options_from_collection_for_select(@data.orders_suppliers, :id, :name, params[:supplier_id_in]), {class: "select2 fullwidth", multiple: true})
.row
.alpha.two.columns= label_tag nil, t(:report_customers_cycle)
.omega.fourteen.columns
= f.select(:order_cycle_id_in, report_order_cycle_options(@data.order_cycles), {selected: params.dig(:q, :order_cycle_id_in)}, {class: "select2 fullwidth", multiple: true})

View File

@@ -1768,6 +1768,7 @@ en:
pack_by_customer: Pack By Customer
pack_by_supplier: Pack By Supplier
pack_by_product: Pack By Product
pay_your_suppliers: Pay your suppliers
display:
report_is_big: "This report is big and may slow down your device."
display_anyway: "Display anyway"
@@ -1814,6 +1815,8 @@ en:
enterprise_fee_summary:
name: "Enterprise Fee Summary"
description: "Summary of Enterprise Fees collected"
suppliers:
name: Suppliers
enterprise_fees_with_tax_report_by_order: "Enterprise Fees With Tax Report By Order"
enterprise_fees_with_tax_report_by_producer: "Enterprise Fees With Tax Report By Producer"
errors:
@@ -3172,6 +3175,8 @@ See the %{link} to find out more about %{sitename}'s features and to start using
report_render_options: Rendering Options
report_header_ofn_uid: OFN UID
report_header_order_cycle: Order Cycle
report_header_order_cycle_start_date: OC Start Date
report_header_order_cycle_end_date: OC End Date
report_header_user: User
report_header_email: Email
report_header_status: Status
@@ -3192,6 +3197,7 @@ See the %{link} to find out more about %{sitename}'s features and to start using
report_header_hub_legal_name: "Hub Legal Name"
report_header_hub_contact_name: "Hub Contact Name"
report_header_hub_email: "Hub Public Email"
report_header_hub_contact_email: Hub Contact Email
report_header_hub_owner_email: Hub Owner Email
report_header_hub_phone: "Hub Phone Number"
report_header_hub_address_line1: "Hub Address Line 1"
@@ -3252,6 +3258,7 @@ See the %{link} to find out more about %{sitename}'s features and to start using
report_header_quantity: Quantity
report_header_max_quantity: Max Quantity
report_header_variant: Variant
report_header_variant_unit_name: Variant Unit Name
report_header_variant_value: Variant Value
report_header_variant_unit: Variant Unit
report_header_total_available: Total available
@@ -3263,6 +3270,8 @@ See the %{link} to find out more about %{sitename}'s features and to start using
report_header_producer_suburb: Producer Suburb
report_header_producer_tax_status: Producer Tax Status
report_header_producer_charges_sales_tax?: GST/VAT Registered
report_header_producer_abn_acn: Producer ABN/ACN
report_header_producer_address: Producer Address
report_header_unit: Unit
report_header_group_buy_unit_quantity: Group Buy Unit Quantity
report_header_cost: Cost
@@ -3323,7 +3332,11 @@ See the %{link} to find out more about %{sitename}'s features and to start using
report_header_total_units: Total Units
report_header_sum_max_total: "Sum Max Total"
report_header_total_excl_vat: "Total excl. tax (%{currency_symbol})"
report_header_total_fees_excl_tax: "Total fees excl. tax (%{currency_symbol})"
report_header_total_tax_on_fees: "Total tax on fees (%{currency_symbol})"
report_header_total: "Total (%{currency_symbol})"
report_header_total_incl_vat: "Total incl. tax (%{currency_symbol})"
report_header_total_excl_fees_and_tax: "Total excl. fees and tax (%{currency_symbol})"
report_header_temp_controlled: TempControlled?
report_header_is_producer: Producer?
report_header_not_confirmed: Not Confirmed

View File

@@ -3,6 +3,8 @@
module Reporting
module Reports
class List
include ReportTypes
def self.all
new.all
end
@@ -22,98 +24,9 @@ module Reporting
xero_invoices: xero_report_types,
packing: packing_report_types,
revenues_by_hub: [],
suppliers: suppliers_report_types,
}
end
protected
def orders_and_fulfillment_report_types
[
[i18n_translate("supplier_totals"), :order_cycle_supplier_totals],
[i18n_translate("supplier_totals_by_distributor"),
:order_cycle_supplier_totals_by_distributor],
[i18n_translate("totals_by_supplier"), :order_cycle_distributor_totals_by_supplier],
[i18n_translate("customer_totals"), :order_cycle_customer_totals]
]
end
def products_and_inventory_report_types
[
[i18n_translate("all_products"), :all_products],
[i18n_translate("inventory"), :inventory, { deprecated: true }],
[i18n_translate("lettuce_share"), :lettuce_share]
]
end
def payments_report_types
[
[I18n.t(:report_payment_by), :payments_by_payment_type],
[I18n.t(:report_itemised_payment), :itemised_payment_totals],
[I18n.t(:report_payment_totals), :payment_totals]
]
end
def enterprise_fee_summary
[
[i18n_translate('enterprise_fee_summary.name'), :fee_summary],
[
i18n_translate('enterprise_fees_with_tax_report_by_order'),
:enterprise_fees_with_tax_report_by_order
],
[
i18n_translate('enterprise_fees_with_tax_report_by_producer'),
:enterprise_fees_with_tax_report_by_producer
],
]
end
def order_cycle_management_report_types
[
[i18n_translate("payment_methods"), :payment_methods],
[i18n_translate("delivery"), :delivery]
]
end
def sales_tax_report_types
[
[i18n_translate("tax_types"), :tax_types],
[i18n_translate("tax_rates"), :tax_rates],
[i18n_translate("sales_tax_totals_by_producer"), :sales_tax_totals_by_producer],
[i18n_translate("sales_tax_totals_by_order"), :sales_tax_totals_by_order]
]
end
def packing_report_types
[
[i18n_translate("pack_by_customer"), :customer],
[i18n_translate("pack_by_supplier"), :supplier],
[i18n_translate("pack_by_product"), :product]
]
end
def xero_report_types
[
[I18n.t(:summary), 'summary'],
[I18n.t(:detailed), 'detailed']
]
end
def bulk_coop_report_types
[
bulk_coop_item(:supplier_report),
bulk_coop_item(:allocation),
bulk_coop_item(:packing_sheets),
bulk_coop_item(:customer_payments)
]
end
def bulk_coop_item(key)
[I18n.t("order_management.reports.bulk_coop.filters.bulk_coop_#{key}"), key]
end
def i18n_translate(key)
I18n.t(key, scope: "admin.reports")
end
end
end
end

View File

@@ -0,0 +1,103 @@
# frozen_string_literal: true
module Reporting
module Reports
module ReportTypes
protected
def orders_and_fulfillment_report_types
[
[i18n_translate("supplier_totals"), :order_cycle_supplier_totals],
[i18n_translate("supplier_totals_by_distributor"),
:order_cycle_supplier_totals_by_distributor],
[i18n_translate("totals_by_supplier"), :order_cycle_distributor_totals_by_supplier],
[i18n_translate("customer_totals"), :order_cycle_customer_totals]
]
end
def products_and_inventory_report_types
[
[i18n_translate("all_products"), :all_products],
[i18n_translate("inventory"), :inventory, { deprecated: true }],
[i18n_translate("lettuce_share"), :lettuce_share]
]
end
def payments_report_types
[
[I18n.t(:report_payment_by), :payments_by_payment_type],
[I18n.t(:report_itemised_payment), :itemised_payment_totals],
[I18n.t(:report_payment_totals), :payment_totals]
]
end
def enterprise_fee_summary
[
[i18n_translate('enterprise_fee_summary.name'), :fee_summary],
[
i18n_translate('enterprise_fees_with_tax_report_by_order'),
:enterprise_fees_with_tax_report_by_order
],
[
i18n_translate('enterprise_fees_with_tax_report_by_producer'),
:enterprise_fees_with_tax_report_by_producer
],
]
end
def order_cycle_management_report_types
[
[i18n_translate("payment_methods"), :payment_methods],
[i18n_translate("delivery"), :delivery]
]
end
def sales_tax_report_types
[
[i18n_translate("tax_types"), :tax_types],
[i18n_translate("tax_rates"), :tax_rates],
[i18n_translate("sales_tax_totals_by_producer"), :sales_tax_totals_by_producer],
[i18n_translate("sales_tax_totals_by_order"), :sales_tax_totals_by_order]
]
end
def packing_report_types
[
[i18n_translate("pack_by_customer"), :customer],
[i18n_translate("pack_by_supplier"), :supplier],
[i18n_translate("pack_by_product"), :product]
]
end
def xero_report_types
[
[I18n.t(:summary), 'summary'],
[I18n.t(:detailed), 'detailed']
]
end
def bulk_coop_report_types
[
bulk_coop_item(:supplier_report),
bulk_coop_item(:allocation),
bulk_coop_item(:packing_sheets),
bulk_coop_item(:customer_payments)
]
end
def suppliers_report_types
[
[i18n_translate(:pay_your_suppliers), :pay_your_suppliers]
]
end
def bulk_coop_item(key)
[I18n.t("order_management.reports.bulk_coop.filters.bulk_coop_#{key}"), key]
end
def i18n_translate(key)
I18n.t(key, scope: "admin.reports")
end
end
end
end

View File

@@ -0,0 +1,99 @@
# frozen_string_literal: true
module Reporting
module Reports
module Suppliers
class Base < ReportTemplate
include Helpers::ColumnsHelper
def default_params
{
q: {
completed_at_gt: 1.month.ago.beginning_of_day,
completed_at_lt: 1.day.from_now.beginning_of_day
}
}
end
def search
report_line_items.orders
end
def query_result
report_line_items.list(line_item_includes)
end
def columns
{
producer:,
producer_address:,
producer_abn_acn:,
email:,
hub:,
hub_address:,
hub_contact_email:,
order_number:,
order_date:,
order_cycle:,
order_cycle_start_date:,
order_cycle_end_date:,
product:,
variant_unit_name:,
quantity:,
total_excl_fees_and_tax:,
total_excl_vat:,
total_fees_excl_tax:,
total_tax_on_fees:,
total_tax:,
total:,
}
end
def rules
[
{
group_by: :producer,
header: true,
summary_row: proc do |_key, line_items|
summary_hash = Hash.new(0)
line_items.each do |line_item|
summary_hash[:total_excl_fees_and_tax] += total_excl_fees_and_tax.call(line_item)
summary_hash[:total_excl_vat] += total_excl_vat.call(line_item)
summary_hash[:total_fees_excl_tax] += total_fees_excl_tax.call(line_item)
summary_hash[:total_tax_on_fees] += total_tax_on_fees.call(line_item)
summary_hash[:total_tax] += total_tax.call(line_item)
summary_hash[:total] += total.call(line_item)
end
summary_hash
end
}
]
end
private
def order_permissions
return @order_permissions unless @order_permissions.nil?
@order_permissions = ::Permissions::Order.new(@user, ransack_params)
end
def report_line_items
@report_line_items ||= Reporting::LineItems.new(order_permissions, params)
end
def line_item_includes
[{
order: [
:distributor,
:adjustments,
],
variant: [:product, :supplier]
}]
end
end
end
end
end

View File

@@ -0,0 +1,122 @@
# frozen_string_literal: true
module Reporting
module Reports
module Suppliers
module Helpers
module ColumnsHelper
include LineItemsAccessHelper
def producer
proc { |line_item| supplier(line_item).name }
end
def producer_address
proc { |line_item| supplier(line_item).address&.full_address }
end
def producer_abn_acn
proc do |line_items|
supplier = supplier(line_items)
# return nil if both abn and acn are nil so that it can be converted to "none"
[supplier.abn, supplier.acn].compact_blank.join("/").presence
end
end
def email
proc { |line_item| supplier(line_item).email_address }
end
def hub
proc { |line_item| distributor(line_item).name }
end
def hub_address
proc { |line_item| distributor(line_item).address&.full_address }
end
def hub_contact_email
proc { |line_item| distributor(line_item).email_address }
end
def order_number
proc { |line_item| order(line_item).number }
end
def order_date
proc { |line_item| order(line_item).completed_at.to_date }
end
def order_cycle
proc { |line_item| item_order_cycle(line_item).name }
end
def order_cycle_start_date
proc { |line_item| item_order_cycle(line_item).orders_open_at.to_date }
end
def order_cycle_end_date
proc { |line_item| item_order_cycle(line_item).orders_close_at.to_date }
end
def product
proc { |line_item| variant(line_item).product.name }
end
def variant_unit_name
proc { |line_item| variant(line_item).full_name }
end
def quantity
proc { |line_item| line_item.quantity }
end
def total_excl_fees_and_tax
proc do |line_item|
included_tax = adjustments_by_type(line_item, :tax, included: true)
line_item.amount - included_tax
end
end
def total_excl_vat
proc do |line_item|
total_fees = adjustments_by_type(line_item, :fees)
total_excl_fees_and_tax.call(line_item) + total_fees
end
end
def total_fees_excl_tax
proc do |line_item|
included_tax = tax_on_fees(line_item, included: true)
adjustments_by_type(line_item, :fees) - included_tax
end
end
def total_tax_on_fees
proc { |line_item| tax_on_fees(line_item) + tax_on_fees(line_item, included: true) }
end
def total_tax
proc do |line_item|
excluded_tax = adjustments_by_type(line_item, :tax)
included_tax = adjustments_by_type(line_item, :tax, included: true)
excluded_tax + included_tax
end
end
def total
proc do |line_item|
total_price = total_excl_fees_and_tax.call(line_item)
total_fees = total_fees_excl_tax.call(line_item)
total_fees_tax = total_tax_on_fees.call(line_item)
tax = total_tax.call(line_item)
total_price + total_fees + total_fees_tax + tax
end
end
end
end
end
end
end

View File

@@ -0,0 +1,66 @@
# frozen_string_literal: true
module Reporting
module Reports
module Suppliers
module Helpers
module LineItemsAccessHelper
def variant(line_item)
line_item.variant
end
def order(line_item)
line_item.order
end
def supplier(line_item)
variant(line_item).supplier
end
def distributor(line_item)
order(line_item).distributor
end
def item_order_cycle(line_item)
line_item.order_cycle
end
def suppliers_adjustments(line_item, adjustment_type = 'EnterpriseFee')
adjustments = line_item.adjustments
return adjustments.tax if adjustment_type == 'Spree::TaxRate'
supplier_id = line_item.supplier_id
adjustments.enterprise_fee.select do |adjustment|
label = adjustment.label
adjustment_enterprise_id = adjustment.originator.enterprise_id
label.include?('supplier') && adjustment_enterprise_id == supplier_id
end
end
def adjustments_by_type(line_item, type, included: false)
total_amount = 0.0
adjustment_type = type == :tax ? 'Spree::TaxRate' : 'EnterpriseFee'
suppliers_adjustments(line_item, adjustment_type).each do |adjustment|
amount = included == adjustment.included ? adjustment.amount : 0.0
total_amount += amount
end
total_amount
end
def tax_on_fees(line_item, included: false)
total_amount = 0.0
suppliers_adjustments(line_item).each do |adjustment|
adjustment.adjustments.tax.each do |fee_adjustment|
amount = included == fee_adjustment.included ? fee_adjustment.amount : 0.0
total_amount += amount
end
end
total_amount
end
end
end
end
end
end

View File

@@ -0,0 +1,105 @@
# frozen_string_literal: true
require "spec_helper"
RSpec.describe "Pay Your Suppliers Report" do
let(:hub) { create(:distributor_enterprise) }
let(:order_cycle) { create(:open_order_cycle, distributors: [hub]) }
let(:product) { order.products.first }
let(:variant) { product.variants.first }
let(:supplier) { variant.supplier }
let(:current_user) { hub.owner }
let!(:order) do
create(:completed_order_with_totals, distributor: hub, order_cycle:, line_items_count: 1)
end
let(:params) { { display_summary_row: true } }
let(:report) { Reporting::Reports::Suppliers::Base.new(current_user, { q: params }) }
let(:report_table_rows) { report.rows }
context "without fees and taxes" do
it "Generates the report" do
expect(report_table_rows.length).to eq(1)
table_row = report_table_rows.first
expect(table_row.producer).to eq(supplier.name)
expect(table_row.producer_address).to eq(supplier.address.full_address)
expect(table_row.producer_abn_acn).to eq("none")
expect(table_row.email).to eq("none")
expect(table_row.hub).to eq(hub.name)
expect(table_row.hub_address).to eq(hub.address.full_address)
expect(table_row.hub_contact_email).to eq("none")
expect(table_row.order_number).to eq(order.number)
expect(table_row.order_date).to eq(order.completed_at.to_date.to_s)
expect(table_row.order_cycle).to eq(order_cycle.name)
expect(table_row.order_cycle_start_date).to eq(
order_cycle.orders_open_at.to_date.to_s
)
expect(table_row.order_cycle_end_date).to eq(order_cycle.orders_close_at.to_date.to_s)
expect(table_row.product).to eq(product.name)
expect(table_row.variant_unit_name).to eq(variant.full_name)
expect(table_row.quantity).to eq(1)
expect(table_row.total_excl_fees_and_tax.to_f).to eq(10.0)
expect(table_row.total_excl_vat.to_f).to eq(10.0)
expect(table_row.total_fees_excl_tax.to_f).to eq(0.0)
expect(table_row.total_tax_on_fees.to_f).to eq(0.0)
expect(table_row.total_tax.to_f).to eq(0.0)
expect(table_row.total.to_f).to eq(10.0)
end
end
context "with taxes and fees" do
let(:line_item) { order.line_items.first }
let(:tax_category) {
create(
:tax_category,
tax_rates: [
create(
:tax_rate,
zone: create(:zone_with_member)
)
]
)
}
let!(:enterprise_fee) do
create(
:enterprise_fee,
enterprise: supplier,
fee_type: 'sales',
amount: 0.1,
tax_category:
)
end
before do
# Prepare order or line_item to have respective tax adjustments
hub.update!(charges_sales_tax: true)
supplier.update!(charges_sales_tax: true)
line_item.variant.update!(tax_category:)
line_item.copy_tax_category
exchange = order_cycle.exchanges.take
exchange.enterprise_fees << enterprise_fee
exchange.exchange_variants.build(variant: line_item.variant)
exchange.incoming = true
exchange.save!
OpenFoodNetwork::EnterpriseFeeCalculator
.new(hub, order_cycle)
.create_line_item_adjustments_for(line_item)
order.create_tax_charge!
end
it "Generates the report" do
expect(report_table_rows.length).to eq(1)
table_row = report_table_rows.first
expect(table_row.total_excl_fees_and_tax.to_f).to eq(10.0)
expect(table_row.total_excl_vat.to_f).to eq(10.1)
expect(table_row.total_fees_excl_tax.to_f).to eq(0.1)
expect(table_row.total_tax_on_fees.to_f).to eq(0.01)
expect(table_row.total_tax.to_f).to eq(1.0)
expect(table_row.total.to_f).to eq(11.11)
end
end
end

View File

@@ -0,0 +1,141 @@
# frozen_string_literal: true
require "system_helper"
RSpec.describe "Pay Your Suppliers Report" do
include ReportsHelper
let(:owner) { create(:user) }
let(:hub1) { create(:enterprise, owner:) }
let(:order_cycle1) { create(:open_order_cycle, distributors: [hub1]) }
let!(:order1) do
create(
:completed_order_with_totals,
distributor: hub1,
order_cycle: order_cycle1,
line_items_count: 2
)
end
let(:hub2) { create(:enterprise, owner:) }
let(:product2) { order1.products.first }
let(:variant2) { product2.variants.first }
let(:supplier2) { variant2.supplier }
let(:order_cycle2) { create(:open_order_cycle, distributors: [hub2]) }
let!(:order2) do
create(
:completed_order_with_totals,
distributor: hub2,
order_cycle: order_cycle2,
line_items_count: 3
)
end
before do
login_as owner
visit admin_reports_path
end
context "on Reports page" do
it "should generate 'Pay Your Suppliers' report" do
click_on 'Pay your suppliers'
expect(page).to have_button("Go")
run_report
expect(page.find("table.report__table thead tr").text).to have_content([
"Producer",
"Producer Address",
"Producer ABN/ACN",
"Email",
"Hub",
"Hub Address",
"Hub Contact Email",
"Order number",
"Order date",
"Order Cycle",
"OC Start Date",
"OC End Date",
"Product",
"Variant Unit Name",
"Quantity",
"Total excl. fees and tax ($)",
"Total excl. tax ($)",
"Total fees excl. tax ($)",
"Total tax on fees ($)",
"Total Tax ($)",
"Total ($)"
].join(" "))
lines = page.all('table.report__table tbody tr').map(&:text)
# 5 line_item rows + 1 summary row = 6 rows
expect(lines.count).to be(6)
hub1_rows = lines.select { |line| line.include?(hub1.name) }
order1.line_items.each_with_index do |line_item, index|
variant = line_item.variant
supplier = line_item.supplier
product = line_item.variant.product
line = hub1_rows[index]
expect(line).to have_content([
supplier.name,
supplier.address.full_address,
"none",
"none",
hub1.name,
hub1.address.full_address,
"none",
order1.number,
order1.completed_at.to_date.to_s,
order_cycle1.name,
order_cycle1.orders_open_at.to_date.to_s,
order_cycle1.orders_close_at.to_date.to_s,
product.name,
variant.full_name,
1,
10.0,
10.0,
0.0,
0.0,
0.0,
10.0,
].compact.join(" "))
end
hub2_rows = lines.select { |line| line.include?(hub2.name) }
order2.line_items.each_with_index do |line_item, index|
variant = line_item.variant
supplier = line_item.supplier
product = line_item.variant.product
line = hub2_rows[index]
expect(line).to have_content([
supplier.name,
supplier.address.full_address,
"none",
"none",
hub2.name,
hub2.address.full_address,
"none",
order2.number,
order2.completed_at.to_date.to_s,
order_cycle2.name,
order_cycle2.orders_open_at.to_date.to_s,
order_cycle2.orders_close_at.to_date.to_s,
product.name,
variant.full_name,
1,
10.0,
10.0,
0.0,
0.0,
0.0,
10.0,
].compact.join(" "))
end
# summary row
expect(lines.last).to have_content("TOTAL 50.0 50.0 0.0 0.0 0.0 50.0")
end
end
end