diff --git a/app/services/order_fees_handler.rb b/app/services/order_fees_handler.rb index 5f7862beb1b..f05351aec7e 100644 --- a/app/services/order_fees_handler.rb +++ b/app/services/order_fees_handler.rb @@ -1,12 +1,12 @@ # frozen_string_literal: true class OrderFeesHandler - attr_reader :order, :distributor, :order_cycle + attr_reader :order + + delegate :distributor, :order_cycle, to: :order def initialize(order) @order = order - @distributor = order.distributor - @order_cycle = order.order_cycle end def recreate_all_fees! diff --git a/app/views/admin/reports/filters/_sales_tax_totals_by_order.html.haml b/app/views/admin/reports/filters/_sales_tax_totals_by_order.html.haml new file mode 100644 index 00000000000..10f6f5ef951 --- /dev/null +++ b/app/views/admin/reports/filters/_sales_tax_totals_by_order.html.haml @@ -0,0 +1,8 @@ +.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}) diff --git a/config/locales/en.yml b/config/locales/en.yml index 106d7ed76e8..231ff401de5 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -1416,6 +1416,7 @@ en: payment_methods: Payment Methods Report delivery: Delivery Report sales_tax_totals_by_producer: Sales Tax Totals By Producer + sales_tax_totals_by_order: Sales Tax Totals By Order tax_types: Tax Types tax_rates: Tax Rates pack_by_customer: Pack By Customer diff --git a/lib/reporting/reports/list.rb b/lib/reporting/reports/list.rb index fffc2a5852c..a41527236a2 100644 --- a/lib/reporting/reports/list.rb +++ b/lib/reporting/reports/list.rb @@ -71,7 +71,8 @@ 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_producer"), :sales_tax_totals_by_producer], + [i18n_translate("sales_tax_totals_by_order"), :sales_tax_totals_by_order] ] end diff --git a/lib/reporting/reports/sales_tax/sales_tax_totals_by_order.rb b/lib/reporting/reports/sales_tax/sales_tax_totals_by_order.rb new file mode 100644 index 00000000000..637e6ca893a --- /dev/null +++ b/lib/reporting/reports/sales_tax/sales_tax_totals_by_order.rb @@ -0,0 +1,194 @@ +# frozen_string_literal: true + +# rubocop:disable Metrics/ClassLength +module Reporting + module Reports + module SalesTax + class SalesTaxTotalsByOrder < Base + def search + report_line_items.orders + end + + def order_permissions + @order_permissions ||= ::Permissions::Order.new(user, ransack_params) + end + + def report_line_items + # Needed to filter by supplier_id + @report_line_items ||= Reporting::LineItems.new(order_permissions, params) + end + + def query_result + # We'll group the line items by + # [tax_rate_id, order_id] + orders = report_line_items.list.map(&:order).uniq + orders.flat_map(&join_tax_rate) + .group_by(&group_key) + .map(&change_root_to_order) + end + + def join_tax_rate + proc do |order| + tax_rate_ids = order.all_adjustments.tax.pluck("distinct(originator_id)") + tax_rate_ids << nil if tax_rate_ids.empty? + tax_rate_ids.map do |tax_rate_id| + { + tax_rate_id: tax_rate_id, + order: order + } + end + end + end + + def group_key + proc do |hash| + [ + hash[:tax_rate_id], + hash[:order].id + ] + end + end + + def change_root_to_order + proc do |key, value| + [key, value.first[:order]] + end + end + + def columns + { + distributor: :distributor, + order_cycle: :order_cycle, + order_number: :order_number, + tax_category: :tax_category, + tax_rate_name: :tax_rate_name, + tax_rate: :tax_rate_amount, + total_excl_tax: :total_excl_tax, + tax: :tax_rate_total, + total_incl_tax: :total_incl_tax, + first_name: :first_name, + last_name: :last_name, + code: :code, + email: :email, + } + end + + def columns_format + { + tax_rate: :percentage + } + end + + def rules + [ + { + group_by: :distributor, + }, + { + group_by: :order_cycle, + }, + { + group_by: :order_number, + summary_row: proc do |_key, items, _rows| + order = items.first.second + { + total_excl_tax: order.total - order.total_tax, + tax: order.total_tax, + total_incl_tax: order.total, + first_name: order.customer&.first_name, + last_name: order.customer&.last_name, + code: order.customer&.code, + email: order.email + } + end + } + ] + end + + def distributor(query_result_row) + order(query_result_row).distributor&.name + end + + def order_cycle(query_result_row) + order(query_result_row).order_cycle&.name + end + + def order_number(query_result_row) + order(query_result_row).number + end + + def tax_category(query_result_row) + tax_rate(query_result_row)&.tax_category&.name + end + + def tax_rate_name(query_result_row) + tax_rate(query_result_row)&.name + end + + def tax_rate_amount(query_result_row) + tax_rate(query_result_row)&.amount + end + + def total_excl_tax(query_result_row) + order(query_result_row).total - order(query_result_row).total_tax + end + + def tax_rate_total(query_result_row) + order(query_result_row).all_adjustments + .tax + .where(originator_id: tax_rate_id(query_result_row)) + .pluck('sum(amount)').first || 0 + end + + def total_incl_tax(query_result_row) + order(query_result_row).total - + order(query_result_row).total_tax + + tax_rate_total(query_result_row) + end + + def first_name(query_result_row) + order(query_result_row).customer&.first_name + end + + def last_name(query_result_row) + order(query_result_row).customer&.last_name + end + + def code(query_result_row) + order(query_result_row).customer&.code + end + + def email(query_result_row) + order(query_result_row).email + end + + def tax_rate(query_result_row) + targeted_tax_rate_id = tax_rate_id(query_result_row) + tax_rates(query_result_row).find do |tax_rate| + tax_rate.id == targeted_tax_rate_id + end + end + + def tax_rate_id(query_result_row) + key(query_result_row).first + end + + def tax_rates(query_result_row) + order(query_result_row).all_adjustments + .tax + .select("distinct(originator_id)", "originator_type") + .map(&:originator) + end + + def key(query_result_row) + query_result_row.first + end + + def order(query_result_row) + query_result_row.second + end + end + end + end +end +# rubocop:enable Metrics/ClassLength diff --git a/spec/controllers/admin/reports_controller_spec.rb b/spec/controllers/admin/reports_controller_spec.rb index 0fcd50a369e..6d01c1eb52f 100644 --- a/spec/controllers/admin/reports_controller_spec.rb +++ b/spec/controllers/admin/reports_controller_spec.rb @@ -352,4 +352,28 @@ end end end + + context "Sales Tax Reports By Order" do + let!(:present_objects) { [orderA1, orderA2, orderB1, orderB2] } + let(:report_type) { :sales_tax_totals_by_order } + context "as an admin" do + before do + controller_login_as_admin + end + it "generates the report" do + spree_get :show, report_type: :sales_tax, report_subtype: report_type + expect(response).to have_http_status(:ok) + expect(resulting_orders_prelim).to include(orderA1, orderA2, orderB1, orderB2) + end + end + context "as distributor1" do + before { controller_login_as_enterprise_user [distributor1] } + it "generates the report" do + spree_get :show, report_type: :sales_tax, report_subtype: report_type + expect(response).to have_http_status(:ok) + expect(resulting_orders_prelim).to include(orderA1, orderB1) + expect(resulting_orders_prelim).to_not include(orderA2, orderB2) + end + end + end end diff --git a/spec/system/admin/reports/sales_tax/sales_tax_totals_by_order_spec.rb b/spec/system/admin/reports/sales_tax/sales_tax_totals_by_order_spec.rb new file mode 100644 index 00000000000..eb9699c99b8 --- /dev/null +++ b/spec/system/admin/reports/sales_tax/sales_tax_totals_by_order_spec.rb @@ -0,0 +1,427 @@ +# frozen_string_literal: true + +require 'system_helper' + +describe "Sales Tax Totals By order" do + # Scenarion 1: added tax + # 1 producer + # 1 distributor + # product that costs 100$ + # shipping costs 10$ + # the packaging cost is 5$ + # 1 order with 1 line item + # the line item match 2 tax rates: country (2.5%) and state (1.5%) + let!(:table_header){ + [ + "Distributor", + "Order Cycle", + "Order Number", + "Tax Category", + "Tax Rate Name", + "Tax Rate", + "Total excl. Tax ($)", + "Tax", + "Total incl. Tax ($)", + "First Name", + "Last Name", + "Code", + "Email" + ].join(" ").upcase + } + let!(:state_zone){ create(:zone_with_state_member) } + let!(:country_zone){ create(:zone_with_member) } + let!(:tax_category){ create(:tax_category, name: 'tax_category') } + let!(:state_tax_rate){ + create(:tax_rate, zone: state_zone, tax_category: tax_category, + name: 'State', amount: 0.015) + } + let!(:country_tax_rate){ + create(:tax_rate, zone: country_zone, tax_category: tax_category, + name: 'Country', amount: 0.025) + } + let!(:ship_address){ + create(:ship_address, + state: state_zone.members.first.zoneable, + country: country_zone.members.first.zoneable) + } + + let!(:variant){ create(:variant) } + let!(:product){ variant.product } + let!(:supplier){ create(:supplier_enterprise, name: 'Supplier', charges_sales_tax: true) } + let!(:distributor){ + create(:distributor_enterprise_with_tax, name: 'Distributor', charges_sales_tax: true) + } + let!(:distributor_fee){ + create(:enterprise_fee, :flat_rate, amount: 5, + tax_category_id: tax_category.id, + enterprise_id: distributor.id) + } + let!(:payment_method){ create(:payment_method, :flat_rate) } + let!(:shipping_method){ + create(:shipping_method, :flat_rate, amount: 10, tax_category_id: tax_category.id) + } + + let!(:order){ create(:order_with_distributor, distributor: distributor) } + let!(:order_cycle){ + create(:simple_order_cycle, name: 'oc1', suppliers: [supplier], distributors: [distributor], + variants: [variant]) + } + let!(:customer1){ + create(:customer, enterprise: create(:enterprise), + user: create(:user), + first_name: 'cfname', last_name: 'clname', code: 'ABC123') + } + + let(:admin){ create(:admin_user) } + + before do + order_cycle.cached_outgoing_exchanges.first.enterprise_fees << distributor_fee + distributor.shipping_methods << shipping_method + distributor.payment_methods << payment_method + + product.update!( + tax_category_id: tax_category.id, + supplier_id: supplier.id + ) + + order.update!( + number: 'ORDER_NUMBER_1', + order_cycle_id: order_cycle.id, + ship_address_id: ship_address.id, + customer_id: customer1.id, + email: 'order1@example.com' + ) + order.line_items.create(variant: variant, quantity: 1, price: 100) + end + + context 'added tax' do + before do + # the enterprise fees can be known only when the user selects the variants + # we'll need to create them by calling recreate_all_fees! + order.recreate_all_fees! + OrderWorkflow.new(order).complete! + end + + it "generates the report" do + login_as admin + visit admin_reports_path + click_on I18n.t("admin.reports.sales_tax_totals_by_order") + + expect(page).to have_button("Go") + click_on "Go" + expect(page.find("table.report__table thead tr").text).to have_content(table_header) + + expect(page.find("table.report__table tbody").text).to have_content([ + "Distributor", + "oc1", + "ORDER_NUMBER_1", + "tax_category", + "State", + "1.5 %", + "115.0", + "1.73", + "116.73", + "cfname", + "clname", + "ABC123", + "order1@example.com" + ].join(" ")) + + expect(page.find("table.report__table tbody").text).to have_content([ + "Distributor", + "oc1", + "ORDER_NUMBER_1", + "tax_category", + "Country", + "2.5 %", + "115.0", + "2.88", + "117.88", + "cfname", + "clname", + "ABC123", + "order1@example.com" + ].join(" ")) + + expect(page.find("table.report__table tbody").text).to have_content([ + "TOTAL", + "115.0", + "4.61", + "119.61", + "cfname", + "clname", + "ABC123", + "order1@example.com" + ].join(" ")) + end + end + + context 'included tax' do + before do + state_tax_rate.update!({ included_in_price: true }) + country_tax_rate.update!({ included_in_price: true }) + + order.recreate_all_fees! + OrderWorkflow.new(order).complete! + end + it "generates the report" do + login_as admin + visit admin_reports_path + click_on I18n.t("admin.reports.sales_tax_totals_by_order") + + expect(page).to have_button("Go") + click_on "Go" + expect(page.find("table.report__table thead tr").text).to have_content(table_header) + + expect(page.find("table.report__table tbody").text).to have_content([ + "Distributor", + "oc1", + "ORDER_NUMBER_1", + "tax_category", + "State", + "1.5 %", + "110.5", + "1.7", + "112.2", + "cfname", + "clname", + "ABC123", + "order1@example.com" + ].join(" ")) + + expect(page.find("table.report__table tbody").text).to have_content([ + "Distributor", + "oc1", + "ORDER_NUMBER_1", + "tax_category", + "Country", + "2.5 %", + "110.5", + "2.8", + "113.3", + "cfname", + "clname", + "ABC123", + "order1@example.com" + ].join(" ")) + + expect(page.find("table.report__table tbody").text).to have_content([ + "TOTAL", + "110.5", + "4.5", + "115.0", + "cfname", + "clname", + "ABC123", + "order1@example.com" + ].join(" ")) + end + end + + context 'should filter by customer' do + let!(:order2){ create(:order_with_distributor, distributor: distributor) } + let!(:customer2){ create(:customer, enterprise: create(:enterprise), user: create(:user)) } + let!(:customer_email_dropdown_selector){ "#s2id_q_customer_id_in" } + let!(:table_raw_selector){ "table.report__table tbody tr" } + let(:customer1_country_tax_rate_row){ + [ + "Distributor", + "oc1", + "ORDER_NUMBER_1", + "tax_category", + "Country", + "2.5 %", + "115.0", + "2.88", + "117.88", + "cfname", + "clname", + "ABC123", + "order1@example.com" + ].join(" ") + } + let(:customer1_state_tax_rate_row){ + [ + "Distributor", + "oc1", + "ORDER_NUMBER_1", + "tax_category", + "State", + "1.5 %", + "115.0", + "1.73", + "116.73", + "cfname", + "clname", + "ABC123", + "order1@example.com" + ].join(" ") + } + let(:customer1_summary_row){ + [ + "TOTAL", + "115.0", + "4.61", + "119.61", + "cfname", + "clname", + "ABC123", + "order1@example.com" + ].join(" ") + } + + let(:customer2_country_tax_rate_row){ + [ + "Distributor", + "oc1", + "ORDER_NUMBER_2", + "tax_category", + "Country", + "2.5 %", + "215.0", + "5.38", + "220.38", + "c2fname", + "c2lname", + "DEF456", + "order2@example.com" + ].join(" ") + } + let(:customer2_state_tax_rate_row){ + [ + "Distributor", + "oc1", + "ORDER_NUMBER_2", + "tax_category", + "State", + "1.5 %", + "215.0", + "3.23", + "218.23", + "c2fname", + "c2lname", + "DEF456", + "order2@example.com" + ].join(" ") + } + let(:customer2_summary_row){ + [ + "TOTAL", + "215.0", + "8.61", + "223.61", + "c2fname", + "c2lname", + "DEF456", + "order2@example.com" + ].join(" ") + } + + before do + order.recreate_all_fees! + OrderWorkflow.new(order).complete! + + customer2.update!({ first_name: 'c2fname', last_name: 'c2lname', code: 'DEF456' }) + order2.line_items.create({ variant: variant, quantity: 1, price: 200 }) + order2.update!({ + order_cycle_id: order_cycle.id, + ship_address_id: customer2.bill_address_id, + customer_id: customer2.id, + number: 'ORDER_NUMBER_2', + email: 'order2@example.com' + }) + order2.recreate_all_fees! + OrderWorkflow.new(order2).complete! + + login_as admin + visit admin_reports_path + click_on I18n.t("admin.reports.sales_tax_totals_by_order") + end + + it "should load all the orders" do + expect(page).to have_button("Go") + click_on "Go" + + expect(page.find("table.report__table thead tr").text).to have_content(table_header) + expect( + page.find("table.report__table tbody").text + ).to have_content(customer1_country_tax_rate_row) + expect( + page.find("table.report__table tbody").text + ).to have_content(customer1_state_tax_rate_row) + expect(page.find("table.report__table tbody").text).to have_content(customer1_summary_row) + expect( + page.find("table.report__table tbody").text + ).to have_content(customer2_country_tax_rate_row) + expect( + page.find("table.report__table tbody").text + ).to have_content(customer2_state_tax_rate_row) + expect(page.find("table.report__table tbody").text).to have_content(customer2_summary_row) + expect(page).to have_selector(table_raw_selector, count: 6) + end + + it "should filter customer1 orders" do + pending + page.find(customer_email_dropdown_selector).click + find('li', text: customer1.email).click + + expect(page).to have_button("Go") + click_on "Go" + + expect(page.find("table.report__table thead tr").text).to have_content(table_header) + expect( + page.find("table.report__table tbody").text + ).to have_content(customer1_country_tax_rate_row) + expect( + page.find("table.report__table tbody").text + ).to have_content(customer1_state_tax_rate_row) + expect(page.find("table.report__table tbody").text).to have_content(customer1_summary_row) + expect(page).to have_selector(table_raw_selector, count: 3) + end + + it "should filter customer2 orders" do + pending + page.find(customer_email_dropdown_selector).click + find('li', text: customer2.email).click + + expect(page).to have_button("Go") + click_on "Go" + + expect(page.find("table.report__table thead tr").text).to have_content(table_header) + expect( + page.find("table.report__table tbody").text + ).to have_content(customer2_country_tax_rate_row) + expect( + page.find("table.report__table tbody").text + ).to have_content(customer2_state_tax_rate_row) + expect(page.find("table.report__table tbody").text).to have_content(customer2_summary_row) + expect(page).to have_selector(table_raw_selector, count: 3) + end + + it "should filter customer1 and customer2 orders" do + pending + page.find(customer_email_dropdown_selector).click + find('li', text: customer1.email).click + page.find(customer_email_dropdown_selector).click + find('li', text: customer2.email).click + click_on "Go" + + expect(page.find("table.report__table thead tr").text).to have_content(table_header) + expect( + page.find("table.report__table tbody").text + ).to have_content(customer1_country_tax_rate_row) + expect( + page.find("table.report__table tbody").text + ).to have_content(customer1_state_tax_rate_row) + expect(page.find("table.report__table tbody").text).to have_content(customer1_summary_row) + expect( + page.find("table.report__table tbody").text + ).to have_content(customer2_country_tax_rate_row) + expect( + page.find("table.report__table tbody").text + ).to have_content(customer2_state_tax_rate_row) + expect(page.find("table.report__table tbody").text).to have_content(customer2_summary_row) + expect(page).to have_selector(table_raw_selector, count: 6) + end + end +end