# frozen_string_literal: true require "system_helper" RSpec.describe ' As an administrator I want numbers, all the numbers! ' do include WebHelper include AuthenticationHelper context "Permissions for different reports" do context "As an enterprise user" do let(:user) do create(:user, enterprises: [create(:distributor_enterprise)]) end it "does not show super admin only report" do login_as user visit admin_reports_path expect(page).not_to have_content "Users & Enterprises" end end context "As an admin user" do it "shows the super admin only report" do login_to_admin_section click_link "Reports" expect(page).to have_content "Users & Enterprises" end end end describe "Background processing" do it "renders UTF-8 characters" do # We had a problem when UTF-8 was in the page and the report because # ActiveStorage read ASCII. # - https://github.com/openfoodfoundation/openfoodnetwork/issues/10758 # # Create order to inject special characters: order = create(:completed_order_with_totals) # Render special characters in the page (filter option): order.distributor.update!(name: "Späti") # Render special character within the report: order.billing_address.update!(lastname: "Müller") # Run the report: login_as_admin visit admin_report_path(report_type: :customers) run_report expect(page).to have_content "Späti" expect(page).to have_content "First Name Last Name Billing Address Email" expect(page).to have_content "Müller" end it "requires confirmation to display big reports" do # Mock data is much faster and accurate than creating many orders: allow_any_instance_of(Reporting::Reports::Customers::Base) .to receive(:columns).and_return( { first_name: proc { |_| "Little Bobby Tables " * (10**5) }, # 2 MB } ) # We still need an order for the report to render a row: create(:completed_order_with_totals) login_as_admin visit admin_report_path(report_type: :customers) run_report expect(page).to have_content "This report is big" expect(page).not_to have_content "Little Bobby Tables" click_on "Display anyway" expect(page).to have_content "First Name" expect(page).to have_content "Little Bobby Tables" end it "displays a friendly timeout message and offers download" do login_as_admin visit admin_report_path(report_type: :customers) stub_const("ReportJob::NOTIFICATION_TIME", 0) run_report # We also get an email. perform_enqueued_jobs(only: ActionMailer::MailDeliveryJob) email = ActionMailer::Base.deliveries.last expect(email.body).to have_link( "customers", href: %r"^http://.*/rails/active_storage/disk/.*/customers_[0-9]+\.html$" ) # ActiveStorage links usually expire after 5 minutes. # But we want a longer expiry in emailed links. parsed_email = Capybara::Node::Simple.new(email.body.to_s) report_link = parsed_email.find(:link, "customers")[:href] content = URI.parse(report_link).read expect(content).to match "\nFirst Name\n" # Let's also check the expiry of the emailed link: travel(3.days) do content = URI.parse(report_link).read expect(content).to match "\nFirst Name\n" end # The link should still expire though: travel(3.months) do expect { URI.parse(report_link).read } .to raise_error OpenURI::HTTPError, "404 Not Found" end end it "allows the report to finish before the loading screen is rendered" do login_as_admin visit admin_report_path(report_type: :customers) # The controller wants to execute the ReportJob in the background. # But we change the logic here, execute it immediately and then wait # until the report is displayed in the browser. # The controller will still be waiting while the browser is receiving # the report via web socket. breakpoint = Mutex.new breakpoint.lock expect(ReportJob).to receive(:perform_later) do |**args| ReportJob.perform_now(**args) breakpoint.synchronize { "continue after unlocked" } end click_on "Go" expect(page).to have_content "First Name Last Name Billing Address Email" # Now that we see the report, we need to make sure that it's not replaced # by the "loading" spinner when the controller action finishes. # Unlocking the breakpoint will continue execution of the controller. breakpoint.unlock # Now the controller response will show the loading spinner again and # the fallback mechanism will render the report later. expect(page).to have_selector ".loading" expect(page).to have_content "First Name Last Name Billing Address Email" expect(page).not_to have_selector ".loading" end end describe "Order cycle management report" do before do login_as_admin visit admin_reports_path end it "payment method report" do click_link "Payment Methods Report" run_report rows = find("table.report__table").all("thead tr") table = rows.map { |r| r.all("th").map { |c| c.text.strip } } expect(table.sort).to eq([ ["First Name", "Last Name", "Hub", "Customer Code", "Email", "Phone", "Shipping Method", "Payment Method", "Amount", "Balance"] ].sort) end it "delivery report" do click_link "Delivery Report" run_report rows = find("table.report__table").all("thead tr") table = rows.map { |r| r.all("th").map { |c| c.text.strip } } expect(table.sort).to eq([ ["First Name", "Last Name", "Hub", "Customer Code", "Delivery Address", "Delivery Postcode", "Phone", "Shipping Method", "Payment Method", "Amount", "Balance", "Temp Controlled Items?", "Special Instructions"] ].sort) end end context "for a complete, paid order" do let!(:ready_to_ship_order) { create(:order_ready_to_ship) } before do login_as_admin visit admin_reports_path end it "generates the payments reports" do click_link 'Payments By Type' run_report rows = find("table.report__table").all("thead tr") table_headers = rows.map { |r| r.all("th").map { |c| c.text.strip } } expect(table_headers).to eq([ ['Payment State', 'Distributor', 'Payment Type', "Total (%s)" % currency_symbol] ]) expect(all('table.report__table tbody tr').count).to eq( Spree::Payment.where( order_id: ready_to_ship_order.id # Total rows should equal number of payments, per order ).count ) end end context "sales tax report" do let(:distributor1) { create(:distributor_enterprise, with_payment_and_shipping: true, charges_sales_tax: true) } let(:distributor2) { create(:distributor_enterprise, with_payment_and_shipping: true, charges_sales_tax: true) } let(:user1) { create(:user, enterprises: [distributor1]) } let(:user2) { create(:user, enterprises: [distributor2]) } let(:shipping_tax_rate) { create(:tax_rate, amount: 0.20, included_in_price: true, zone:) } let(:shipping_tax_category) { create(:tax_category, tax_rates: [shipping_tax_rate]) } let!(:shipping_method) { create(:shipping_method_with, :expensive_name, distributors: [distributor1], tax_category: shipping_tax_category) } let(:enterprise_fee) { create(:enterprise_fee, enterprise: user1.enterprises.first, tax_category: product2.variants.first.tax_category, calculator: Calculator::FlatRate.new(preferred_amount: 120.0)) } let(:order_cycle) { create(:simple_order_cycle, coordinator: distributor1, coordinator_fees: [enterprise_fee], distributors: [distributor1], variants: [product1.variants.first, product2.variants.first]) } let!(:zone) { create(:zone_with_member) } let(:address) { create(:address) } let(:order1) { create(:order, order_cycle:, distributor: user1.enterprises.first, ship_address: address, bill_address: address) } let(:product1) { create(:taxed_product, zone:, price: 12.54, tax_rate_amount: 0, included_in_price: true) } let(:product2) { create(:taxed_product, zone:, price: 500.15, tax_rate_amount: 0.2, included_in_price: true) } let!(:line_item1) { create(:line_item, variant: product1.variants.first, price: 12.54, quantity: 1, order: order1) } let!(:line_item2) { create(:line_item, variant: product2.variants.first, price: 500.15, quantity: 3, order: order1) } before do order1.reload while !order1.delivery? break unless order1.next! end order1.select_shipping_method(shipping_method.id) order1.recreate_all_fees! order_workflow = Orders::WorkflowService.new(order1) order_workflow.advance_to_payment create(:payment, state: "checkout", order: order1, amount: order1.reload.total, payment_method: create(:payment_method, distributors: [distributor1])) order_workflow.complete! login_as_admin visit admin_reports_path end it "generate Tax Types reports" do click_link "Tax Types" run_report # Then it should give me access only to managed enterprises expect(page).to have_select 'q_distributor_id_eq', with_options: [user1.enterprises.first.name] expect(page).not_to have_select 'q_distributor_id_eq', with_options: [user2.enterprises.first.name] # When I filter to just one distributor select user1.enterprises.first.name, from: 'q_distributor_id_eq' run_report # Then I should see the relevant order 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 "250.08" # sales tax expect(page).to have_content "20.0" # enterprise fee tax # And the shipping cost and tax should be correct expect(page).to have_content "100.55" # shipping cost expect(page).to have_content "16.76" # shipping tax # And the total tax should be correct expect(page).to have_content "286.84" # total tax end it "generate Tax Rates report" do click_link "Tax Rates" run_report expect(page).to have_css(".report__table thead th", text: "20.0% ($)") expect(page).to have_css(".report__table thead th", text: "0.0% ($)") expect(page).to have_table_row [order1.number.to_s, "1446.7", "16.76", "0", "270.08", "286.84", "1733.54"] end end describe "products and inventory report" do let(:supplier) { create(:supplier_enterprise, name: 'Supplier Name') } let(:taxon) { create(:taxon, name: 'Taxon Name') } let(:product1) { create(:simple_product, name: "Product Name", price: 100, primary_taxon_id: taxon.id, supplier_id: supplier.id) } let(:product2) { create(:simple_product, name: "Product 2", price: 99.0, variant_unit: 'weight', variant_unit_scale: 1, unit_value: '100', primary_taxon_id: taxon.id, sku: "product_sku", supplier_id: supplier.id) } let(:variant1) { product1.variants.first } let(:variant2) { create(:variant, product: product1, price: 80.0, primary_taxon: taxon, supplier:) } let(:variant3) { product2.variants.first } before do product1.set_property 'Organic', 'NASAA 12345' product2.set_property 'Organic', 'NASAA 12345' variant1.on_hand = 10 variant1.update!(sku: "sku1") variant2.on_hand = 20 variant2.update!(sku: "sku2") variant3.on_hand = 9 variant3.update!(sku: "") end it "shows report error at the bottom of page" do login_as_admin visit admin_reports_path click_link 'All products' report = Reporting::Reports::ProductsAndInventory::AllProducts click_on "Go" allow(report).to receive(:new).and_raise(StandardError, 'Provoked error for testing') perform_enqueued_jobs(only: ReportJob) expect(page).not_to have_selector ".loading" expect(page).to have_button "Go", disabled: false expect(page).to have_content 'This report failed. It may be too big to process. ' \ 'We will look into it but please let us know ' \ 'if the problem persists.' # Admin shoulb be able to make some changes and retry allow(report).to receive(:new).and_call_original run_report expect(page).to have_content "Supplier" end it "shows products and inventory report" do login_as_admin visit admin_reports_path expect(page).to have_content "All products" expect(page).to have_content "Inventory (on hand)" click_link 'All products' run_report expect(page).to have_content "Supplier" expect(page).to have_table_row ["Supplier", "Producer Suburb", "Product", "Product Properties", "Taxons", "Variant Value", "Price", "Group Buy Unit Quantity", "Amount", "SKU", "On Demand?", "On Hand"] expect(page).to have_table_row [supplier.name, supplier.address.city, "Product Name", product1.properties.map(&:presentation).join(", "), taxon.name, "1g", "100.0", "none", "", "sku1", "No", "10"] expect(page).to have_table_row [supplier.name, supplier.address.city, "Product Name", product1.properties.map(&:presentation).join(", "), taxon.name, "1g", "80.0", "none", "", "sku2", "No", "20"] expect(page).to have_table_row [supplier.name, supplier.address.city, "Product 2", product1.properties.map(&:presentation).join(", "), taxon.name, "100g", "99.0", "none", "", "product_sku", "No", "9"] end it "shows the LettuceShare report" do login_as_admin visit admin_reports_path click_link 'LettuceShare' run_report expect(page).to have_table_row ['PRODUCT', 'Description', 'Qty', 'Pack Size', 'Unit', 'Unit Price', 'Total', 'GST incl.', 'Grower and growing method', 'Taxon'] expect(page).to have_table_row ['Product 2', '100g', '', '100', 'g', '99.0', '', '0', 'Supplier Name (Organic - NASAA 12345)', 'Taxon Name'] end end describe "users and enterprises report" do let!(:enterprise1) { create( :enterprise, owner: create(:user) ) } let!(:enterprise2) { create( :enterprise, owner: create(:user) ) } let!(:enterprise3) { create( :enterprise, owner: create(:user) ) } before do enterprise3.enterprise_roles.build( user: enterprise1.owner ).save login_as_admin visit admin_reports_path click_link 'Users & Enterprises' end it "shows users and enterprises report" do run_report rows = find("table.report__table").all("tr") table = rows.map { |r| r.all("th,td").map { |c| c.text.strip }[0..2] } expect(table.sort).to eq([ ["User", "Relationship", "Enterprise"], [enterprise1.owner.email, "owns", enterprise1.name], [enterprise1.owner.email, "manages", enterprise1.name], [enterprise2.owner.email, "owns", enterprise2.name], [enterprise2.owner.email, "manages", enterprise2.name], [enterprise3.owner.email, "owns", enterprise3.name], [enterprise3.owner.email, "manages", enterprise3.name], [enterprise1.owner.email, "manages", enterprise3.name] ].sort) end it "filters the list" do select enterprise3.name, from: "enterprise_id_in" select enterprise1.owner.email, from: "user_id_in" run_report rows = find("table.report__table").all("tr") table = rows.map { |r| r.all("th,td").map { |c| c.text.strip }[0..2] } expect(table.sort).to eq([ ["User", "Relationship", "Enterprise"], [enterprise1.owner.email, "manages", enterprise3.name] ].sort) end end describe 'bulk coop report' do let!(:order) { create(:completed_order_with_totals) } before do login_as_admin visit admin_reports_path end it "generating Bulk Co-op Supplier Report" do click_link "Bulk Co-op Supplier Report" run_report expect(page).to have_table_row [ "Supplier", "Product", "Bulk Unit Size", "Variant", "Variant Value", "Variant Unit", "Weight", "Sum Total", "Units Required", "Unallocated", "Max Quantity Excess" ] end it "generating Bulk Co-op Allocation report" do click_link "Bulk Co-op Allocation" run_report expect(page).to have_table_row [ "Customer", "Product", "Bulk Unit Size", "Variant", "Variant Value", "Variant Unit", "Weight", "Sum Total", "Total available", "Unallocated", "Max Quantity Excess" ] end it "generating Bulk Co-op Packing Sheets report" do click_link "Bulk Co-op Packing Sheets" run_report expect(page).to have_table_row [ "Customer", "Product", "Variant", "Sum Total" ] end it "generating Bulk Co-op Customer Payments report" do click_link "Bulk Co-op Customer Payments" run_report expect(page).to have_table_row [ "Customer", "Date of Order", "Total Cost", "Amount Owing", "Amount Paid" ] end end describe "Xero invoices report" do let(:distributor1) { create(:distributor_enterprise, with_payment_and_shipping: true, charges_sales_tax: true) } let(:distributor2) { create(:distributor_enterprise, with_payment_and_shipping: true, charges_sales_tax: true) } let(:user1) { create(:user, enterprises: [distributor1]) } let(:user2) { create(:user, enterprises: [distributor2]) } let(:shipping_method) { create(:shipping_method_with, :expensive_name) } let(:shipment) { create(:shipment_with, :shipping_method, shipping_method:) } let(:enterprise_fee1) { create(:enterprise_fee, enterprise: user1.enterprises.first, tax_category: product2.variants.first.tax_category, calculator: Calculator::FlatRate.new(preferred_amount: 10)) } let(:enterprise_fee2) { create(:enterprise_fee, enterprise: user1.enterprises.first, tax_category: product2.variants.first.tax_category, calculator: Calculator::FlatRate.new(preferred_amount: 20)) } let(:order_cycle) { create(:simple_order_cycle, coordinator: distributor1, coordinator_fees: [enterprise_fee1, enterprise_fee2], distributors: [distributor1], variants: [product1.variants.first]) } let!(:zone) { create(:zone_with_member) } let(:bill_address) { create(:address, firstname: 'Customer', lastname: 'Name', address1: 'customer l1', address2: '', city: 'customer city', zipcode: 1234) } let(:order1) { create(:order, order_cycle:, distributor: user1.enterprises.first, shipments: [shipment], bill_address:, state: 'payment') } let(:product1) { create(:taxed_product, zone:, price: 12.54, tax_rate_amount: 0, sku: 'sku1') } let(:product2) { create(:taxed_product, zone:, price: 500.15, tax_rate_amount: 0.2, sku: 'sku2') } describe "with adjustments" do let!(:line_item1) { create(:line_item, variant: product1.variants.first, price: 12.54, quantity: 1, order: order1) } let!(:line_item2) { create(:line_item, variant: product2.variants.first, price: 500.15, quantity: 3, order: order1) } let!(:tax_category) { create(:tax_category) } let!(:tax_rate) { create(:tax_rate, tax_category:) } let!(:adj_shipping) { create(:adjustment, order: order1, adjustable: order1, label: "Shipping", originator: shipping_method, amount: 100.55) } let!(:adj_fee1) { create(:adjustment, order: order1, adjustable: order1, originator: enterprise_fee1, label: "Enterprise fee untaxed", amount: 10) } let!(:adj_fee2) { create(:adjustment, order: order1, adjustable: order1, originator: enterprise_fee2, label: "Enterprise fee taxed", amount: 20, tax_category:) } let!(:adj_fee2_tax) { create(:adjustment, order: order1, adjustable: adj_fee2, originator: tax_rate, amount: 3, state: "closed") } let!(:adj_admin1) { create(:adjustment, order: order1, adjustable: order1, originator: nil, label: "Manual adjustment", amount: 30) } let!(:adj_admin2) { create(:adjustment, order: order1, adjustable: order1, originator: nil, label: "Manual adjustment", amount: 40, tax_category:) } before do order1.update_order! order1.update!(email: 'customer@email.com') order1.shipment.update(included_tax_total: 10.06) travel_to(Time.zone.local(2021, 4, 25, 14, 0, 0)) { order1.finalize! } order1.reload order1.create_tax_charge! end before do travel_to(Time.zone.local(2021, 4, 26, 14, 0, 0)) end context "summary report" do before do login_as_admin visit admin_reports_path click_link "Summary" run_report end it "shows Xero invoices report" do expect(xero_invoice_table).to match_table [ 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, 'GST on Income'), xero_invoice_summary_row('Total untaxable fees (no tax)', 10.0, 'GST Free Income'), xero_invoice_summary_row('Total taxable fees (tax inclusive)', 20.0, 'GST on Income'), xero_invoice_summary_row('Delivery Shipping Cost (tax inclusive)', 100.55, 'GST on Income'), xero_invoice_summary_row('Total untaxable admin adjustments (no tax)', 30.0, 'GST Free Income'), xero_invoice_summary_row('Total taxable admin adjustments (tax inclusive)', 40.0, 'GST on Income') ] end it "can customise a number of fields" do fill_in 'initial_invoice_number', with: '5' pick_datetime '#invoice_date', Date.new(2021, 2, 12) pick_datetime '#due_date', Date.new(2021, 3, 12) fill_in 'account_code', with: 'abc123' run_report opts = { invoice_number: '5', invoice_date: '2021-02-12', due_date: '2021-03-12', account_code: 'abc123' } expect(xero_invoice_table).to match_table [ 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, 'GST on Income', opts), xero_invoice_summary_row('Total untaxable fees (no tax)', 10.0, 'GST Free Income', opts), xero_invoice_summary_row('Total taxable fees (tax inclusive)', 20.0, 'GST on Income', opts), xero_invoice_summary_row('Delivery Shipping Cost (tax inclusive)', 100.55, 'GST on Income', opts), xero_invoice_summary_row('Total untaxable admin adjustments (no tax)', 30.0, 'GST Free Income', opts), xero_invoice_summary_row('Total taxable admin adjustments (tax inclusive)', 40.0, 'GST on Income', opts) ] end end context "detailed report" do it "generates a detailed report" do login_as_admin visit admin_reports_path click_link "Detailed" run_report opts = {} expect(xero_invoice_table).to match_table [ xero_invoice_header, xero_invoice_li_row(line_item1), xero_invoice_li_row(line_item2), xero_invoice_adjustment_row(adj_admin1), xero_invoice_adjustment_row(adj_admin2), xero_invoice_summary_row('Total untaxable fees (no tax)', 10.0, 'GST Free Income', opts), xero_invoice_summary_row('Total taxable fees (tax inclusive)', 20.0, 'GST on Income', opts), xero_invoice_summary_row('Delivery Shipping Cost (tax inclusive)', 100.55, 'GST on Income', opts) ] end end end private def xero_invoice_table find("table.report__table") end def xero_invoice_header %w(*ContactName EmailAddress POAddressLine1 POAddressLine2 POAddressLine3 POAddressLine4 POCity PORegion POPostalCode POCountry *InvoiceNumber Reference *InvoiceDate *DueDate InventoryItemCode *Description *Quantity *UnitAmount Discount *AccountCode *TaxType TrackingName1 TrackingOption1 TrackingName2 TrackingOption2 Currency BrandingTheme Paid?) end def xero_invoice_summary_row(description, amount, tax_type, opts = {}) xero_invoice_row '', description, amount, '1', tax_type, opts end def xero_invoice_li_row(line_item, opts = {}) tax_type = line_item.has_tax? ? 'GST on Income' : 'GST Free Income' xero_invoice_row line_item.variant.sku, line_item.product_and_full_name, line_item.price.to_s, line_item.quantity.to_s, tax_type, opts end def xero_invoice_adjustment_row(adjustment, opts = {}) tax_type = adjustment.has_tax? ? 'GST on Income' : 'GST Free Income' xero_invoice_row('', adjustment.label, adjustment.amount, '1', tax_type, opts) end def xero_invoice_row(sku, description, amount, quantity, tax_type, opts = {}) opts.reverse_merge!(customer_name: 'Customer Name', address1: 'customer l1', city: 'customer city', state: 'Victoria', zipcode: '1234', country: 'Australia', invoice_number: order1.number, order_number: order1.number, invoice_date: '2021-04-26', due_date: '2021-05-26', account_code: 'food sales') [opts[:customer_name], 'customer@email.com', opts[:address1], '', '', '', opts[:city], opts[:state], opts[:zipcode], opts[:country], opts[:invoice_number], opts[:order_number], opts[:invoice_date], opts[:due_date], sku, description, quantity, amount.to_s, '', opts[:account_code], tax_type, '', '', '', '', "AUD", '', 'N'] end end end