diff --git a/.rubocop_todo.yml b/.rubocop_todo.yml index bb9a3b0171..ae5ba7ddd7 100644 --- a/.rubocop_todo.yml +++ b/.rubocop_todo.yml @@ -1,6 +1,6 @@ # This configuration was generated by # `rubocop --auto-gen-config --auto-gen-only-exclude --exclude-limit 1400` -# on 2022-02-25 01:04:47 UTC using RuboCop version 1.22.2. +# on 2022-03-29 16:07:39 UTC using RuboCop version 1.22.2. # The point is for the user to remove these configuration records # one by one as the offenses are removed from the code base. # Note that changes in the inspected code, or installation of new @@ -53,7 +53,7 @@ Layout/LeadingCommentSpace: Exclude: - 'spec/system/admin/enterprises_spec.rb' -# Offense count: 828 +# Offense count: 856 # Cop supports --auto-correct. # Configuration parameters: Max, AllowHeredoc, AllowURI, URISchemes, IgnoreCopDirectives, IgnoredPatterns. # URISchemes: http, https @@ -108,7 +108,6 @@ Layout/LineLength: - 'app/services/order_syncer.rb' - 'app/services/products_renderer.rb' - 'app/services/variant_units/variant_and_line_item_naming.rb' - - 'lib/open_food_network/bulk_coop_report.rb' - 'engines/order_management/app/services/order_management/subscriptions/validator.rb' - 'engines/order_management/spec/services/order_management/order/updater_spec.rb' - 'engines/web/app/helpers/web/cookies_policy_helper.rb' @@ -117,15 +116,20 @@ Layout/LineLength: - 'lib/open_food_network/enterprise_fee_applicator.rb' - 'lib/open_food_network/enterprise_fee_calculator.rb' - 'lib/open_food_network/enterprise_issue_validator.rb' - - 'lib/open_food_network/lettuce_share_report.rb' - 'lib/open_food_network/order_cycle_form_applicator.rb' - - 'lib/open_food_network/order_cycle_management_report.rb' - - 'lib/open_food_network/order_cycle_permissions.rb' - - 'lib/open_food_network/payments_report.rb' - - 'lib/open_food_network/reports/line_items.rb' - - 'lib/open_food_network/sales_tax_report.rb' - 'lib/open_food_network/scope_variants_for_search.rb' - - 'lib/open_food_network/xero_invoices_report.rb' + - 'lib/reporting/line_items.rb' + - 'lib/reporting/reports/bulk_coop/bulk_coop_report.rb' + - 'lib/reporting/reports/enterprise_fee_summary/report_data/enterprise_fee_type_total.rb' + - 'lib/reporting/reports/order_cycle_management/order_cycle_management_report.rb' + - 'lib/open_food_network/order_cycle_permissions.rb' + - 'lib/reporting/reports/orders_and_fulfillment/customer_totals_report.rb' + - 'lib/reporting/reports/orders_and_fulfillment/distributor_totals_by_supplier_report.rb' + - 'lib/reporting/reports/payments/payments_report.rb' + - 'lib/reporting/reports/products_and_inventory/lettuce_share_report.rb' + - 'lib/reporting/reports/sales_tax/sales_tax_report.rb' + - 'lib/reporting/reports/users_and_enterprises/users_and_enterprises_report.rb' + - 'lib/reporting/reports/xero_invoices/xero_invoices_report.rb' - 'lib/spree/localized_number.rb' - 'lib/tasks/data.rake' - 'lib/tasks/enterprises.rake' @@ -176,19 +180,20 @@ Layout/LineLength: - 'spec/helpers/spree/admin/base_helper_spec.rb' - 'spec/jobs/subscription_confirm_job_spec.rb' - 'spec/jobs/subscription_placement_job_spec.rb' - - 'spec/lib/open_food_network/customers_report_spec.rb' - 'spec/lib/open_food_network/enterprise_fee_calculator_spec.rb' - - 'spec/lib/open_food_network/group_buy_report_spec.rb' - 'spec/lib/open_food_network/order_cycle_form_applicator_spec.rb' - - 'spec/lib/open_food_network/order_cycle_management_report_spec.rb' - 'spec/lib/open_food_network/order_cycle_permissions_spec.rb' - - 'spec/lib/open_food_network/order_grouper_spec.rb' - 'spec/lib/open_food_network/permissions_spec.rb' - - 'spec/lib/open_food_network/products_and_inventory_report_spec.rb' - 'spec/lib/open_food_network/scope_variant_to_hub_spec.rb' - 'spec/lib/open_food_network/tag_rule_applicator_spec.rb' - - 'spec/lib/open_food_network/users_and_enterprises_report_spec.rb' + - 'spec/lib/reports/customers_report_spec.rb' + - 'spec/lib/reports/order_cycle_management_report_spec.rb' + - 'spec/lib/reports/order_grouper_spec.rb' + - 'spec/lib/reports/orders_and_fulfillment/orders_and_fulfillment_report_spec.rb' - 'spec/lib/reports/packing/packing_report_spec.rb' + - 'spec/lib/reports/products_and_inventory_default_report_spec.rb' + - 'spec/lib/reports/users_and_enterprises_report_spec.rb' + - 'spec/lib/reports/xero_invoices_report_spec.rb' - 'spec/lib/stripe/authorize_response_patcher_spec.rb' - 'spec/mailers/order_mailer_spec.rb' - 'spec/mailers/producer_mailer_spec.rb' @@ -309,7 +314,15 @@ Layout/MultilineMethodCallBraceLayout: Exclude: - 'lib/reporting/queries/joins.rb' -# Offense count: 17 +# Offense count: 2 +# Cop supports --auto-correct. +# Configuration parameters: EnforcedStyle, IndentationWidth. +# SupportedStyles: aligned, indented, indented_relative_to_receiver +Layout/MultilineMethodCallIndentation: + Exclude: + - 'lib/reporting/reports/customers/customers_report.rb' + +# Offense count: 20 # Cop supports --auto-correct. # Configuration parameters: AllowInHeredoc. Layout/TrailingWhitespace: @@ -331,7 +344,7 @@ Lint/ConstantDefinitionInBlock: - 'lib/tasks/users.rake' - 'spec/controllers/spree/admin/base_controller_spec.rb' - 'spec/helpers/serializer_helper_spec.rb' - - 'spec/lib/open_food_network/reports/line_items_spec.rb' + - 'spec/lib/reports/line_items_spec.rb' - 'spec/models/spree/ability_spec.rb' - 'spec/models/spree/gateway_spec.rb' - 'spec/models/spree/preferences/configuration_spec.rb' @@ -398,7 +411,7 @@ Lint/UselessMethodDefinition: - 'app/controllers/spree/user_registrations_controller.rb' - 'app/models/spree/gateway.rb' -# Offense count: 39 +# Offense count: 38 # Configuration parameters: IgnoredMethods, CountRepeatedAttributes, Max. Metrics/AbcSize: Exclude: @@ -419,22 +432,21 @@ Metrics/AbcSize: - 'app/models/spree/order/checkout.rb' - 'app/models/spree/preferences/preferable_class_methods.rb' - 'app/models/spree/return_authorization.rb' - - 'lib/open_food_network/bulk_coop_report.rb' - 'lib/discourse/single_sign_on.rb' - - 'lib/open_food_network/customers_report.rb' - - 'lib/open_food_network/group_buy_report.rb' - - 'lib/open_food_network/orders_and_distributors_report.rb' - 'lib/open_food_network/order_cycle_form_applicator.rb' + - 'lib/reporting/reports/bulk_coop/bulk_coop_report.rb' + - 'lib/reporting/reports/customers/customers_report.rb' - 'lib/open_food_network/order_cycle_permissions.rb' - - 'lib/open_food_network/payments_report.rb' - - 'lib/open_food_network/sales_tax_report.rb' + - 'lib/reporting/reports/orders_and_distributors/orders_and_distributors_report.rb' - 'lib/reporting/reports/packing/customer.rb' + - 'lib/reporting/reports/payments/payments_report.rb' + - 'lib/reporting/reports/sales_tax/sales_tax_report.rb' - 'lib/spree/core/controller_helpers/order.rb' - 'lib/spree/core/s3_support.rb' - 'lib/tasks/enterprises.rake' - 'spec/services/order_checkout_restart_spec.rb' -# Offense count: 45 +# Offense count: 43 # Configuration parameters: CountComments, Max, CountAsOne, ExcludedMethods, IgnoredMethods. # IgnoredMethods: refine Metrics/BlockLength: @@ -458,13 +470,12 @@ Metrics/BlockLength: - 'spec/factories/subscription_factory.rb' - 'spec/factories/user_factory.rb' - 'spec/factories/variant_factory.rb' - - 'spec/lib/open_food_network/group_buy_report_spec.rb' - 'spec/requests/api/orders_spec.rb' - 'spec/spec_helper.rb' - - 'spec/swagger_helper.rb' - 'spec/support/cancan_helper.rb' - 'spec/support/matchers/select2_matchers.rb' - 'spec/support/matchers/table_matchers.rb' + - 'spec/swagger_helper.rb' - 'spec/system/admin/order_cycles/complex_updating_specific_time_spec.rb' - 'spec/system/consumer/shopping/checkout_spec.rb' @@ -474,7 +485,7 @@ Metrics/BlockNesting: Exclude: - 'app/models/spree/payment/processing.rb' -# Offense count: 49 +# Offense count: 50 # Configuration parameters: CountComments, Max, CountAsOne. Metrics/ClassLength: Exclude: @@ -518,19 +529,18 @@ Metrics/ClassLength: - 'app/services/cart_service.rb' - 'app/services/order_syncer.rb' - 'engines/order_management/app/services/order_management/order/updater.rb' - - 'lib/open_food_network/bulk_coop_report.rb' - - 'engines/order_management/app/services/order_management/reports/enterprise_fee_summary/scope.rb' - 'lib/open_food_network/enterprise_fee_calculator.rb' - 'lib/open_food_network/order_cycle_form_applicator.rb' - - 'lib/open_food_network/order_cycle_management_report.rb' - - 'lib/open_food_network/order_cycle_permissions.rb' - - 'lib/open_food_network/payments_report.rb' - 'lib/open_food_network/permissions.rb' - - 'lib/open_food_network/users_and_enterprises_report.rb' - - 'lib/open_food_network/xero_invoices_report.rb' - - 'lib/open_food_network/bulk_coop_report.rb' + - 'lib/reporting/reports/bulk_coop/bulk_coop_report.rb' + - 'lib/reporting/reports/enterprise_fee_summary/scope.rb' + - 'lib/reporting/reports/order_cycle_management/order_cycle_management_report.rb' + - 'lib/open_food_network/order_cycle_permissions.rb' + - 'lib/reporting/reports/payments/payments_report.rb' + - 'lib/reporting/reports/users_and_enterprises/users_and_enterprises_report.rb' + - 'lib/reporting/reports/xero_invoices/xero_invoices_report.rb' -# Offense count: 40 +# Offense count: 39 # Configuration parameters: IgnoredMethods, Max. Metrics/CyclomaticComplexity: Exclude: @@ -556,20 +566,19 @@ Metrics/CyclomaticComplexity: - 'app/models/spree/tax_rate.rb' - 'app/models/spree/variant.rb' - 'app/models/spree/zone.rb' - - 'lib/open_food_network/bulk_coop_report.rb' - 'lib/discourse/single_sign_on.rb' - - 'lib/open_food_network/customers_report.rb' - 'lib/open_food_network/enterprise_issue_validator.rb' - - 'lib/open_food_network/group_buy_report.rb' - - 'lib/open_food_network/orders_and_fulfillment_report/customer_totals_report.rb' - - 'lib/open_food_network/payments_report.rb' - - 'lib/open_food_network/xero_invoices_report.rb' + - 'lib/reporting/reports/bulk_coop/bulk_coop_report.rb' + - 'lib/reporting/reports/customers/customers_report.rb' + - 'lib/reporting/reports/orders_and_fulfillment/customer_totals_report.rb' + - 'lib/reporting/reports/payments/payments_report.rb' + - 'lib/reporting/reports/xero_invoices/xero_invoices_report.rb' - 'lib/spree/core/controller_helpers/order.rb' - 'lib/spree/core/controller_helpers/respond_with.rb' - 'lib/spree/localized_number.rb' - 'spec/models/product_importer_spec.rb' -# Offense count: 31 +# Offense count: 32 # Configuration parameters: CountComments, Max, CountAsOne, ExcludedMethods, IgnoredMethods. Metrics/MethodLength: Exclude: @@ -579,23 +588,23 @@ Metrics/MethodLength: - 'app/controllers/spree/orders_controller.rb' - 'app/helpers/checkout_helper.rb' - 'app/helpers/spree/admin/navigation_helper.rb' - - "app/json_schemas/json_api_schema.rb" + - 'app/json_schemas/json_api_schema.rb' - 'app/models/spree/ability.rb' - 'app/models/spree/gateway/pay_pal_express.rb' - 'app/models/spree/order/checkout.rb' - 'app/models/spree/payment/processing.rb' - 'app/models/spree/preferences/preferable_class_methods.rb' - - 'lib/open_food_network/bulk_coop_report.rb' - - 'engines/order_management/app/services/order_management/reports/enterprise_fee_summary/scope.rb' - 'lib/discourse/single_sign_on.rb' - 'lib/open_food_network/order_cycle_form_applicator.rb' - - 'lib/open_food_network/order_cycle_management_report.rb' + - 'lib/reporting/reports/bulk_coop/bulk_coop_report.rb' + - 'lib/reporting/reports/enterprise_fee_summary/scope.rb' + - 'lib/reporting/reports/order_cycle_management/order_cycle_management_report.rb' - 'lib/open_food_network/order_cycle_permissions.rb' - - 'lib/open_food_network/payments_report.rb' - - 'lib/open_food_network/xero_invoices_report.rb' + - 'lib/reporting/reports/payments/payments_report.rb' + - 'lib/reporting/reports/xero_invoices/xero_invoices_report.rb' - 'lib/tasks/sample_data/product_factory.rb' -# Offense count: 51 +# Offense count: 54 # Configuration parameters: CountComments, Max, CountAsOne. Metrics/ModuleLength: Exclude: @@ -626,17 +635,20 @@ Metrics/ModuleLength: - 'spec/controllers/spree/admin/adjustments_controller_spec.rb' - 'spec/controllers/spree/admin/payment_methods_controller_spec.rb' - 'spec/lib/open_food_network/address_finder_spec.rb' - - 'spec/lib/open_food_network/customers_report_spec.rb' - 'spec/lib/open_food_network/enterprise_fee_calculator_spec.rb' - 'spec/lib/open_food_network/order_cycle_form_applicator_spec.rb' - - 'spec/lib/open_food_network/order_cycle_management_report_spec.rb' - 'spec/lib/open_food_network/order_cycle_permissions_spec.rb' - - 'spec/lib/open_food_network/order_grouper_spec.rb' - 'spec/lib/open_food_network/permissions_spec.rb' - - 'spec/lib/open_food_network/products_and_inventory_report_spec.rb' - 'spec/lib/open_food_network/scope_variant_to_hub_spec.rb' - 'spec/lib/open_food_network/tag_rule_applicator_spec.rb' - - 'spec/lib/open_food_network/users_and_enterprises_report_spec.rb' + - 'spec/lib/reports/customers_report_spec.rb' + - 'spec/lib/reports/enterprise_fee_summary/authorizer_spec.rb' + - 'spec/lib/reports/order_cycle_management_report_spec.rb' + - 'spec/lib/reports/order_grouper_spec.rb' + - 'spec/lib/reports/orders_and_fulfillment/customer_totals_report_spec.rb' + - 'spec/lib/reports/orders_and_fulfillment/orders_and_fulfillment_report_spec.rb' + - 'spec/lib/reports/products_and_inventory_default_report_spec.rb' + - 'spec/lib/reports/users_and_enterprises_report_spec.rb' - 'spec/models/spree/adjustment_spec.rb' - 'spec/models/spree/credit_card_spec.rb' - 'spec/models/spree/line_item_spec.rb' @@ -657,11 +669,11 @@ Metrics/ParameterLists: Exclude: - 'app/helpers/angular_form_builder.rb' - 'app/models/product_import/entry_processor.rb' - - 'lib/open_food_network/xero_invoices_report.rb' + - 'lib/reporting/reports/xero_invoices/xero_invoices_report.rb' - 'spec/support/controller_requests_helper.rb' - 'spec/system/admin/reports_spec.rb' -# Offense count: 8 +# Offense count: 7 # Configuration parameters: IgnoredMethods, Max. Metrics/PerceivedComplexity: Exclude: @@ -670,9 +682,8 @@ Metrics/PerceivedComplexity: - 'app/models/enterprise_relationship.rb' - 'app/models/spree/ability.rb' - 'app/models/spree/order/checkout.rb' - - 'lib/open_food_network/bulk_coop_report.rb' - - 'lib/open_food_network/group_buy_report.rb' - - 'lib/open_food_network/payments_report.rb' + - 'lib/reporting/reports/bulk_coop/bulk_coop_report.rb' + - 'lib/reporting/reports/payments/payments_report.rb' # Offense count: 9 Naming/AccessorMethodName: @@ -717,7 +728,7 @@ Naming/VariableNumber: - 'app/controllers/spree/orders_controller.rb' - 'app/models/content_configuration.rb' - 'app/models/preference_sections/main_links_section.rb' - - 'lib/open_food_network/orders_and_fulfillment_report/customer_totals_report.rb' + - 'lib/reporting/reports/orders_and_fulfillment/customer_totals_report.rb' - 'lib/spree/core/controller_helpers/common.rb' - 'spec/controllers/spree/admin/search_controller_spec.rb' - 'spec/factories/stock_location_factory.rb' @@ -896,7 +907,7 @@ Rails/LexicallyScopedActionFilter: - 'app/controllers/spree/admin/zones_controller.rb' - 'app/controllers/spree/users_controller.rb' -# Offense count: 18 +# Offense count: 19 Rails/OutputSafety: Exclude: - 'app/controllers/spree/admin/reports_controller.rb' @@ -1093,9 +1104,9 @@ Style/MissingRespondToMissing: # Offense count: 1 Style/MixinUsage: Exclude: - - 'lib/open_food_network/orders_and_fulfillment_report.rb' + - 'lib/reporting/reports/orders_and_fulfillment/orders_and_fulfillment_report.rb' -# Offense count: 2 +# Offense count: 3 # Cop supports --auto-correct. # Configuration parameters: EnforcedStyle. # SupportedStyles: literals, strict @@ -1119,7 +1130,7 @@ Style/NestedModifier: - 'spec/system/admin/payments_stripe_spec.rb' - 'spec/system/admin/reports_spec.rb' -# Offense count: 25 +# Offense count: 26 # Configuration parameters: AllowedMethods. # AllowedMethods: respond_to_missing? Style/OptionalBooleanParameter: @@ -1133,18 +1144,17 @@ Style/OptionalBooleanParameter: - 'app/models/spree/order_contents.rb' - 'app/models/spree/preferences/file_configuration.rb' - 'app/models/spree/shipment.rb' - - 'lib/open_food_network/bulk_coop_report.rb' - 'engines/order_management/app/services/order_management/stock/estimator.rb' - - 'engines/order_management/app/services/order_management/reports/enterprise_fee_summary/enterprise_fee_summary_report.rb' - - 'lib/open_food_network/customers_report.rb' - - 'lib/open_food_network/orders_and_distributors_report.rb' - - 'lib/open_food_network/order_cycle_management_report.rb' - - 'lib/open_food_network/orders_and_fulfillment_report.rb' - - 'lib/open_food_network/payments_report.rb' - - 'lib/open_food_network/products_and_inventory_report.rb' - - 'lib/open_food_network/users_and_enterprises_report.rb' - - 'lib/open_food_network/xero_invoices_report.rb' - - 'lib/open_food_network/bulk_coop_report.rb' + - 'lib/reporting/reports/bulk_coop/bulk_coop_report.rb' + - 'lib/reporting/reports/customers/customers_report.rb' + - 'lib/reporting/reports/enterprise_fee_summary/enterprise_fee_summary_report.rb' + - 'lib/reporting/reports/order_cycle_management/order_cycle_management_report.rb' + - 'lib/reporting/reports/orders_and_distributors/orders_and_distributors_report.rb' + - 'lib/reporting/reports/orders_and_fulfillment/orders_and_fulfillment_report.rb' + - 'lib/reporting/reports/payments/payments_report.rb' + - 'lib/reporting/reports/products_and_inventory/products_and_inventory_report.rb' + - 'lib/reporting/reports/users_and_enterprises/users_and_enterprises_report.rb' + - 'lib/reporting/reports/xero_invoices/xero_invoices_report.rb' - 'lib/spree/core/controller_helpers/order.rb' - 'lib/spree/core/delegate_belongs_to.rb' - 'spec/support/request/web_helper.rb' @@ -1164,11 +1174,10 @@ Style/RedundantReturn: Exclude: - 'app/controllers/spree/admin/shipping_methods_controller.rb' -# Offense count: 213 +# Offense count: 205 Style/Send: Exclude: - 'app/controllers/split_checkout_controller.rb' - - 'engines/order_management/spec/services/order_management/reports/bulk_coop/bulk_coop_report_spec.rb' - 'spec/controllers/admin/subscriptions_controller_spec.rb' - 'spec/controllers/checkout_controller_spec.rb' - 'spec/controllers/payment_gateways/paypal_controller_spec.rb' @@ -1180,13 +1189,10 @@ Style/Send: - 'spec/lib/open_food_network/address_finder_spec.rb' - 'spec/lib/open_food_network/enterprise_fee_applicator_spec.rb' - 'spec/lib/open_food_network/enterprise_fee_calculator_spec.rb' - - 'spec/lib/open_food_network/lettuce_share_report_spec.rb' - 'spec/lib/open_food_network/order_cycle_form_applicator_spec.rb' - 'spec/lib/open_food_network/permissions_spec.rb' - - 'spec/lib/open_food_network/products_and_inventory_report_spec.rb' - - 'spec/lib/open_food_network/sales_tax_report_spec.rb' - 'spec/lib/open_food_network/tag_rule_applicator_spec.rb' - - 'spec/lib/open_food_network/xero_invoices_report_spec.rb' + - 'spec/lib/reports/xero_invoices_report_spec.rb' - 'spec/lib/stripe/webhook_handler_spec.rb' - 'spec/models/calculator/weight_spec.rb' - 'spec/models/enterprise_spec.rb' @@ -1211,7 +1217,7 @@ Style/SingleArgumentDig: Exclude: - 'app/services/checkout/form_data_adapter.rb' -# Offense count: 5 +# Offense count: 4 # Cop supports --auto-correct. Style/SlicingWithRange: Exclude: @@ -1219,9 +1225,8 @@ Style/SlicingWithRange: - 'app/services/embedded_page_service.rb' - 'engines/order_management/app/services/order_management/subscriptions/validator.rb' - 'lib/discourse/single_sign_on.rb' - - 'spec/lib/open_food_network/order_grouper_spec.rb' -# Offense count: 31 +# Offense count: 28 # Cop supports --auto-correct. # Configuration parameters: Mode. Style/StringConcatenation: @@ -1238,11 +1243,8 @@ Style/StringConcatenation: - 'app/serializers/api/enterprise_shopfront_list_serializer.rb' - 'app/services/embedded_page_service.rb' - 'app/services/products_renderer.rb' - - 'lib/open_food_network/bulk_coop_report.rb' - - 'lib/open_food_network/orders_and_fulfillment_report/customer_totals_report.rb' - 'lib/spree/api/controller_setup.rb' - 'lib/spree/core/environment_extension.rb' - - 'spec/lib/open_food_network/order_grouper_spec.rb' - 'spec/models/spree/line_item_spec.rb' - 'spec/models/spree/product_spec.rb' - 'spec/models/spree/variant_spec.rb' diff --git a/app/controllers/spree/admin/reports_controller.rb b/app/controllers/spree/admin/reports_controller.rb index 552def4802..253ebbf45a 100644 --- a/app/controllers/spree/admin/reports_controller.rb +++ b/app/controllers/spree/admin/reports_controller.rb @@ -1,19 +1,17 @@ # frozen_string_literal: true -require 'open_food_network/reports/list' -require 'open_food_network/orders_and_distributors_report' -require 'open_food_network/products_and_inventory_report' -require 'open_food_network/lettuce_share_report' -require 'open_food_network/group_buy_report' -require 'open_food_network/order_grouper' -require 'open_food_network/customers_report' -require 'open_food_network/users_and_enterprises_report' -require 'open_food_network/order_cycle_management_report' -require 'open_food_network/sales_tax_report' -require 'open_food_network/xero_invoices_report' -require 'open_food_network/payments_report' -require 'open_food_network/orders_and_fulfillment_report' -require 'open_food_network/bulk_coop_report' +# require 'open_food_network/orders_and_distributors_report' +# require 'open_food_network/products_and_inventory_report' +# require 'open_food_network/lettuce_share_report' +# require 'open_food_network/order_grouper' +# require 'open_food_network/customers_report' +# require 'open_food_network/users_and_enterprises_report' +# require 'open_food_network/order_cycle_management_report' +# require 'open_food_network/sales_tax_report' +# require 'open_food_network/xero_invoices_report' +# require 'open_food_network/payments_report' +# require 'open_food_network/orders_and_fulfillment_report' +# require 'open_food_network/bulk_coop_report' module Spree module Admin @@ -30,7 +28,7 @@ module Spree respond_to :html def report_types - OpenFoodNetwork::Reports::List.all + Reporting::Reports::List.all end def index @@ -119,11 +117,7 @@ module Spree def render_report @report_subtypes = report_types[action_name.to_sym] @report_subtype = params[:report_subtype] - klass = if action_name == 'enterprise_fee_summary' - OrderManagement::Reports::EnterpriseFeeSummary::EnterpriseFeeSummaryReport - else - "OpenFoodNetwork::#{action_name.camelize}Report".constantize - end + klass = "Reporting::Reports::#{action_name.camelize}::#{action_name.camelize}Report".constantize @report = klass.new spree_current_user, raw_params, render_content? if report_format.present? data = Reporting::ReportRenderer.new(@report).public_send("to_#{report_format}") diff --git a/app/validators/date_time_string_validator.rb b/app/validators/date_time_string_validator.rb index a70bf9a0c7..6b5706b114 100644 --- a/app/validators/date_time_string_validator.rb +++ b/app/validators/date_time_string_validator.rb @@ -38,8 +38,13 @@ # post.valid? # => false # post.errors[:published_at] # => ["must be valid"] class DateTimeStringValidator < ActiveModel::EachValidator - NOT_STRING_ERROR = I18n.t("validators.date_time_string_validator.not_string_error") - INVALID_FORMAT_ERROR = I18n.t("validators.date_time_string_validator.invalid_format_error") + def self.not_string_error + I18n.t("validators.date_time_string_validator.not_string_error") + end + + def self.invalid_format_error + I18n.t("validators.date_time_string_validator.invalid_format_error") + end def validate_each(record, attribute, value) return if value.nil? || value == "" @@ -53,13 +58,13 @@ class DateTimeStringValidator < ActiveModel::EachValidator def validate_attribute_is_string(record, attribute, value) return if value.is_a?(String) - record.errors.add(attribute, NOT_STRING_ERROR) + record.errors.add(attribute, DateTimeStringValidator.not_string_error) end def validate_attribute_is_datetime_string(record, attribute, value) return unless value.is_a?(String) datetime = Time.zone.parse(value) - record.errors.add(attribute, INVALID_FORMAT_ERROR) if datetime.blank? + record.errors.add(attribute, DateTimeStringValidator.invalid_format_error) if datetime.blank? end end diff --git a/app/validators/integer_array_validator.rb b/app/validators/integer_array_validator.rb index 64f718a4d3..883b69f0cf 100644 --- a/app/validators/integer_array_validator.rb +++ b/app/validators/integer_array_validator.rb @@ -35,8 +35,13 @@ # post.valid? # => false # post.errors[:related_post_ids] # => ["must contain only valid integers"] class IntegerArrayValidator < ActiveModel::EachValidator - NOT_ARRAY_ERROR = I18n.t("validators.integer_array_validator.not_array_error") - INVALID_ELEMENT_ERROR = I18n.t("validators.integer_array_validator.invalid_element_error") + def self.not_array_error + I18n.t("validators.integer_array_validator.not_array_error") + end + + def self.invalid_element_error + I18n.t("validators.integer_array_validator.invalid_element_error") + end def validate_each(record, attribute, value) return if value.nil? @@ -50,7 +55,7 @@ class IntegerArrayValidator < ActiveModel::EachValidator def validate_attribute_is_array(record, attribute, value) return if value.is_a?(Array) - record.errors.add(attribute, NOT_ARRAY_ERROR) + record.errors.add(attribute, IntegerArrayValidator.not_array_error) end def validate_attribute_elements_are_integer(record, attribute, array) @@ -60,6 +65,6 @@ class IntegerArrayValidator < ActiveModel::EachValidator Integer(element) end rescue ArgumentError - record.errors.add(attribute, INVALID_ELEMENT_ERROR) + record.errors.add(attribute, IntegerArrayValidator.invalid_element_error) end end diff --git a/engines/order_management/app/services/reports/authorizer.rb b/engines/order_management/app/services/reports/authorizer.rb deleted file mode 100644 index 2e58266579..0000000000 --- a/engines/order_management/app/services/reports/authorizer.rb +++ /dev/null @@ -1,29 +0,0 @@ -# frozen_string_literal: true - -module Reports - class Authorizer - class ParameterNotAllowedError < StandardError; end - - attr_accessor :parameters, :permissions - - def initialize(parameters, permissions) - @parameters = parameters - @permissions = permissions - end - - def self.parameter_not_allowed_error_message - i18n_scope = "order_management.reports.enterprise_fee_summary" - I18n.t("parameter_not_allowed_error", scope: i18n_scope) - end - - private - - def require_ids_allowed(array, allowed_objects) - error_klass = ::Reports::Authorizer::ParameterNotAllowedError - error_message = self.class.parameter_not_allowed_error_message - ids_allowed = (array - allowed_objects.map(&:id).map(&:to_s)).blank? - - raise error_klass, error_message unless ids_allowed - end - end -end diff --git a/engines/order_management/app/services/reports/parameters/base.rb b/engines/order_management/app/services/reports/parameters/base.rb deleted file mode 100644 index 0a3490f914..0000000000 --- a/engines/order_management/app/services/reports/parameters/base.rb +++ /dev/null @@ -1,35 +0,0 @@ -# frozen_string_literal: true - -module Reports - module Parameters - class Base - extend ActiveModel::Naming - extend ActiveModel::Translation - include ActiveModel::Validations - include ActiveModel::Validations::Callbacks - - def initialize(attributes = {}) - attributes.each do |key, value| - public_send("#{key}=", value) - end - end - - def self.date_end_before_start_error_message - i18n_scope = "order_management.reports.enterprise_fee_summary" - I18n.t("date_end_before_start_error", scope: i18n_scope) - end - - # The parameters are never persisted. - def to_key; end - - protected - - def require_valid_datetime_range - return if start_at.blank? || end_at.blank? - - error_message = self.class.date_end_before_start_error_message - errors.add(:end_at, error_message) unless start_at < end_at - end - end - end -end diff --git a/engines/order_management/app/services/reports/report_data/base.rb b/engines/order_management/app/services/reports/report_data/base.rb deleted file mode 100644 index 94fd55ac3f..0000000000 --- a/engines/order_management/app/services/reports/report_data/base.rb +++ /dev/null @@ -1,13 +0,0 @@ -# frozen_string_literal: true - -module Reports - module ReportData - class Base - def initialize(attributes = {}) - attributes.each do |key, value| - public_send("#{key}=", value) - end - end - end - end -end diff --git a/engines/order_management/spec/services/order_management/reports/enterprise_fee_summary/authorizer_spec.rb b/engines/order_management/spec/services/order_management/reports/enterprise_fee_summary/authorizer_spec.rb deleted file mode 100644 index bc9e40c852..0000000000 --- a/engines/order_management/spec/services/order_management/reports/enterprise_fee_summary/authorizer_spec.rb +++ /dev/null @@ -1,174 +0,0 @@ -# frozen_string_literal: true - -require "spec_helper" - -describe OrderManagement::Reports::EnterpriseFeeSummary::Authorizer do - let(:report_klass) { OrderManagement::Reports::EnterpriseFeeSummary } - let(:user) { create(:user) } - - let(:parameters) { report_klass::Parameters.new(params) } - let(:permissions) { report_klass::Permissions.new(user) } - let(:authorizer) { described_class.new(parameters, permissions) } - - context "for distributors" do - before do - allow(permissions).to receive(:allowed_distributors) do - stub_model_collection(Enterprise, :id, ["1", "2", "3"]) - end - end - - context "when distributors are allowed" do - let(:params) { { distributor_ids: ["1", "3"] } } - - it "does not raise error" do - expect { authorizer.authorize! }.not_to raise_error - end - end - - context "when a distributor is not allowed" do - let(:params) { { distributor_ids: ["1", "4"] } } - - it "raises ParameterNotAllowedError" do - expect { authorizer.authorize! } - .to raise_error(Reports::Authorizer::ParameterNotAllowedError) - end - end - end - - context "for producers" do - before do - allow(permissions).to receive(:allowed_producers) do - stub_model_collection(Enterprise, :id, ["1", "2", "3"]) - end - end - - context "when producers are allowed" do - let(:params) { { producer_ids: ["1", "3"] } } - - it "does not raise error" do - expect { authorizer.authorize! }.not_to raise_error - end - end - - context "when a producer is not allowed" do - let(:params) { { producer_ids: ["1", "4"] } } - - it "raises ParameterNotAllowedError" do - expect { authorizer.authorize! } - .to raise_error(Reports::Authorizer::ParameterNotAllowedError) - end - end - end - - context "for order cycles" do - before do - allow(permissions).to receive(:allowed_order_cycles) do - stub_model_collection(OrderCycle, :id, ["1", "2", "3"]) - end - end - - context "when order cycles are allowed" do - let(:params) { { order_cycle_ids: ["1", "3"] } } - - it "does not raise error" do - expect { authorizer.authorize! }.not_to raise_error - end - end - - context "when an order cycle is not allowed" do - let(:params) { { order_cycle_ids: ["1", "4"] } } - - it "raises ParameterNotAllowedError" do - expect { authorizer.authorize! } - .to raise_error(Reports::Authorizer::ParameterNotAllowedError) - end - end - end - - context "for enterprise fees" do - before do - allow(permissions).to receive(:allowed_enterprise_fees) do - stub_model_collection(EnterpriseFee, :id, ["1", "2", "3"]) - end - end - - context "when enterprise fees are allowed" do - let(:params) { { enterprise_fee_ids: ["1", "3"] } } - - it "does not raise error" do - expect { authorizer.authorize! }.not_to raise_error - end - end - - context "when an enterprise fee is not allowed" do - let(:params) { { enterprise_fee_ids: ["1", "4"] } } - - it "raises ParameterNotAllowedError" do - expect { authorizer.authorize! } - .to raise_error(Reports::Authorizer::ParameterNotAllowedError) - end - end - end - - context "for shipping methods" do - before do - allow(permissions).to receive(:allowed_shipping_methods) do - stub_model_collection(Spree::ShippingMethod, :id, ["1", "2", "3"]) - end - end - - context "when shipping methods are allowed" do - let(:params) { { shipping_method_ids: ["1", "3"] } } - - it "does not raise error" do - expect { authorizer.authorize! }.not_to raise_error - end - end - - context "when a shipping method is not allowed" do - let(:params) { { shipping_method_ids: ["1", "4"] } } - - it "raises ParameterNotAllowedError" do - expect { authorizer.authorize! } - .to raise_error(Reports::Authorizer::ParameterNotAllowedError) - end - end - end - - context "for payment methods" do - before do - allow(permissions).to receive(:allowed_payment_methods) do - stub_model_collection(Spree::PaymentMethod, :id, ["1", "2", "3"]) - end - end - - context "when payment methods are allowed" do - let(:params) { { payment_method_ids: ["1", "3"] } } - - it "does not raise error" do - expect { authorizer.authorize! }.not_to raise_error - end - end - - context "when a payment method is not allowed" do - let(:params) { { payment_method_ids: ["1", "4"] } } - - it "raises ParameterNotAllowedError" do - expect { authorizer.authorize! } - .to raise_error(Reports::Authorizer::ParameterNotAllowedError) - end - end - end - - def stub_model_collection(model, attribute_name, attribute_list) - attribute_list.map do |attribute_value| - stub_model(model, attribute_name => attribute_value) - end - end - - def stub_model(model, params) - model.new.tap do |instance| - instance.stub(params) - end - end -end diff --git a/engines/order_management/spec/services/order_management/reports/enterprise_fee_summary/parameters_spec.rb b/engines/order_management/spec/services/order_management/reports/enterprise_fee_summary/parameters_spec.rb deleted file mode 100644 index 0a2da275b1..0000000000 --- a/engines/order_management/spec/services/order_management/reports/enterprise_fee_summary/parameters_spec.rb +++ /dev/null @@ -1,90 +0,0 @@ -# frozen_string_literal: true - -require "spec_helper" - -require "date_time_string_validator" - -describe OrderManagement::Reports::EnterpriseFeeSummary::Parameters do - describe "validation" do - let(:parameters) { described_class.new } - - it "allows all parameters to be blank" do - expect(parameters).to be_valid - end - - context "for type of parameters" do - it { is_expected.to validate_date_time_format_of(:start_at) } - it { is_expected.to validate_date_time_format_of(:end_at) } - it { is_expected.to validate_integer_array(:distributor_ids) } - it { is_expected.to validate_integer_array(:producer_ids) } - it { is_expected.to validate_integer_array(:order_cycle_ids) } - it { is_expected.to validate_integer_array(:enterprise_fee_ids) } - it { is_expected.to validate_integer_array(:shipping_method_ids) } - it { is_expected.to validate_integer_array(:payment_method_ids) } - - it "allows integer arrays to include blank string and cleans it up" do - subject.distributor_ids = ["", "1"] - subject.producer_ids = ["", "1"] - subject.order_cycle_ids = ["", "1"] - subject.enterprise_fee_ids = ["", "1"] - subject.shipping_method_ids = ["", "1"] - subject.payment_method_ids = ["", "1"] - - expect(subject).to be_valid - - expect(subject.distributor_ids).to eq(["1"]) - expect(subject.producer_ids).to eq(["1"]) - expect(subject.order_cycle_ids).to eq(["1"]) - expect(subject.enterprise_fee_ids).to eq(["1"]) - expect(subject.shipping_method_ids).to eq(["1"]) - expect(subject.payment_method_ids).to eq(["1"]) - end - - describe "requiring start_at to be before end_at" do - let(:now) { Time.zone.now.utc } - - it "adds error when start_at is after end_at" do - allow(subject).to receive(:start_at) { now.to_s } - allow(subject).to receive(:end_at) { (now - 1.hour).to_s } - - expect(subject).not_to be_valid - error_message = described_class.date_end_before_start_error_message - expect(subject.errors[:end_at]).to eq([error_message]) - end - - it "does not add error when start_at is before end_at" do - allow(subject).to receive(:start_at) { now.to_s } - allow(subject).to receive(:end_at) { (now + 1.hour).to_s } - - expect(subject).to be_valid - end - end - end - end - - describe "smoke authorization" do - let!(:order_cycle) { create(:order_cycle) } - let!(:user) { create(:user) } - - let(:permissions) do - report_klass::Permissions.new(nil).tap do |instance| - instance.stub(allowed_order_cycles: [order_cycle]) - end - end - - it "does not raise error when the parameters are allowed" do - parameters = described_class.new(order_cycle_ids: [order_cycle.id.to_s]) - expect { parameters.authorize!(permissions) }.not_to raise_error - end - - it "raises error when the parameters are not allowed" do - parameters = described_class.new(order_cycle_ids: [(order_cycle.id + 1).to_s]) - expect { parameters.authorize!(permissions) } - .to raise_error(Reports::Authorizer::ParameterNotAllowedError) - end - end - - def report_klass - OrderManagement::Reports::EnterpriseFeeSummary - end -end diff --git a/lib/open_food_network/bulk_coop_allocation_report.rb b/lib/open_food_network/bulk_coop_allocation_report.rb deleted file mode 100644 index 37d6071ec1..0000000000 --- a/lib/open_food_network/bulk_coop_allocation_report.rb +++ /dev/null @@ -1,63 +0,0 @@ -# frozen_string_literal: true - -module OpenFoodNetwork - class BulkCoopAllocationReport - def table_headers - [ - I18n.t(:report_header_customer), - I18n.t(:report_header_product), - I18n.t(:report_header_bulk_unit_size), - I18n.t(:report_header_variant), - I18n.t(:report_header_variant_value), - I18n.t(:report_header_variant_unit), - I18n.t(:report_header_weight), - I18n.t(:report_header_sum_total), - I18n.t(:report_header_total_available), - I18n.t(:report_header_unallocated), - I18n.t(:report_header_max_quantity_excess), - ] - end - - def rules - [ - { - group_by: proc { |line_item| line_item.product }, - sort_by: proc { |product| product.name }, - summary_columns: [ - :total_label, - :variant_product_name, - :variant_product_group_buy_unit_size_f, - :empty_cell, - :empty_cell, - :empty_cell, - :empty_cell, - :total_amount, - :total_available, - :remainder, - :max_quantity_excess - ] - }, - { - group_by: proc { |line_item| line_item.order }, - sort_by: proc { |order| order.to_s } - } - ] - end - - def columns - [ - :order_billing_address_name, - :product_name, - :product_group_buy_unit_size, - :full_name, - :option_value_value, - :option_value_unit, - :weight_from_unit_value, - :total_amount, - :empty_cell, - :empty_cell, - :empty_cell - ] - end - end -end diff --git a/lib/open_food_network/bulk_coop_report.rb b/lib/open_food_network/bulk_coop_report.rb deleted file mode 100644 index 271c172b48..0000000000 --- a/lib/open_food_network/bulk_coop_report.rb +++ /dev/null @@ -1,320 +0,0 @@ -# frozen_string_literal: true - -require "open_food_network/reports/line_items" -require 'open_food_network/order_grouper' -require 'open_food_network/bulk_coop_allocation_report' -require 'open_food_network/bulk_coop_supplier_report' - -module OpenFoodNetwork - class BulkCoopReport - attr_reader :params - - def initialize(user, params = {}, render_table = false) - @params = params - @user = user - @render_table = render_table - - @supplier_report = BulkCoopSupplierReport.new - @allocation_report = BulkCoopAllocationReport.new - @filter_canceled = false - end - - def table_headers - case params[:report_subtype] - when "bulk_coop_supplier_report" - @supplier_report.table_headers - when "bulk_coop_allocation" - @allocation_report.table_headers - when "bulk_coop_packing_sheets" - [I18n.t(:report_header_customer), - I18n.t(:report_header_product), - I18n.t(:report_header_variant), - I18n.t(:report_header_sum_total)] - when "bulk_coop_customer_payments" - [I18n.t(:report_header_customer), - I18n.t(:report_header_date_of_order), - I18n.t(:report_header_total_cost), - I18n.t(:report_header_amount_owing), - I18n.t(:report_header_amount_paid)] - else - [I18n.t(:report_header_supplier), - I18n.t(:report_header_product), - I18n.t(:report_header_product), - I18n.t(:report_header_bulk_unit_size), - I18n.t(:report_header_variant), - I18n.t(:report_header_weight), - I18n.t(:report_header_sum_total), - I18n.t(:report_header_sum_max_total), - I18n.t(:report_header_units_required), - I18n.t(:report_header_remainder)] - end - end - - def search - report_line_items.orders - end - - def table_items - return [] unless @render_table - - report_line_items.list(line_item_includes) - end - - def table_rows - order_grouper = OpenFoodNetwork::OrderGrouper.new rules, columns, self - order_grouper.table(table_items) - end - - def rules - case params[:report_subtype] - when "bulk_coop_supplier_report" - @supplier_report.rules - when "bulk_coop_allocation" - @allocation_report.rules - when "bulk_coop_packing_sheets" - [{ group_by: proc { |li| li.product }, - sort_by: proc { |product| product.name } }, - { group_by: proc { |li| li.full_name }, - sort_by: proc { |full_name| full_name } }, - { group_by: proc { |li| li.order }, - sort_by: proc { |order| order.to_s } }] - when "bulk_coop_customer_payments" - [{ group_by: proc { |li| li.order }, - sort_by: proc { |order| order.completed_at } }] - else - [{ group_by: proc { |li| li.product.supplier }, - sort_by: proc { |supplier| supplier.name } }, - { group_by: proc { |li| li.product }, - sort_by: proc { |product| product.name }, - summary_columns: [proc { |lis| lis.first.product.supplier.name }, - proc { |lis| lis.first.product.name }, - proc { |lis| lis.first.product.group_buy_unit_size || 0.0 }, - proc { |_lis| "" }, - proc { |_lis| "" }, - proc { |lis| - lis.sum { |li| - li.quantity * (li.weight_from_unit_value || 0) - } - }, - proc { |lis| - lis.sum { |li| - (li.max_quantity || 0) * (li.weight_from_unit_value || 0) - } - }, - proc { |lis| - ( if (lis.first.product.group_buy_unit_size || 0).zero? - 0 - else - ( lis.sum { |li| - [li.max_quantity || 0, - li.quantity || 0].max * (li.weight_from_unit_value || 0) - } / lis.first.product.group_buy_unit_size ) - end ).floor - }, - proc { |lis| - lis.sum { |li| - [li.max_quantity || 0, - li.quantity || 0].max * (li.weight_from_unit_value || 0) - } - ( ( if (lis.first.product.group_buy_unit_size || 0).zero? - 0 - else - ( lis.sum { |li| - [li.max_quantity || 0, - li.quantity || 0].max * (li.weight_from_unit_value || 0) - } / lis.first.product.group_buy_unit_size ) - end ).floor * (lis.first.product.group_buy_unit_size || 0) ) - }] }, - { group_by: proc { |li| li.full_name }, - sort_by: proc { |full_name| full_name } }] - end - end - - def columns - case params[:report_subtype] - when "bulk_coop_supplier_report" - @supplier_report.columns - when "bulk_coop_allocation" - @allocation_report.columns - when "bulk_coop_packing_sheets" - [ - :order_billing_address_name, - :product_name, - :full_name, - :total_quantity - ] - when "bulk_coop_customer_payments" - [ - :order_billing_address_name, - :order_completed_at, - :customer_payments_total_cost, - :customer_payments_amount_owed, - :customer_payments_amount_paid - ] - else - [ - :product_supplier_name, - :product_name, - :product_group_buy_unit_size, - :full_name, - :weight_from_unit_value, - :total_quantity, - :total_max_quantity, - :empty_cell, - :empty_cell - ] - end - end - - private - - attr_reader :filter_canceled - - def line_item_includes - [ - { - order: [:bill_address], - variant: [{ option_values: :option_type }, { product: :supplier }] - }, - :option_values - ] - end - - def order_permissions - @order_permissions ||= ::Permissions::Order.new(@user, filter_canceled) - end - - def report_line_items - @report_line_items ||= OpenFoodNetwork::Reports::LineItems.new( - order_permissions, - @params, - CompleteVisibleOrders.new(order_permissions).query - ) - end - - def customer_payments_total_cost(line_items) - unique_orders(line_items).sum(&:total) - end - - def customer_payments_amount_owed(line_items) - unique_orders(line_items).sum(&:new_outstanding_balance) - end - - def customer_payments_amount_paid(line_items) - unique_orders(line_items).sum(&:payment_total) - end - - def unique_orders(line_items) - line_items.map(&:order).uniq - end - - def empty_cell(_line_items) - "" - end - - def full_name(line_items) - line_items.first.full_name - end - - def group_buy_unit_size(line_items) - unit_size = line_items.first.variant.product.group_buy_unit_size || 0.0 - unit_size / (line_items.first.product.variant_unit_scale || 1) - end - - def max_quantity_excess(line_items) - max_quantity_amount(line_items) - total_amount(line_items) - end - - def max_quantity_amount(line_items) - line_items.sum do |line_item| - max_quantity = [line_item.max_quantity || 0, line_item.quantity || 0].max - max_quantity * scaled_unit_value(line_item.variant) - end - end - - def option_value_value(line_items) - VariantUnits::OptionValueNamer.new(line_items.first).value - end - - def option_value_unit(line_items) - VariantUnits::OptionValueNamer.new(line_items.first).unit - end - - def order_billing_address_name(line_items) - billing_address = line_items.first.order.bill_address - billing_address.firstname + " " + billing_address.lastname - end - - def order_completed_at(line_items) - line_items.first.order.completed_at.to_s - end - - def product_group_buy_unit_size(line_items) - line_items.first.product.group_buy_unit_size || 0.0 - end - - def product_name(line_items) - line_items.first.product.name - end - - def product_supplier_name(line_items) - line_items.first.product.supplier.name - end - - def remainder(line_items) - remainder = total_available(line_items) - total_amount(line_items) - remainder >= 0 ? remainder : '' - end - - def scaled_final_weight_volume(line_item) - (line_item.final_weight_volume || 0) / (line_item.product.variant_unit_scale || 1) - end - - def scaled_unit_value(variant) - (variant.unit_value || 0) / (variant.product.variant_unit_scale || 1) - end - - def total_amount(line_items) - line_items.sum { |li| scaled_final_weight_volume(li) } - end - - def total_available(line_items) - units_required(line_items) * group_buy_unit_size(line_items) - end - - def total_max_quantity(line_items) - line_items.sum { |line_item| line_item.max_quantity || 0 } - end - - def total_quantity(line_items) - line_items.sum(&:quantity) - end - - def total_label(_line_items) - I18n.t('admin.reports.total') - end - - def units_required(line_items) - if group_buy_unit_size(line_items).zero? - 0 - else - ( total_amount(line_items) / group_buy_unit_size(line_items) ).ceil - end - end - - def variant_product_group_buy_unit_size_f(line_items) - group_buy_unit_size(line_items) - end - - def variant_product_name(line_items) - line_items.first.variant.product.name - end - - def variant_product_supplier_name(line_items) - line_items.first.variant.product.supplier.name - end - - def weight_from_unit_value(line_items) - line_items.first.weight_from_unit_value || 0 - end - end -end diff --git a/lib/open_food_network/bulk_coop_supplier_report.rb b/lib/open_food_network/bulk_coop_supplier_report.rb deleted file mode 100644 index 1f2645f81b..0000000000 --- a/lib/open_food_network/bulk_coop_supplier_report.rb +++ /dev/null @@ -1,61 +0,0 @@ -# frozen_string_literal: true - -module OpenFoodNetwork - class BulkCoopSupplierReport - def table_headers - [ - I18n.t(:report_header_supplier), - I18n.t(:report_header_product), - I18n.t(:report_header_bulk_unit_size), - I18n.t(:report_header_variant), - I18n.t(:report_header_variant_value), - I18n.t(:report_header_variant_unit), - I18n.t(:report_header_weight), - I18n.t(:report_header_sum_total), - I18n.t(:report_header_units_required), - I18n.t(:report_header_unallocated), - I18n.t(:report_header_max_quantity_excess), - ] - end - - def rules - [ - { group_by: proc { |line_item| line_item.product.supplier }, - sort_by: proc { |supplier| supplier.name } }, - { group_by: proc { |line_item| line_item.product }, - sort_by: proc { |product| product.name }, - summary_columns: [ - :variant_product_supplier_name, - :variant_product_name, - :variant_product_group_buy_unit_size_f, - :empty_cell, - :empty_cell, - :empty_cell, - :empty_cell, - :total_amount, - :units_required, - :remainder, - :max_quantity_excess - ] }, - { group_by: proc { |line_item| line_item.full_name }, - sort_by: proc { |full_name| full_name } } - ] - end - - def columns - [ - :variant_product_supplier_name, - :variant_product_name, - :variant_product_group_buy_unit_size_f, - :full_name, - :option_value_value, - :option_value_unit, - :weight_from_unit_value, - :total_amount, - :empty_cell, - :empty_cell, - :empty_cell - ] - end - end -end diff --git a/lib/open_food_network/customers_report.rb b/lib/open_food_network/customers_report.rb deleted file mode 100644 index c52edad648..0000000000 --- a/lib/open_food_network/customers_report.rb +++ /dev/null @@ -1,99 +0,0 @@ -# frozen_string_literal: true - -module OpenFoodNetwork - class CustomersReport - attr_reader :params - - def initialize(user, params = {}, compile_table = false) - @params = params - @user = user - @compile_table = compile_table - end - - def table_headers - if is_mailing_list? - [I18n.t(:report_header_email), - I18n.t(:report_header_first_name), - I18n.t(:report_header_last_name), - I18n.t(:report_header_suburb)] - else - [I18n.t(:report_header_first_name), - I18n.t(:report_header_last_name), - I18n.t(:report_header_billing_address), - I18n.t(:report_header_email), - I18n.t(:report_header_phone), - I18n.t(:report_header_hub), - I18n.t(:report_header_hub_address), - I18n.t(:report_header_shipping_method)] - end - end - - def table_rows - return [] unless @compile_table - - orders.map do |order| - if is_mailing_list? - [order.email, - order.billing_address.firstname, - order.billing_address.lastname, - order.billing_address.city] - else - ba = order.billing_address - da = order.distributor&.address - [ba.firstname, - ba.lastname, - [ba.address1, ba.address2, ba.city].join(" "), - order.email, - ba.phone, - order.distributor&.name, - [da&.address1, da&.address2, da&.city].join(" "), - order.shipping_method&.name] - end - end - end - - def orders - filter Spree::Order.managed_by(@user).distributed_by_user(@user).complete.not_state(:canceled) - end - - def filter(orders) - filter_to_supplier filter_to_distributor filter_to_order_cycle orders - end - - def filter_to_supplier(orders) - if params[:supplier_id].to_i > 0 - orders.select do |order| - order.line_items.includes(:product) - .where("spree_products.supplier_id = ?", params[:supplier_id].to_i) - .references(:product) - .count - .positive? - end - else - orders - end - end - - def filter_to_distributor(orders) - if params[:distributor_id].to_i > 0 - orders.where(distributor_id: params[:distributor_id]) - else - orders - end - end - - def filter_to_order_cycle(orders) - if params[:order_cycle_id].to_i > 0 - orders.where(order_cycle_id: params[:order_cycle_id]) - else - orders - end - end - - private - - def is_mailing_list? - params[:report_subtype] == "mailing_list" - end - end -end diff --git a/lib/open_food_network/lettuce_share_report.rb b/lib/open_food_network/lettuce_share_report.rb deleted file mode 100644 index b465769568..0000000000 --- a/lib/open_food_network/lettuce_share_report.rb +++ /dev/null @@ -1,89 +0,0 @@ -# frozen_string_literal: true - -require 'variant_units/option_value_namer' - -module OpenFoodNetwork - class LettuceShareReport - attr_reader :context - - delegate :variants, :render_table, to: :context - - def initialize(context) - @context = context - end - - def table_headers - # NOTE: These are NOT to be translated, they need to be in this exact format to work with LettucShare - [ - "PRODUCT", - "Description", - "Qty", - "Pack Size", - "Unit", - "Unit Price", - "Total", - "GST incl.", - "Grower and growing method", - "Taxon" - ] - end - - def table_rows - return [] unless render_table - - variants.select(&:in_stock?) - .map do |variant| - [ - variant.product.name, - variant.full_name, - '', - VariantUnits::OptionValueNamer.new(variant).value, - VariantUnits::OptionValueNamer.new(variant).unit, - variant.price, - '', - gst(variant), - grower_and_method(variant), - variant.product.primary_taxon.name - ] - end - end - - private - - def gst(variant) - tax_category = variant.product.tax_category - if tax_category && tax_category.tax_rates.present? - tax_rate = tax_category.tax_rates.first - line_item = mock_line_item(variant) - tax_rate.calculator.compute line_item - else - 0 - end - end - - def mock_line_item(variant) - line_item = Spree::LineItem.new quantity: 1 - line_item.define_singleton_method(:product) { variant.product } - line_item.define_singleton_method(:price) { variant.price } - line_item - end - - def grower_and_method(variant) - cert = certification(variant) - - result = producer_name(variant) - result += " (#{cert})" if cert.present? - result - end - - def producer_name(variant) - variant.product.supplier.name - end - - def certification(variant) - variant.product.properties_including_inherited.map do |p| - "#{p[:name]} - #{p[:value]}" - end.join(', ') - end - end -end diff --git a/lib/open_food_network/order_cycle_management_report.rb b/lib/open_food_network/order_cycle_management_report.rb deleted file mode 100644 index 06857cf063..0000000000 --- a/lib/open_food_network/order_cycle_management_report.rb +++ /dev/null @@ -1,165 +0,0 @@ -# frozen_string_literal: true - -module OpenFoodNetwork - class OrderCycleManagementReport - DEFAULT_DATE_INTERVAL = { from: -1.month, to: 1.day }.freeze - - attr_reader :params - - def initialize(user, params = {}, render_table = false) - @params = sanitize_params(params) - @user = user - @render_table = render_table - end - - def table_headers - if is_payment_methods? - [ - I18n.t(:report_header_first_name), - I18n.t(:report_header_last_name), - I18n.t(:report_header_hub), - I18n.t(:report_header_hub_code), - I18n.t(:report_header_email), - I18n.t(:report_header_phone), - I18n.t(:report_header_shipping_method), - I18n.t(:report_header_payment_method), - I18n.t(:report_header_amount), - I18n.t(:report_header_balance), - ] - else - [ - I18n.t(:report_header_first_name), - I18n.t(:report_header_last_name), - I18n.t(:report_header_hub), - I18n.t(:report_header_hub_code), - I18n.t(:report_header_delivery_address), - I18n.t(:report_header_delivery_postcode), - I18n.t(:report_header_phone), - I18n.t(:report_header_shipping_method), - I18n.t(:report_header_payment_method), - I18n.t(:report_header_amount), - I18n.t(:report_header_balance), - I18n.t(:report_header_temp_controlled_items), - I18n.t(:report_header_special_instructions), - ] - end - end - - def search - Spree::Order. - finalized. - not_state(:canceled). - distributed_by_user(@user). - managed_by(@user). - ransack(params[:q]) - end - - def orders - search_result = search.result.order(:completed_at) - orders_with_balance = OutstandingBalance.new(search_result). - query. - select('spree_orders.*') - - filter(orders_with_balance) - end - - def table_rows - return [] unless @render_table - - if is_payment_methods? - orders.map { |o| payment_method_row o } - else - orders.map { |o| delivery_row o } - end - end - - def filter(search_result) - filter_to_payment_method filter_to_shipping_method filter_to_order_cycle search_result - end - - private - - # This method relies on `balance_value` as a computed DB column. See `CompleteOrdersWithBalance` - # for reference. - def balance(order) - order.balance_value - end - - def payment_method_row(order) - ba = order.billing_address - [ba&.firstname, - ba&.lastname, - order.distributor&.name, - customer_code(order.email), - order.email, - ba&.phone, - order.shipping_method&.name, - order.payments.last&.payment_method&.name, - order.total, - balance(order)] - end - - def delivery_row(order) - sa = order.shipping_address - [sa.firstname, - sa.lastname, - order.distributor&.name, - customer_code(order.email), - "#{sa.address1} #{sa.address2} #{sa.city}", - sa.zipcode, - sa.phone, - order.shipping_method&.name, - order.payments.first&.payment_method&.name, - order.total, - balance(order), - has_temperature_controlled_items?(order), - order.special_instructions] - end - - def filter_to_payment_method(orders) - if params[:payment_method_in].present? - orders.joins(payments: :payment_method).where(spree_payments: { payment_method_id: params[:payment_method_in] }) - else - orders - end - end - - def filter_to_shipping_method(orders) - if params[:shipping_method_in].present? - orders.joins(shipments: :shipping_rates).where(spree_shipping_rates: { selected: true, shipping_method_id: params[:shipping_method_in] }) - else - orders - end - end - - def filter_to_order_cycle(orders) - if params[:order_cycle_id].present? - orders.where(order_cycle_id: params[:order_cycle_id]) - else - orders - end - end - - def has_temperature_controlled_items?(order) - order.line_items.any? { |line_item| - line_item.product.shipping_category&.temperature_controlled - } - end - - def is_payment_methods? - params[:report_subtype] == "payment_methods" - end - - def customer_code(email) - customer = Customer.where(email: email).first - customer.nil? ? "" : customer.code - end - - def sanitize_params(params) - params[:q] ||= {} - params[:q][:completed_at_gt] ||= Time.zone.today + DEFAULT_DATE_INTERVAL[:from] - params[:q][:completed_at_lt] ||= Time.zone.today + DEFAULT_DATE_INTERVAL[:to] - params - end - end -end diff --git a/lib/open_food_network/order_cycle_permissions.rb b/lib/open_food_network/order_cycle_permissions.rb index a6e0024ed4..8f3ede515d 100644 --- a/lib/open_food_network/order_cycle_permissions.rb +++ b/lib/open_food_network/order_cycle_permissions.rb @@ -25,7 +25,7 @@ module OpenFoodNetwork if @coordinator.sells == "any" # If the coordinator sells any, relationships come into play related_enterprises_granting(:add_to_order_cycle, - to: [@coordinator.id]).each do |enterprise_id| + to: [@coordinator.id]).each do |enterprise_id| coordinator_permitted_ids << enterprise_id end @@ -92,8 +92,8 @@ module OpenFoodNetwork variant_ids = Spree::Variant.joins(:exchanges). where( "exchanges.receiver_id IN (?) - AND exchanges.order_cycle_id = (?) - AND exchanges.incoming = 'f'", + AND exchanges.order_cycle_id = (?) + AND exchanges.incoming = 'f'", managed_participating_hubs.select("enterprises.id"), @order_cycle ).pluck(:id).uniq @@ -125,7 +125,7 @@ module OpenFoodNetwork # Find the variants that a user can POTENTIALLY see within incoming exchanges def visible_variants_for_incoming_exchanges_from(producer) if @order_cycle && - (user_manages_coordinator_or(producer) || user_is_permitted_add_to_oc_by(producer)) + (user_manages_coordinator_or(producer) || user_is_permitted_add_to_oc_by(producer)) all_variants_supplied_by(producer) else no_variants @@ -188,8 +188,8 @@ module OpenFoodNetwork # so things don't break. TODO: Remove this when all P-OC are sorted out active_variants = Spree::Variant.joins(:exchanges, :product). where("exchanges.receiver_id = (?) - AND spree_products.supplier_id IN (?) - AND incoming = 'f'", + AND spree_products.supplier_id IN (?) + AND incoming = 'f'", hub.id, managed_producer_ids) @@ -212,8 +212,8 @@ module OpenFoodNetwork # Variants produced by MY PRODUCERS that are in this OC, # where my producer has granted P-OC to the hub granting_producer_ids = related_enterprises_granting(:add_to_order_cycle, - to: [hub.id], - scope: granted_producers) + to: [hub.id], + scope: granted_producers) permitted_variants = variants_from_suppliers(granting_producer_ids) Spree::Variant.where(id: permitted_variants) @@ -299,8 +299,8 @@ module OpenFoodNetwork # any incoming exchanges supplying variants in my outgoing exchanges variant_ids = Spree::Variant.joins(:exchanges). where("exchanges.receiver_id IN (?) - AND exchanges.order_cycle_id = (?) - AND exchanges.incoming = 'f'", + AND exchanges.order_cycle_id = (?) + AND exchanges.incoming = 'f'", hubs.select("enterprises.id"), @order_cycle).pluck(:id).uniq diff --git a/lib/open_food_network/orders_and_distributors_report.rb b/lib/open_food_network/orders_and_distributors_report.rb deleted file mode 100644 index 252f33f3e0..0000000000 --- a/lib/open_food_network/orders_and_distributors_report.rb +++ /dev/null @@ -1,112 +0,0 @@ -# frozen_string_literal: true - -module OpenFoodNetwork - class OrdersAndDistributorsReport - def initialize(user, params = {}, render_table = false) - @params = params - @user = user - @render_table = render_table - - @permissions = ::Permissions::Order.new(user, @params[:q]) - end - - def table_headers - [ - I18n.t(:report_header_order_date), - I18n.t(:report_header_order_id), - I18n.t(:report_header_customer_name), - I18n.t(:report_header_customer_email), - I18n.t(:report_header_customer_phone), - I18n.t(:report_header_customer_city), - I18n.t(:report_header_sku), - I18n.t(:report_header_item_name), - I18n.t(:report_header_variant), - I18n.t(:report_header_quantity), - I18n.t(:report_header_max_quantity), - I18n.t(:report_header_cost), - I18n.t(:report_header_shipping_cost), - I18n.t(:report_header_payment_method), - I18n.t(:report_header_distributor), - I18n.t(:report_header_distributor_address), - I18n.t(:report_header_distributor_city), - I18n.t(:report_header_distributor_postcode), - I18n.t(:report_header_shipping_method), - I18n.t(:report_header_shipping_instructions) - ] - end - - def search - @permissions.visible_orders.select("DISTINCT spree_orders.*"). - complete.not_state(:canceled). - ransack(@params[:q]) - end - - def table_rows - return [] unless @render_table - - orders = search.result - - orders.select{ |order| orders_with_hidden_details(orders).include? order }.each do |order| - OrderDataMasker.new(order).call - end - - line_item_details orders - end - - private - - def orders_with_hidden_details(orders) - # If empty array is passed in, the where clause will return all line_items, which is bad - if @permissions.editable_orders.empty? - orders - else - orders. - where('spree_orders.id NOT IN (?)', - @permissions.editable_orders.select(&:id)) - end - end - - def line_item_details(orders) - order_and_distributor_details = [] - - orders.each do |order| - order.line_items.each do |line_item| - order_and_distributor_details << row_for(line_item, order) - end - end - - order_and_distributor_details - end - - # Returns a row with the data to display for the specified line_item and - # its order - # - # @param line_item [Spree::LineItem] - # @param order [Spree::Order] - # @return [Array] - def row_for(line_item, order) - [ - order.completed_at.strftime("%F %T"), - order.id, - order.bill_address.full_name, - order.email, - order.bill_address.phone, - order.bill_address.city, - line_item.product.sku, - line_item.product.name, - line_item.options_text, - line_item.quantity, - line_item.max_quantity, - line_item.price * line_item.quantity, - line_item.distribution_fee, - order.payments.first&.payment_method&.name, - order.distributor&.name, - order.distributor.address.address1, - order.distributor.address.city, - order.distributor.address.zipcode, - order.shipping_method&.name, - order.special_instructions - ] - end - end -end diff --git a/lib/open_food_network/orders_and_fulfillment_report.rb b/lib/open_food_network/orders_and_fulfillment_report.rb deleted file mode 100644 index ac62fa09d2..0000000000 --- a/lib/open_food_network/orders_and_fulfillment_report.rb +++ /dev/null @@ -1,120 +0,0 @@ -# frozen_string_literal: true - -require "open_food_network/reports/line_items" -require "open_food_network/orders_and_fulfillment_report/supplier_totals_report" -require "open_food_network/orders_and_fulfillment_report/supplier_totals_by_distributor_report" -require "open_food_network/orders_and_fulfillment_report/distributor_totals_by_supplier_report" -require "open_food_network/orders_and_fulfillment_report/customer_totals_report" -require 'open_food_network/orders_and_fulfillment_report/default_report' -require 'open_food_network/order_grouper' - -include Spree::ReportsHelper - -module OpenFoodNetwork - class OrdersAndFulfillmentReport - attr_reader :options, :report_type - - delegate :table_headers, :rules, :columns, to: :report - - def initialize(user, options = {}, render_table = false) - @user = user - @options = options - @report_type = options[:report_subtype] - @render_table = render_table - @variant_scopers_by_distributor_id = {} - end - - def search - report_line_items.orders - end - - def table_items - return [] unless @render_table - - report_line_items.list(report.line_item_includes) - end - - def table_rows - order_grouper = OpenFoodNetwork::OrderGrouper.new report.rules, report.columns, report - order_grouper.table(table_items) - end - - def line_item_name - proc { |line_item| line_item.variant.full_name } - end - - def line_items_name - proc { |line_items| line_items.first.variant.full_name } - end - - def supplier_name - proc { |line_items| line_items.first.variant.product.supplier.name } - end - - def product_name - proc { |line_items| line_items.first.variant.product.name } - end - - def total_units(line_items) - return " " if not_all_have_unit?(line_items) - - total_units = line_items.sum do |li| - product = li.variant.product - li.quantity * li.unit_value / scale_factor(product) - end - - total_units.round(3) - end - - def variant_scoper_for(distributor_id) - @variant_scopers_by_distributor_id[distributor_id] ||= - OpenFoodNetwork::ScopeVariantToHub.new( - distributor_id, - report_variant_overrides[distributor_id] || {}, - ) - end - - private - - def report - @report ||= report_klass.new(self) - end - - def report_klass - case report_type - when SupplierTotalsReport::REPORT_TYPE then SupplierTotalsReport - when SupplierTotalsByDistributorReport::REPORT_TYPE then SupplierTotalsByDistributorReport - when DistributorTotalsBySupplierReport::REPORT_TYPE then DistributorTotalsBySupplierReport - when CustomerTotalsReport::REPORT_TYPE then CustomerTotalsReport - else - DefaultReport - end - end - - def not_all_have_unit?(line_items) - line_items.map { |li| li.unit_value.nil? }.any? - end - - def scale_factor(product) - product.variant_unit == 'weight' ? 1000 : 1 - end - - def order_permissions - return @order_permissions unless @order_permissions.nil? - - @order_permissions = ::Permissions::Order.new(@user, options[:q]) - end - - def report_line_items - @report_line_items ||= Reports::LineItems.new(order_permissions, options) - end - - def report_variant_overrides - @report_variant_overrides ||= - VariantOverridesIndexed.new( - order_permissions.visible_line_items.select('DISTINCT variant_id'), - report_line_items.orders.result.select('DISTINCT distributor_id'), - ).indexed - end - end -end diff --git a/lib/open_food_network/orders_and_fulfillment_report/customer_totals_report.rb b/lib/open_food_network/orders_and_fulfillment_report/customer_totals_report.rb deleted file mode 100644 index 8ec81136a4..0000000000 --- a/lib/open_food_network/orders_and_fulfillment_report/customer_totals_report.rb +++ /dev/null @@ -1,221 +0,0 @@ -# frozen_string_literal: true - -# rubocop:disable Metrics/ClassLength -module OpenFoodNetwork - class OrdersAndFulfillmentReport - class CustomerTotalsReport - REPORT_TYPE = "order_cycle_customer_totals" - - attr_reader :context - - delegate :line_item_name, to: :context - delegate :variant_scoper_for, to: :context - - def initialize(context) - @context = context - @scopers_by_distributor_id = {} - end - - # rubocop:disable Metrics/AbcSize - def table_headers - [I18n.t(:report_header_hub), I18n.t(:report_header_customer), I18n.t(:report_header_email), - I18n.t(:report_header_phone), I18n.t(:report_header_producer), - I18n.t(:report_header_product), I18n.t(:report_header_variant), - I18n.t(:report_header_quantity), - I18n.t(:report_header_item_price, currency: currency_symbol), - I18n.t(:report_header_item_fees_price, currency: currency_symbol), - I18n.t(:report_header_admin_handling_fees, currency: currency_symbol), - I18n.t(:report_header_ship_price, currency: currency_symbol), - I18n.t(:report_header_pay_fee_price, currency: currency_symbol), - I18n.t(:report_header_total_price, currency: currency_symbol), - I18n.t(:report_header_paid), I18n.t(:report_header_shipping), - I18n.t(:report_header_delivery), I18n.t(:report_header_ship_street), - I18n.t(:report_header_ship_street_2), I18n.t(:report_header_ship_city), - I18n.t(:report_header_ship_postcode), I18n.t(:report_header_ship_state), - I18n.t(:report_header_comments), I18n.t(:report_header_sku), - I18n.t(:report_header_order_cycle), I18n.t(:report_header_payment_method), - I18n.t(:report_header_customer_code), I18n.t(:report_header_tags), - I18n.t(:report_header_billing_street), I18n.t(:report_header_billing_street_2), - I18n.t(:report_header_billing_city), I18n.t(:report_header_billing_postcode), - I18n.t(:report_header_billing_state), - I18n.t(:report_header_order_number), - I18n.t(:report_header_date)] - end - - # rubocop:enable Metrics/AbcSize - # rubocop:disable Metrics/AbcSize - # rubocop:disable Metrics/MethodLength - def rules - [ - { - group_by: proc { |line_item| line_item.order.distributor }, - sort_by: proc { |distributor| distributor.name } - }, - { - group_by: proc { |line_item| line_item.order }, - sort_by: proc { |order| order.bill_address.full_name_reverse }, - summary_columns: [ - proc { |line_items| line_items.first.order.distributor.name }, - proc { |line_items| line_items.first.order.bill_address.full_name }, - proc { |_line_items| "" }, - proc { |_line_items| "" }, - proc { |_line_items| "" }, - proc { |_line_items| I18n.t('admin.reports.total') }, - proc { |_line_items| "" }, - - proc { |_line_items| "" }, - proc { |line_items| line_items.sum(&:amount) }, - proc { |line_items| line_items.sum(&:amount_with_adjustments) }, - proc { |line_items| line_items.first.order.admin_and_handling_total }, - proc { |line_items| line_items.first.order.ship_total }, - proc { |line_items| line_items.first.order.payment_fee }, - proc { |line_items| line_items.first.order.total }, - proc { |line_items| line_items.first.order.paid? ? I18n.t(:yes) : I18n.t(:no) }, - - proc { |_line_items| "" }, - proc { |_line_items| "" }, - - proc { |_line_items| "" }, - proc { |_line_items| "" }, - proc { |_line_items| "" }, - proc { |_line_items| "" }, - proc { |_line_items| "" }, - - proc { |line_items| line_items.first.order.special_instructions }, - proc { |_line_items| "" }, - - proc { |line_items| line_items.first.order.order_cycle&.name }, - proc { |line_items| - line_items.first.order.payments.first&.payment_method&.name - }, - proc { |_line_items| "" }, - proc { |_line_items| "" }, - proc { |_line_items| "" }, - proc { |_line_items| "" }, - proc { |_line_items| "" }, - proc { |_line_items| "" }, - proc { |_line_items| "" }, - proc { |line_items| line_items.first.order.number }, - proc { |line_items| line_items.first.order.completed_at.strftime("%F %T") }, - ] - }, - { - group_by: proc { |line_item| line_item.variant.product }, - sort_by: proc { |product| product.name } - }, - { - group_by: proc { |line_item| line_item.variant }, - sort_by: proc { |variant| variant.full_name } - }, - { - group_by: line_item_name, - sort_by: proc { |full_name| full_name } - } - ] - end - # rubocop:enable Metrics/AbcSize - # rubocop:enable Metrics/MethodLength - - # rubocop:disable Metrics/AbcSize - # rubocop:disable Metrics/MethodLength - # rubocop:disable Metrics/PerceivedComplexity - def columns - rsa = proc { |line_items| shipping_method(line_items)&.delivery? } - [ - proc { |line_items| line_items.first.order.distributor.name }, - proc { |line_items| - bill_address = line_items.first.order.bill_address - bill_address.firstname + " " + bill_address.lastname - }, - proc { |line_items| line_items.first.order.email }, - proc { |line_items| line_items.first.order.bill_address.phone }, - proc { |line_items| line_items.first.variant.product.supplier.name }, - proc { |line_items| line_items.first.variant.product.name }, - proc { |line_items| line_items.first.variant.full_name }, - - proc { |line_items| line_items.to_a.sum(&:quantity) }, - proc { |line_items| line_items.sum(&:amount) }, - proc { |line_items| line_items.sum(&:amount_with_adjustments) }, - proc { |_line_items| "" }, - proc { |_line_items| "" }, - proc { |_line_items| "" }, - proc { |_line_items| "" }, - proc { |line_items| - line_items.all? { |li| li.order.paid? } ? I18n.t(:yes) : I18n.t(:no) - }, - - proc { |line_items| shipping_method(line_items)&.name }, - proc { |line_items| rsa.call(line_items) ? I18n.t(:yes) : I18n.t(:no) }, - - proc { |line_items| - line_items.first.order.ship_address&.address1 if rsa.call(line_items) - }, - proc { |line_items| - line_items.first.order.ship_address&.address2 if rsa.call(line_items) - }, - proc { |line_items| - line_items.first.order.ship_address&.city if rsa.call(line_items) - }, - proc { |line_items| - line_items.first.order.ship_address&.zipcode if rsa.call(line_items) - }, - proc { |line_items| - line_items.first.order.ship_address&.state if rsa.call(line_items) - }, - - proc { |_line_items| "" }, - proc do |line_items| - line_item = line_items.first - variant_scoper_for(line_item.order.distributor_id).scope(line_item.variant) - line_item.variant.sku - end, - - proc { |line_items| line_items.first.order.order_cycle&.name }, - proc { |line_items| - payment = line_items.first.order.payments.first - payment&.payment_method&.name - }, - proc { |line_items| - distributor = line_items.first.order.distributor - user = line_items.first.order.user - user&.customer_of(distributor)&.code - }, - proc { |line_items| - distributor = line_items.first.order.distributor - user = line_items.first.order.user - user&.customer_of(distributor)&.tags&.join(', ') - }, - - proc { |line_items| line_items.first.order.bill_address&.address1 }, - proc { |line_items| line_items.first.order.bill_address&.address2 }, - proc { |line_items| line_items.first.order.bill_address&.city }, - proc { |line_items| line_items.first.order.bill_address&.zipcode }, - proc { |line_items| line_items.first.order.bill_address&.state }, - proc { |line_items| line_items.first.order.number }, - proc { |line_items| line_items.first.order.completed_at.strftime("%F %T") }, - ] - end - # rubocop:enable Metrics/AbcSize - # rubocop:enable Metrics/MethodLength - # rubocop:enable Metrics/PerceivedComplexity - - def line_item_includes - [{ variant: [{ option_values: :option_type }, { product: :supplier }], - order: [:bill_address, :ship_address, :order_cycle, :adjustments, :payments, - :user, :distributor, :shipments] }] - end - - private - - def shipping_method(line_items) - shipping_rates = line_items.first.order.shipments.first&.shipping_rates - - return unless shipping_rates - - shipping_rate = shipping_rates.find(&:selected) || shipping_rates.first - shipping_rate.try(:shipping_method) - end - end - end -end -# rubocop:enable Metrics/ClassLength diff --git a/lib/open_food_network/orders_and_fulfillment_report/default_report.rb b/lib/open_food_network/orders_and_fulfillment_report/default_report.rb deleted file mode 100644 index 2ec3c20a4e..0000000000 --- a/lib/open_food_network/orders_and_fulfillment_report/default_report.rb +++ /dev/null @@ -1,64 +0,0 @@ -# frozen_string_literal: true - -module OpenFoodNetwork - class OrdersAndFulfillmentReport - class DefaultReport - delegate :line_item_name, :supplier_name, :product_name, :line_items_name, to: :context - - def initialize(context) - @context = context - end - - def table_headers - [ - I18n.t(:report_header_producer), - I18n.t(:report_header_product), - I18n.t(:report_header_variant), - I18n.t(:report_header_quantity), - I18n.t(:report_header_curr_cost_per_unit), - I18n.t(:report_header_total_cost), - I18n.t(:report_header_status), - I18n.t(:report_header_incoming_transport) - ] - end - - def rules - [ - { - group_by: proc { |line_item| line_item.variant.product.supplier }, - sort_by: proc { |supplier| supplier.name } - }, - { - group_by: proc { |line_item| line_item.variant.product }, - sort_by: proc { |product| product.name } - }, - { - group_by: line_item_name, - sort_by: proc { |full_name| full_name } - } - ] - end - - def columns - [ - supplier_name, - product_name, - line_items_name, - proc { |line_items| line_items.to_a.sum(&:quantity) }, - proc { |line_items| line_items.first.price }, - proc { |line_items| line_items.sum { |li| li.quantity * li.price } }, - proc { |_line_items| "" }, - proc { |_line_items| I18n.t(:report_header_incoming_transport) } - ] - end - - def line_item_includes - [] - end - - private - - attr_reader :context - end - end -end diff --git a/lib/open_food_network/orders_and_fulfillment_report/distributor_totals_by_supplier_report.rb b/lib/open_food_network/orders_and_fulfillment_report/distributor_totals_by_supplier_report.rb deleted file mode 100644 index 472aadf38d..0000000000 --- a/lib/open_food_network/orders_and_fulfillment_report/distributor_totals_by_supplier_report.rb +++ /dev/null @@ -1,78 +0,0 @@ -# frozen_string_literal: true - -module OpenFoodNetwork - class OrdersAndFulfillmentReport - class DistributorTotalsBySupplierReport - REPORT_TYPE = "order_cycle_distributor_totals_by_supplier" - - attr_reader :context - - def initialize(context) - @context = context - end - - def table_headers - [I18n.t(:report_header_hub), I18n.t(:report_header_producer), - I18n.t(:report_header_product), I18n.t(:report_header_variant), - I18n.t(:report_header_quantity), I18n.t(:report_header_curr_cost_per_unit), - I18n.t(:report_header_total_cost), I18n.t(:report_header_total_shipping_cost), - I18n.t(:report_header_shipping_method)] - end - - # rubocop:disable Metrics/AbcSize - # rubocop:disable Metrics/MethodLength - def rules - [ - { - group_by: proc { |line_item| line_item.order.distributor }, - sort_by: proc { |distributor| distributor.name }, - summary_columns: [ - proc { |_line_items| "" }, - proc { |_line_items| I18n.t('admin.reports.total') }, - proc { |_line_items| "" }, - proc { |_line_items| "" }, - proc { |_line_items| "" }, - proc { |_line_items| "" }, - proc { |line_items| line_items.sum(&:amount) }, - proc { |line_items| line_items.map(&:order).uniq.sum(&:ship_total) }, - proc { |_line_items| "" } - ] - }, - { - group_by: proc { |line_item| line_item.variant.product.supplier }, - sort_by: proc { |supplier| supplier.name } - }, - { - group_by: proc { |line_item| line_item.variant.product }, - sort_by: proc { |product| product.name } - }, - { - group_by: proc { |line_item| line_item.variant.full_name }, - sort_by: proc { |full_name| full_name } - } - ] - end - # rubocop:enable Metrics/AbcSize - # rubocop:enable Metrics/MethodLength - - # rubocop:disable Metrics/AbcSize - def columns - [proc { |line_items| line_items.first.order.distributor.name }, - proc { |line_items| line_items.first.variant.product.supplier.name }, - proc { |line_items| line_items.first.variant.product.name }, - proc { |line_items| line_items.first.variant.full_name }, - proc { |line_items| line_items.to_a.sum(&:quantity) }, - proc { |line_items| line_items.first.price }, - proc { |line_items| line_items.sum(&:amount) }, - proc { |_line_items| "" }, - proc { |_line_items| I18n.t(:report_header_shipping_method) }] - end - # rubocop:enable Metrics/AbcSize - - def line_item_includes - [{ order: [:distributor, :adjustments, { shipments: { shipping_rates: :shipping_method } }], - variant: [{ option_values: :option_type }, { product: :supplier }] }] - end - end - end -end diff --git a/lib/open_food_network/orders_and_fulfillment_report/supplier_totals_by_distributor_report.rb b/lib/open_food_network/orders_and_fulfillment_report/supplier_totals_by_distributor_report.rb deleted file mode 100644 index 8731c58c70..0000000000 --- a/lib/open_food_network/orders_and_fulfillment_report/supplier_totals_by_distributor_report.rb +++ /dev/null @@ -1,77 +0,0 @@ -# frozen_string_literal: true - -module OpenFoodNetwork - class OrdersAndFulfillmentReport - class SupplierTotalsByDistributorReport - REPORT_TYPE = "order_cycle_supplier_totals_by_distributor" - - attr_reader :context - - delegate :supplier_name, to: :context - - def initialize(context) - @context = context - end - - def table_headers - [I18n.t(:report_header_producer), I18n.t(:report_header_product), - I18n.t(:report_header_variant), I18n.t(:report_header_to_hub), - I18n.t(:report_header_quantity), I18n.t(:report_header_curr_cost_per_unit), - I18n.t(:report_header_total_cost), I18n.t(:report_header_shipping_method)] - end - - # rubocop:disable Metrics/AbcSize - # rubocop:disable Metrics/MethodLength - def rules - [ - { - group_by: proc { |line_item| line_item.variant.product.supplier }, - sort_by: proc { |supplier| supplier.name } - }, - { - group_by: proc { |line_item| line_item.variant.product }, - sort_by: proc { |product| product.name } - }, - { - group_by: proc { |line_item| line_item.variant.full_name }, - sort_by: proc { |full_name| full_name }, - summary_columns: [ - proc { |_line_items| "" }, - proc { |_line_items| "" }, - proc { |_line_items| "" }, - proc { |_line_items| I18n.t('admin.reports.total') }, - proc { |_line_items| "" }, - proc { |_line_items| "" }, - proc { |line_items| line_items.sum(&:amount) }, - proc { |_line_items| "" } - ] - }, - { - group_by: proc { |line_item| line_item.order.distributor }, - sort_by: proc { |distributor| distributor.name } - } - ] - end - # rubocop:enable Metrics/AbcSize - # rubocop:enable Metrics/MethodLength - - def columns - [ - supplier_name, - proc { |line_items| line_items.first.variant.product.name }, - proc { |line_items| line_items.first.variant.full_name }, - proc { |line_items| line_items.first.order.distributor.name }, - proc { |line_items| line_items.to_a.sum(&:quantity) }, - proc { |line_items| line_items.first.price }, - proc { |line_items| line_items.sum(&:amount) }, - proc { |_line_items| I18n.t(:report_header_shipping_method) } - ] - end - - def line_item_includes - [{ order: :distributor, - variant: [{ option_values: :option_type }, { product: :supplier }] }] - end - end - end -end diff --git a/lib/open_food_network/orders_and_fulfillment_report/supplier_totals_report.rb b/lib/open_food_network/orders_and_fulfillment_report/supplier_totals_report.rb deleted file mode 100644 index 8ee9decf89..0000000000 --- a/lib/open_food_network/orders_and_fulfillment_report/supplier_totals_report.rb +++ /dev/null @@ -1,60 +0,0 @@ -# frozen_string_literal: true - -module OpenFoodNetwork - class OrdersAndFulfillmentReport - class SupplierTotalsReport - REPORT_TYPE = "order_cycle_supplier_totals" - - attr_reader :context - - delegate :supplier_name, :product_name, :line_items_name, :total_units, to: :context - - def initialize(context) - @context = context - end - - def table_headers - [I18n.t(:report_header_producer), I18n.t(:report_header_product), - I18n.t(:report_header_variant), I18n.t(:report_header_quantity), - I18n.t(:report_header_total_units), I18n.t(:report_header_curr_cost_per_unit), - I18n.t(:report_header_total_cost), I18n.t(:report_header_status), - I18n.t(:report_header_incoming_transport)] - end - - def rules - [ - { - group_by: proc { |line_item| line_item.variant.product.supplier }, - sort_by: proc { |supplier| supplier.name } - }, - { - group_by: proc { |line_item| line_item.variant.product }, - sort_by: proc { |product| product.name } - }, - { - group_by: proc { |line_item| line_item.variant.full_name }, - sort_by: proc { |full_name| full_name } - } - ] - end - - def columns - [ - supplier_name, - product_name, - line_items_name, - proc { |line_items| line_items.to_a.sum(&:quantity) }, - proc { |line_items| total_units(line_items) }, - proc { |line_items| line_items.first.price }, - proc { |line_items| line_items.sum(&:amount) }, - proc { |_line_items| "" }, - proc { |_line_items| I18n.t(:report_header_incoming_transport) } - ] - end - - def line_item_includes - [{ variant: [{ option_values: :option_type }, { product: :supplier }] }] - end - end - end -end diff --git a/lib/open_food_network/payments_report.rb b/lib/open_food_network/payments_report.rb deleted file mode 100644 index 8dbddcc270..0000000000 --- a/lib/open_food_network/payments_report.rb +++ /dev/null @@ -1,144 +0,0 @@ -# frozen_string_literal: true - -require 'open_food_network/order_grouper' - -module OpenFoodNetwork - class PaymentsReport - attr_reader :params - - def initialize(user, params = {}, render_table = false) - @params = params - @user = user - @render_table = render_table - end - - def table_headers - case params[:report_subtype] - when "payments_by_payment_type" - I18n.t(:report_header_payment_type) - [I18n.t(:report_header_payment_state), I18n.t(:report_header_distributor), I18n.t(:report_header_payment_type), - I18n.t(:report_header_total_price, currency: currency_symbol)] - when "itemised_payment_totals" - [I18n.t(:report_header_payment_state), I18n.t(:report_header_distributor), - I18n.t(:report_header_product_total_price, currency: currency_symbol), - I18n.t(:report_header_shipping_total_price, currency: currency_symbol), - I18n.t(:report_header_outstanding_balance_price, currency: currency_symbol), - I18n.t(:report_header_total_price, currency: currency_symbol)] - when "payment_totals" - [I18n.t(:report_header_payment_state), I18n.t(:report_header_distributor), - I18n.t(:report_header_product_total_price, currency: currency_symbol), - I18n.t(:report_header_shipping_total_price, currency: currency_symbol), - I18n.t(:report_header_total_price, currency: currency_symbol), - I18n.t(:report_header_eft_price, currency: currency_symbol), - I18n.t(:report_header_paypal_price, currency: currency_symbol), - I18n.t(:report_header_outstanding_balance_price, currency: currency_symbol)] - else - [I18n.t(:report_header_payment_state), I18n.t(:report_header_distributor), I18n.t(:report_header_payment_type), - I18n.t(:report_header_total_price, currency: currency_symbol)] - end - end - - def search - Spree::Order.complete.not_state(:canceled).managed_by(@user).ransack(params[:q]) - end - - def table_items - return [] unless @render_table - - orders = search.result - payments = orders.includes(:payments).map do |order| - order.payments.select(&:completed?) - end.flatten - - case params[:report_subtype] - when "payments_by_payment_type" - payments - when "itemised_payment_totals" - orders - when "payment_totals" - orders - else - payments - end - end - - def table_rows - order_grouper = OpenFoodNetwork::OrderGrouper.new rules, columns, self - order_grouper.table(table_items) - end - - def rules - case params[:report_subtype] - when "payments_by_payment_type" - [{ group_by: proc { |payment| payment.order.payment_state }, - sort_by: proc { |payment_state| payment_state } }, - { group_by: proc { |payment| payment.order.distributor }, - sort_by: proc { |distributor| distributor.name } }, - { group_by: proc { |payment| Spree::PaymentMethod.unscoped { payment.payment_method } }, - sort_by: proc { |method| method.name } }] - when "itemised_payment_totals" - [{ group_by: proc { |order| order.payment_state }, - sort_by: proc { |payment_state| payment_state } }, - { group_by: proc { |order| order.distributor }, - sort_by: proc { |distributor| distributor.name } }] - when "payment_totals" - [{ group_by: proc { |order| order.payment_state }, - sort_by: proc { |payment_state| payment_state } }, - { group_by: proc { |order| order.distributor }, - sort_by: proc { |distributor| distributor.name } }] - else - [{ group_by: proc { |payment| payment.order.payment_state }, - sort_by: proc { |payment_state| payment_state } }, - { group_by: proc { |payment| payment.order.distributor }, - sort_by: proc { |distributor| distributor.name } }, - { group_by: proc { |payment| payment.payment_method }, - sort_by: proc { |method| method.name } }] - end - end - - def columns - case params[:report_subtype] - when "payments_by_payment_type" - [proc { |payments| payments.first.order.payment_state }, - proc { |payments| payments.first.order.distributor.name }, - proc { |payments| payments.first.payment_method.name }, - proc { |payments| payments.sum(&:amount) }] - when "itemised_payment_totals" - [proc { |orders| orders.first.payment_state }, - proc { |orders| orders.first.distributor.name }, - proc { |orders| orders.to_a.sum(&:item_total) }, - proc { |orders| orders.sum(&:ship_total) }, - proc { |orders| orders.sum{ |order| order.outstanding_balance.to_f } }, - proc { |orders| orders.map(&:total).sum }] - when "payment_totals" - [proc { |orders| orders.first.payment_state }, - proc { |orders| orders.first.distributor.name }, - proc { |orders| orders.to_a.sum(&:item_total) }, - proc { |orders| orders.sum(&:ship_total) }, - proc { |orders| orders.map(&:total).sum }, - proc { |orders| - orders.sum { |o| - o.payments.select { |payment| - payment.completed? && - (payment.payment_method.name.to_s.include? "EFT") - }.sum(&:amount) - } - }, - proc { |orders| - orders.sum { |o| - o.payments.select { |payment| - payment.completed? && - (payment.payment_method.name.to_s.include? "PayPal") - }.sum(&:amount) - } - }, - proc { |orders| orders.sum{ |order| order.outstanding_balance.to_f } }] - else - [proc { |payments| payments.first.order.payment_state }, - proc { |payments| payments.first.order.distributor.name }, - proc { |payments| payments.first.payment_method.name }, - proc { |payments| payments.sum(&:amount) }] - end - end - end -end diff --git a/lib/open_food_network/products_and_inventory_default_report.rb b/lib/open_food_network/products_and_inventory_default_report.rb deleted file mode 100644 index 2400586c7b..0000000000 --- a/lib/open_food_network/products_and_inventory_default_report.rb +++ /dev/null @@ -1,53 +0,0 @@ -# frozen_string_literal: true - -module OpenFoodNetwork - class ProductsAndInventoryDefaultReport - attr_reader :context - - delegate :variants, :render_table, to: :context - - def initialize(context) - @context = context - end - - def table_headers - [ - I18n.t(:report_header_supplier), - I18n.t(:report_header_producer_suburb), - I18n.t(:report_header_product), - I18n.t(:report_header_product_properties), - I18n.t(:report_header_taxons), - I18n.t(:report_header_variant_value), - I18n.t(:report_header_price), - I18n.t(:report_header_group_buy_unit_quantity), - I18n.t(:report_header_amount), - I18n.t(:report_header_sku) - ] - end - - def table_rows - return [] unless render_table - - variants.map do |variant| - [ - variant.product.supplier.name, - variant.product.supplier.address.city, - variant.product.name, - variant.product.properties.map(&:name).join(", "), - variant.product.taxons.map(&:name).join(", "), - variant.full_name, - variant.price, - variant.product.group_buy_unit_size, - "", - sku_for(variant) - ] - end - end - - def sku_for(variant) - return variant.sku if variant.sku.present? - - variant.product.sku - end - end -end diff --git a/lib/open_food_network/products_and_inventory_report.rb b/lib/open_food_network/products_and_inventory_report.rb deleted file mode 100644 index ae211ec4d0..0000000000 --- a/lib/open_food_network/products_and_inventory_report.rb +++ /dev/null @@ -1,100 +0,0 @@ -# frozen_string_literal: true - -require 'open_food_network/scope_variant_to_hub' -require 'open_food_network/products_and_inventory_default_report' -require 'open_food_network/lettuce_share_report' - -module OpenFoodNetwork - class ProductsAndInventoryReport - attr_reader :params, :render_table - - delegate :table_rows, :table_headers, :rules, :columns, :sku_for, to: :report - - def initialize(user, params = {}, render_table = false) - @user = user - @params = params - @render_table = render_table - end - - def report - @report ||= report_klass.new(self) - end - - def report_type - params[:report_subtype] - end - - def report_klass - if report_type == 'lettuce_share' - OpenFoodNetwork::LettuceShareReport - else - OpenFoodNetwork::ProductsAndInventoryDefaultReport - end - end - - def permissions - @permissions ||= OpenFoodNetwork::Permissions.new(@user) - end - - def visible_products - @visible_products ||= permissions.visible_products - end - - def variants - filter(child_variants) - end - - def child_variants - Spree::Variant. - where(is_master: false). - includes(option_values: :option_type). - joins(:product). - merge(visible_products). - order('spree_products.name') - end - - def filter(variants) - filter_on_hand filter_to_distributor filter_to_order_cycle filter_to_supplier variants - end - - # Using the `in_stock?` method allows overrides by distributors. - def filter_on_hand(variants) - if report_type == 'inventory' - variants.select(&:in_stock?) - else - variants - end - end - - def filter_to_supplier(variants) - if params[:supplier_id].to_i > 0 - variants.where("spree_products.supplier_id = ?", params[:supplier_id]) - else - variants - end - end - - def filter_to_distributor(variants) - if params[:distributor_id].to_i > 0 - distributor = Enterprise.find params[:distributor_id] - scoper = OpenFoodNetwork::ScopeVariantToHub.new(distributor) - variants.in_distributor(distributor).each { |v| scoper.scope(v) } - else - variants - end - end - - def filter_to_order_cycle(variants) - if params[:order_cycle_id].to_i > 0 - order_cycle = OrderCycle.find params[:order_cycle_id] - variant_ids = Exchange.in_order_cycle(order_cycle). - joins("INNER JOIN exchange_variants ON exchanges.id = exchange_variants.exchange_id"). - select("DISTINCT exchange_variants.variant_id") - - variants.where("spree_variants.id IN (#{variant_ids.to_sql})") - else - variants - end - end - end -end diff --git a/lib/open_food_network/reports/line_items.rb b/lib/open_food_network/reports/line_items.rb deleted file mode 100644 index c8622860df..0000000000 --- a/lib/open_food_network/reports/line_items.rb +++ /dev/null @@ -1,59 +0,0 @@ -# frozen_string_literal: true - -module OpenFoodNetwork - module Reports - # shared code to search and list line items - class LineItems - def initialize(order_permissions, params, orders_relation = nil) - @order_permissions = order_permissions - @params = params - complete_not_canceled_visible_orders = CompleteVisibleOrders.new(order_permissions).query.not_state(:canceled) - @orders_relation = orders_relation || complete_not_canceled_visible_orders - end - - def orders - @orders ||= search_orders - end - - def list(line_item_includes = nil) - line_items = order_permissions.visible_line_items.in_orders(orders.result) - - if @params[:supplier_id_in].present? - line_items = line_items.supplied_by_any(@params[:supplier_id_in]) - end - - if line_item_includes.present? - line_items = line_items.includes(*line_item_includes).references(:line_items) - end - - without_editable_line_items = line_items - editable_line_items(line_items) - - without_editable_line_items.each do |line_item| - OrderDataMasker.new(line_item.order).call - end - - line_items - end - - private - - attr_reader :orders_relation, :order_permissions - - def search_orders - orders_relation.ransack(@params[:q]) - end - - # From the line_items given, returns the ones that are editable by the user - def editable_line_items(line_items) - editable_line_items_ids = order_permissions.editable_line_items.select(:id) - - # Although merge could take a relation, here we convert line_items to array - # because, if we pass a relation, merge will overwrite the conditions on the same field - # In this case: the IN clause on spree_line_items.order_id from line_items - # overwrites the IN clause on spree_line_items.order_id on editable_line_items_ids - # We convert to array the relation with less elements: line_items - editable_line_items_ids.merge(line_items.to_a) - end - end - end -end diff --git a/lib/open_food_network/sales_tax_report.rb b/lib/open_food_network/sales_tax_report.rb deleted file mode 100644 index eed03325b5..0000000000 --- a/lib/open_food_network/sales_tax_report.rb +++ /dev/null @@ -1,108 +0,0 @@ -# frozen_string_literal: true - -module OpenFoodNetwork - class SalesTaxReport - include Spree::ReportsHelper - attr_accessor :user, :params - - def initialize(user, params, render_table) - @user = user - @params = params - @render_table = render_table - end - - def table_headers - case params[:report_subtype] - when "tax_rates" - [I18n.t(:report_header_order_number), - I18n.t(:report_header_total_excl_vat, currency_symbol: currency_symbol)] + - relevant_rates.map { |rate| "%.1f%% (%s)" % [rate.amount.to_f * 100, currency_symbol] } + - [I18n.t(:report_header_total_tax, currency_symbol: currency_symbol), - I18n.t(:report_header_total_incl_vat, currency_symbol: currency_symbol)] - else - [I18n.t(:report_header_order_number), - I18n.t(:report_header_date), - I18n.t(:report_header_items), - I18n.t(:report_header_items_total, currency_symbol: currency_symbol), - I18n.t(:report_header_taxable_items_total, currency_symbol: currency_symbol), - I18n.t(:report_header_sales_tax, currency_symbol: currency_symbol), - I18n.t(:report_header_delivery_charge, currency_symbol: currency_symbol), - I18n.t(:report_header_tax_on_delivery, currency_symbol: currency_symbol), - I18n.t(:report_header_tax_on_fees, currency_symbol: currency_symbol), - I18n.t(:report_header_total_tax, currency_symbol: currency_symbol), - I18n.t(:report_header_customer), - I18n.t(:report_header_distributor)] - end - end - - def search - permissions = ::Permissions::Order.new(user) - permissions.editable_orders.complete.not_state(:canceled).ransack(params[:q]) - end - - def orders - search.result - end - - def table_rows - return [] unless @render_table - - case params[:report_subtype] - when "tax_rates" - orders.map do |order| - [order.number, order.total - order.total_tax] + - relevant_rates.map { |rate| - OrderTaxAdjustmentsFetcher.new(order).totals.fetch(rate, 0) - } + [order.total_tax, order.total] - end - else - orders.map do |order| - totals = totals_of order.line_items - shipping_cost = shipping_cost_for order - - [order.number, order.completed_at.strftime("%F %T"), totals[:items], totals[:items_total], - totals[:taxable_total], totals[:sales_tax], shipping_cost, order.shipping_tax, order.enterprise_fee_tax, order.total_tax, - order.bill_address.full_name, order.distributor&.name] - end - end - end - - private - - def relevant_rates - return @relevant_rates unless @relevant_rates.nil? - - @relevant_rates = Spree::TaxRate.distinct - end - - def totals_of(line_items) - totals = { items: 0, items_total: 0.0, taxable_total: 0.0, sales_tax: 0.0 } - - line_items.each do |line_item| - totals[:items] += line_item.quantity - totals[:items_total] += line_item.amount - - sales_tax = tax_included_in line_item - - if sales_tax > 0 - totals[:taxable_total] += line_item.amount - totals[:sales_tax] += sales_tax - end - end - - totals.each_pair do |k, _v| - totals[k] = totals[k].round(2) - end - - totals - end - - def shipping_cost_for(order) - order.shipments.first&.cost || 0.0 - end - - def tax_included_in(line_item) - line_item.adjustments.tax.inclusive.sum(:amount) - end - end -end diff --git a/lib/open_food_network/users_and_enterprises_report.rb b/lib/open_food_network/users_and_enterprises_report.rb deleted file mode 100644 index e3a3e2a619..0000000000 --- a/lib/open_food_network/users_and_enterprises_report.rb +++ /dev/null @@ -1,138 +0,0 @@ -# frozen_string_literal: true - -module OpenFoodNetwork - class UsersAndEnterprisesReport - attr_reader :params - - def initialize(user, params = {}, compile_table = false) - @user = user - @params = params - @compile_table = compile_table - - # Convert arrays of ids to comma delimited strings - if @params[:enterprise_id_in].is_a? Array - @params[:enterprise_id_in] = @params[:enterprise_id_in].join(',') - end - @params[:user_id_in] = @params[:user_id_in].join(',') if @params[:user_id_in].is_a? Array - end - - def table_headers - [ - I18n.t(:report_header_user), - I18n.t(:report_header_relationship), - I18n.t(:report_header_enterprise), - I18n.t(:report_header_is_producer), - I18n.t(:report_header_sells), - I18n.t(:report_header_visible), - I18n.t(:report_header_confirmation_date), - ] - end - - def table_rows - return [] unless @compile_table - - users_and_enterprises.map do |uae| - [ - uae["user_email"], - uae["relationship_type"], - uae["name"], - to_bool(uae["is_primary_producer"]), - uae["sells"], - uae["visible"], - to_local_datetime(uae["created_at"]) - ] - end - end - - def owners_and_enterprises - query = Enterprise.joins("LEFT JOIN spree_users AS owner ON enterprises.owner_id = owner.id") - .where("enterprises.id IS NOT NULL") - - query = filter_by_int_list_if_present(query, "enterprises.id", params[:enterprise_id_in]) - query = filter_by_int_list_if_present(query, "owner.id", params[:user_id_in]) - - query_helper(query, :owner, :owns) - end - - def managers_and_enterprises - query = Enterprise - .joins("LEFT JOIN enterprise_roles ON enterprises.id = enterprise_roles.enterprise_id") - .joins("LEFT JOIN spree_users AS managers ON enterprise_roles.user_id = managers.id") - .where("enterprise_id IS NOT NULL") - .where("user_id IS NOT NULL") - - query = filter_by_int_list_if_present(query, "enterprise_id", params[:enterprise_id_in]) - query = filter_by_int_list_if_present(query, "user_id", params[:user_id_in]) - - query_helper(query, :managers, :manages) - end - - def query_helper(query, email_user, relationship_type) - query.order("enterprises.created_at DESC") - .select(["enterprises.name", - "enterprises.sells", - "enterprises.visible", - "enterprises.is_primary_producer", - "enterprises.created_at", - "#{email_user}.email AS user_email"]) - .to_a - .map { |x| - { - name: x.name, - sells: x.sells, - visible: (x.visible ? 't' : 'f'), - is_primary_producer: (x.is_primary_producer ? 't' : 'f'), - created_at: x.created_at.utc.iso8601, - relationship_type: relationship_type, - user_email: x.user_email - }.stringify_keys - } - end - - def users_and_enterprises - sort( owners_and_enterprises.concat(managers_and_enterprises) ) - end - - def filter_by_int_list_if_present(query, filtered_field_name, int_list) - if int_list.present? - query = query.where("#{filtered_field_name} IN (?)", split_int_list(int_list)) - end - query - end - - def split_int_list(int_list) - int_list.split(',').map(&:to_i) - end - - def sort(results) - results.sort do |a, b| - if a["created_at"].nil? || b["created_at"].nil? - [(a["created_at"].nil? ? 0 : 1), a["name"], b["relationship_type"], a["user_email"]] <=> - [(b["created_at"].nil? ? 0 : 1), b["name"], a["relationship_type"], b["user_email"]] - else - [ - DateTime.parse(b["created_at"]).in_time_zone, - a["name"], - b["relationship_type"], - a["user_email"] - ] <=> [ - DateTime.parse(a["created_at"]).in_time_zone, - b["name"], - a["relationship_type"], - b["user_email"] - ] - end - end - end - - def to_bool(value) - ActiveRecord::Type::Boolean.new.cast(value) - end - - def to_local_datetime(date) - return "" if date.nil? - - date.to_datetime.in_time_zone.strftime "%Y-%m-%d %H:%M" - end - end -end diff --git a/lib/open_food_network/xero_invoices_report.rb b/lib/open_food_network/xero_invoices_report.rb deleted file mode 100644 index da36a04860..0000000000 --- a/lib/open_food_network/xero_invoices_report.rb +++ /dev/null @@ -1,235 +0,0 @@ -# frozen_string_literal: true - -module OpenFoodNetwork - class XeroInvoicesReport - def initialize(user, opts = {}, compile_table = false) - @user = user - - @opts = opts. - symbolize_keys. - reject { |_k, v| v.blank? }. - reverse_merge( report_subtype: 'summary', - invoice_date: Time.zone.today, - due_date: Time.zone.today + 1.month, - account_code: 'food sales' ) - @compile_table = compile_table - end - - def table_headers - # NOTE: These are NOT to be translated, they need to be in this exact format to work with Xero - %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 search - permissions = ::Permissions::Order.new(@user) - permissions.editable_orders.complete.not_state(:canceled).ransack(@opts[:q]) - end - - def orders - search.result.reorder('id DESC') - end - - def table_rows - return [] unless @compile_table - - rows = [] - - orders.each_with_index do |order, i| - invoice_number = invoice_number_for(order, i) - rows += detail_rows_for_order(order, invoice_number, @opts) if detail? - rows += summary_rows_for_order(order, invoice_number, @opts) - end - - rows.compact - end - - private - - def report_options - @opts.merge(line_item_includes: line_item_includes) - end - - def line_item_includes - [:bill_address, :adjustments, - { line_items: { variant: [{ option_values: :option_type }, { product: :supplier }] } }] - end - - def detail_rows_for_order(order, invoice_number, opts) - rows = [] - - rows += line_item_detail_rows(order, invoice_number, opts) - rows += adjustment_detail_rows(order, invoice_number, opts) - - rows - end - - def line_item_detail_rows(order, invoice_number, opts) - order.line_items.map do |line_item| - line_item_detail_row(line_item, invoice_number, opts) - end - end - - def line_item_detail_row(line_item, invoice_number, opts) - row(line_item.order, - line_item.variant.sku, - line_item.product_and_full_name, - line_item.quantity.to_s, - line_item.price.to_s, - invoice_number, - tax_type(line_item), - opts) - end - - def adjustment_detail_rows(order, invoice_number, opts) - admin_adjustments(order).map do |adjustment| - adjustment_detail_row(adjustment, invoice_number, opts) - end - end - - def adjustment_detail_row(adjustment, invoice_number, opts) - row(adjustment_order(adjustment), - '', - adjustment.label, - 1, - adjustment.amount, - invoice_number, - tax_type(adjustment), - opts) - end - - def summary_rows_for_order(order, invoice_number, opts) - rows = [] - - rows += produce_summary_rows(order, invoice_number, opts) unless detail? - rows += fee_summary_rows(order, invoice_number, opts) - rows += shipping_summary_rows(order, invoice_number, opts) - rows += payment_summary_rows(order, invoice_number, opts) - rows += admin_adjustment_summary_rows(order, invoice_number, opts) unless detail? - - rows - end - - def produce_summary_rows(order, invoice_number, opts) - [summary_row(order, I18n.t(:report_header_total_untaxable_produce), total_untaxable_products(order), invoice_number, I18n.t(:report_header_gst_free_income), opts), - summary_row(order, I18n.t(:report_header_total_taxable_produce), - total_taxable_products(order), invoice_number, I18n.t(:report_header_gst_on_income), opts)] - end - - def fee_summary_rows(order, invoice_number, opts) - [summary_row(order, I18n.t(:report_header_total_untaxable_fees), total_untaxable_fees(order), invoice_number, I18n.t(:report_header_gst_free_income), opts), - summary_row(order, I18n.t(:report_header_total_taxable_fees), total_taxable_fees(order), - invoice_number, I18n.t(:report_header_gst_on_income), opts)] - end - - def shipping_summary_rows(order, invoice_number, opts) - [summary_row(order, I18n.t(:report_header_delivery_shipping_cost), total_shipping(order), - invoice_number, tax_on_shipping_s(order), opts)] - end - - def payment_summary_rows(order, invoice_number, opts) - [summary_row(order, I18n.t(:report_header_transaction_fee), total_transaction(order), - invoice_number, I18n.t(:report_header_gst_free_income), opts)] - end - - def admin_adjustment_summary_rows(order, invoice_number, opts) - [summary_row(order, I18n.t(:report_header_total_untaxable_admin), total_untaxable_admin_adjustments(order), invoice_number, I18n.t(:report_header_gst_free_income), opts), - summary_row(order, I18n.t(:report_header_total_taxable_admin), - total_taxable_admin_adjustments(order), invoice_number, I18n.t(:report_header_gst_on_income), opts)] - end - - def summary_row(order, description, amount, invoice_number, tax_type, opts = {}) - row order, '', description, '1', amount, invoice_number, tax_type, opts - end - - def row(order, sku, description, quantity, amount, invoice_number, tax_type, opts = {}) - return nil if amount == 0 - - [order.bill_address&.full_name, - order.email, - order.bill_address&.address1, - order.bill_address&.address2, - '', - '', - order.bill_address&.city, - order.bill_address&.state, - order.bill_address&.zipcode, - order.bill_address&.country&.name, - invoice_number, - order.number, - opts[:invoice_date], - opts[:due_date], - sku, - description, - quantity, - amount, - '', - opts[:account_code], - tax_type, - '', - '', - '', - '', - Spree::Config.currency, - '', - order.paid? ? I18n.t(:y) : I18n.t(:n)] - end - - def admin_adjustments(order) - order.adjustments.admin - end - - def adjustment_order(adjustment) - adjustment.adjustable.is_a?(Spree::Order) ? adjustment.adjustable : nil - end - - def invoice_number_for(order, idx) - @opts[:initial_invoice_number] ? @opts[:initial_invoice_number].to_i + idx : order.number - end - - def total_untaxable_products(order) - order.line_items.without_tax.to_a.sum(&:amount) - end - - def total_taxable_products(order) - order.line_items.with_tax.to_a.sum(&:amount) - end - - def total_untaxable_fees(order) - order.all_adjustments.enterprise_fee.where(tax_category: nil).sum(:amount) - end - - def total_taxable_fees(order) - order.all_adjustments.enterprise_fee.where.not(tax_category: nil).sum(:amount) - end - - def total_shipping(order) - order.all_adjustments.shipping.sum(:amount) - end - - def total_transaction(order) - order.all_adjustments.payment_fee.sum(:amount) - end - - def tax_on_shipping_s(order) - tax_on_shipping = order.shipments.sum("additional_tax_total + included_tax_total").positive? - tax_on_shipping ? I18n.t(:report_header_gst_on_income) : I18n.t(:report_header_gst_free_income) - end - - def total_untaxable_admin_adjustments(order) - order.adjustments.admin.where(tax_category: nil).sum(:amount) - end - - def total_taxable_admin_adjustments(order) - order.adjustments.admin.where.not(tax_category: nil).sum(:amount) - end - - def detail? - @opts[:report_subtype] == 'detailed' - end - - def tax_type(taxable) - taxable.has_tax? ? I18n.t(:report_header_gst_on_income) : I18n.t(:report_header_gst_free_income) - end - end -end diff --git a/lib/reporting/line_items.rb b/lib/reporting/line_items.rb new file mode 100644 index 0000000000..d5ee578ad3 --- /dev/null +++ b/lib/reporting/line_items.rb @@ -0,0 +1,57 @@ +# frozen_string_literal: true + +module Reporting + # shared code to search and list line items + class LineItems + def initialize(order_permissions, params, orders_relation = nil) + @order_permissions = order_permissions + @params = params + complete_not_canceled_visible_orders = CompleteVisibleOrders.new(order_permissions).query.not_state(:canceled) + @orders_relation = orders_relation || complete_not_canceled_visible_orders + end + + def orders + @orders ||= search_orders + end + + def list(line_item_includes = nil) + line_items = order_permissions.visible_line_items.in_orders(orders.result) + + if @params[:supplier_id_in].present? + line_items = line_items.supplied_by_any(@params[:supplier_id_in]) + end + + if line_item_includes.present? + line_items = line_items.includes(*line_item_includes).references(:line_items) + end + + without_editable_line_items = line_items - editable_line_items(line_items) + + without_editable_line_items.each do |line_item| + OrderDataMasker.new(line_item.order).call + end + + line_items + end + + private + + attr_reader :orders_relation, :order_permissions + + def search_orders + orders_relation.ransack(@params[:q]) + end + + # From the line_items given, returns the ones that are editable by the user + def editable_line_items(line_items) + editable_line_items_ids = order_permissions.editable_line_items.select(:id) + + # Although merge could take a relation, here we convert line_items to array + # because, if we pass a relation, merge will overwrite the conditions on the same field + # In this case: the IN clause on spree_line_items.order_id from line_items + # overwrites the IN clause on spree_line_items.order_id on editable_line_items_ids + # We convert to array the relation with less elements: line_items + editable_line_items_ids.merge(line_items.to_a) + end + end +end diff --git a/lib/open_food_network/order_grouper.rb b/lib/reporting/order_grouper.rb similarity index 98% rename from lib/open_food_network/order_grouper.rb rename to lib/reporting/order_grouper.rb index cc4720bab9..dc78dfff8c 100644 --- a/lib/open_food_network/order_grouper.rb +++ b/lib/reporting/order_grouper.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -module OpenFoodNetwork +module Reporting class OrderGrouper def initialize(rules, column_constructors, report = nil) @rules = rules diff --git a/lib/reporting/reports/bulk_coop/bulk_coop_allocation_report.rb b/lib/reporting/reports/bulk_coop/bulk_coop_allocation_report.rb new file mode 100644 index 0000000000..8295b04a93 --- /dev/null +++ b/lib/reporting/reports/bulk_coop/bulk_coop_allocation_report.rb @@ -0,0 +1,67 @@ +# frozen_string_literal: true + +module Reporting + module Reports + module BulkCoop + class BulkCoopAllocationReport + def table_headers + [ + I18n.t(:report_header_customer), + I18n.t(:report_header_product), + I18n.t(:report_header_bulk_unit_size), + I18n.t(:report_header_variant), + I18n.t(:report_header_variant_value), + I18n.t(:report_header_variant_unit), + I18n.t(:report_header_weight), + I18n.t(:report_header_sum_total), + I18n.t(:report_header_total_available), + I18n.t(:report_header_unallocated), + I18n.t(:report_header_max_quantity_excess), + ] + end + + def rules + [ + { + group_by: proc { |line_item| line_item.product }, + sort_by: proc { |product| product.name }, + summary_columns: [ + :total_label, + :variant_product_name, + :variant_product_group_buy_unit_size_f, + :empty_cell, + :empty_cell, + :empty_cell, + :empty_cell, + :total_amount, + :total_available, + :remainder, + :max_quantity_excess + ] + }, + { + group_by: proc { |line_item| line_item.order }, + sort_by: proc { |order| order.to_s } + } + ] + end + + def columns + [ + :order_billing_address_name, + :product_name, + :product_group_buy_unit_size, + :full_name, + :option_value_value, + :option_value_unit, + :weight_from_unit_value, + :total_amount, + :empty_cell, + :empty_cell, + :empty_cell + ] + end + end + end + end +end diff --git a/lib/reporting/reports/bulk_coop/bulk_coop_report.rb b/lib/reporting/reports/bulk_coop/bulk_coop_report.rb new file mode 100644 index 0000000000..d30641f5df --- /dev/null +++ b/lib/reporting/reports/bulk_coop/bulk_coop_report.rb @@ -0,0 +1,319 @@ +# frozen_string_literal: true + +module Reporting + module Reports + module BulkCoop + class BulkCoopReport + attr_reader :params + + def initialize(user, params = {}, render_table = false) + @params = params + @user = user + @render_table = render_table + + @supplier_report = BulkCoopSupplierReport.new + @allocation_report = BulkCoopAllocationReport.new + @filter_canceled = false + end + + def table_headers + case params[:report_subtype] + when "bulk_coop_supplier_report" + @supplier_report.table_headers + when "bulk_coop_allocation" + @allocation_report.table_headers + when "bulk_coop_packing_sheets" + [I18n.t(:report_header_customer), + I18n.t(:report_header_product), + I18n.t(:report_header_variant), + I18n.t(:report_header_sum_total)] + when "bulk_coop_customer_payments" + [I18n.t(:report_header_customer), + I18n.t(:report_header_date_of_order), + I18n.t(:report_header_total_cost), + I18n.t(:report_header_amount_owing), + I18n.t(:report_header_amount_paid)] + else + [I18n.t(:report_header_supplier), + I18n.t(:report_header_product), + I18n.t(:report_header_product), + I18n.t(:report_header_bulk_unit_size), + I18n.t(:report_header_variant), + I18n.t(:report_header_weight), + I18n.t(:report_header_sum_total), + I18n.t(:report_header_sum_max_total), + I18n.t(:report_header_units_required), + I18n.t(:report_header_remainder)] + end + end + + def search + report_line_items.orders + end + + def table_items + return [] unless @render_table + + report_line_items.list(line_item_includes) + end + + def table_rows + order_grouper = Reporting::OrderGrouper.new rules, columns, self + order_grouper.table(table_items) + end + + def rules + case params[:report_subtype] + when "bulk_coop_supplier_report" + @supplier_report.rules + when "bulk_coop_allocation" + @allocation_report.rules + when "bulk_coop_packing_sheets" + [{ group_by: proc { |li| li.product }, + sort_by: proc { |product| product.name } }, + { group_by: proc { |li| li.full_name }, + sort_by: proc { |full_name| full_name } }, + { group_by: proc { |li| li.order }, + sort_by: proc { |order| order.to_s } }] + when "bulk_coop_customer_payments" + [{ group_by: proc { |li| li.order }, + sort_by: proc { |order| order.completed_at } }] + else + [{ group_by: proc { |li| li.product.supplier }, + sort_by: proc { |supplier| supplier.name } }, + { group_by: proc { |li| li.product }, + sort_by: proc { |product| product.name }, + summary_columns: [proc { |lis| lis.first.product.supplier.name }, + proc { |lis| lis.first.product.name }, + proc { |lis| lis.first.product.group_buy_unit_size || 0.0 }, + proc { |_lis| "" }, + proc { |_lis| "" }, + proc { |lis| + lis.sum { |li| + li.quantity * (li.weight_from_unit_value || 0) + } + }, + proc { |lis| + lis.sum { |li| + (li.max_quantity || 0) * (li.weight_from_unit_value || 0) + } + }, + proc { |lis| + ( if (lis.first.product.group_buy_unit_size || 0).zero? + 0 + else + ( lis.sum { |li| + [li.max_quantity || 0, + li.quantity || 0].max * (li.weight_from_unit_value || 0) + } / lis.first.product.group_buy_unit_size ) + end ).floor + }, + proc { |lis| + lis.sum { |li| + [li.max_quantity || 0, + li.quantity || 0].max * (li.weight_from_unit_value || 0) + } - ( ( if (lis.first.product.group_buy_unit_size || 0).zero? + 0 + else + ( lis.sum { |li| + [li.max_quantity || 0, + li.quantity || 0].max * (li.weight_from_unit_value || 0) + } / lis.first.product.group_buy_unit_size ) + end ).floor * (lis.first.product.group_buy_unit_size || 0) ) + }] }, + { group_by: proc { |li| li.full_name }, + sort_by: proc { |full_name| full_name } }] + end + end + + def columns + case params[:report_subtype] + when "bulk_coop_supplier_report" + @supplier_report.columns + when "bulk_coop_allocation" + @allocation_report.columns + when "bulk_coop_packing_sheets" + [ + :order_billing_address_name, + :product_name, + :full_name, + :total_quantity + ] + when "bulk_coop_customer_payments" + [ + :order_billing_address_name, + :order_completed_at, + :customer_payments_total_cost, + :customer_payments_amount_owed, + :customer_payments_amount_paid + ] + else + [ + :product_supplier_name, + :product_name, + :product_group_buy_unit_size, + :full_name, + :weight_from_unit_value, + :total_quantity, + :total_max_quantity, + :empty_cell, + :empty_cell + ] + end + end + + private + + attr_reader :filter_canceled + + def line_item_includes + [ + { + order: [:bill_address], + variant: [{ option_values: :option_type }, { product: :supplier }] + }, + :option_values + ] + end + + def order_permissions + @order_permissions ||= ::Permissions::Order.new(@user, filter_canceled) + end + + def report_line_items + @report_line_items ||= Reporting::LineItems.new( + order_permissions, + @params, + CompleteVisibleOrders.new(order_permissions).query + ) + end + + def customer_payments_total_cost(line_items) + unique_orders(line_items).sum(&:total) + end + + def customer_payments_amount_owed(line_items) + unique_orders(line_items).sum(&:new_outstanding_balance) + end + + def customer_payments_amount_paid(line_items) + unique_orders(line_items).sum(&:payment_total) + end + + def unique_orders(line_items) + line_items.map(&:order).uniq + end + + def empty_cell(_line_items) + "" + end + + def full_name(line_items) + line_items.first.full_name + end + + def group_buy_unit_size(line_items) + unit_size = line_items.first.variant.product.group_buy_unit_size || 0.0 + unit_size / (line_items.first.product.variant_unit_scale || 1) + end + + def max_quantity_excess(line_items) + max_quantity_amount(line_items) - total_amount(line_items) + end + + def max_quantity_amount(line_items) + line_items.sum do |line_item| + max_quantity = [line_item.max_quantity || 0, line_item.quantity || 0].max + max_quantity * scaled_unit_value(line_item.variant) + end + end + + def option_value_value(line_items) + VariantUnits::OptionValueNamer.new(line_items.first).value + end + + def option_value_unit(line_items) + VariantUnits::OptionValueNamer.new(line_items.first).unit + end + + def order_billing_address_name(line_items) + billing_address = line_items.first.order.bill_address + "#{billing_address.firstname} #{billing_address.lastname}" + end + + def order_completed_at(line_items) + line_items.first.order.completed_at.to_s + end + + def product_group_buy_unit_size(line_items) + line_items.first.product.group_buy_unit_size || 0.0 + end + + def product_name(line_items) + line_items.first.product.name + end + + def product_supplier_name(line_items) + line_items.first.product.supplier.name + end + + def remainder(line_items) + remainder = total_available(line_items) - total_amount(line_items) + remainder >= 0 ? remainder : '' + end + + def scaled_final_weight_volume(line_item) + (line_item.final_weight_volume || 0) / (line_item.product.variant_unit_scale || 1) + end + + def scaled_unit_value(variant) + (variant.unit_value || 0) / (variant.product.variant_unit_scale || 1) + end + + def total_amount(line_items) + line_items.sum { |li| scaled_final_weight_volume(li) } + end + + def total_available(line_items) + units_required(line_items) * group_buy_unit_size(line_items) + end + + def total_max_quantity(line_items) + line_items.sum { |line_item| line_item.max_quantity || 0 } + end + + def total_quantity(line_items) + line_items.sum(&:quantity) + end + + def total_label(_line_items) + I18n.t('admin.reports.total') + end + + def units_required(line_items) + if group_buy_unit_size(line_items).zero? + 0 + else + ( total_amount(line_items) / group_buy_unit_size(line_items) ).ceil + end + end + + def variant_product_group_buy_unit_size_f(line_items) + group_buy_unit_size(line_items) + end + + def variant_product_name(line_items) + line_items.first.variant.product.name + end + + def variant_product_supplier_name(line_items) + line_items.first.variant.product.supplier.name + end + + def weight_from_unit_value(line_items) + line_items.first.weight_from_unit_value || 0 + end + end + end + end +end diff --git a/lib/reporting/reports/bulk_coop/bulk_coop_supplier_report.rb b/lib/reporting/reports/bulk_coop/bulk_coop_supplier_report.rb new file mode 100644 index 0000000000..1806105424 --- /dev/null +++ b/lib/reporting/reports/bulk_coop/bulk_coop_supplier_report.rb @@ -0,0 +1,65 @@ +# frozen_string_literal: true + +module Reporting + module Reports + module BulkCoop + class BulkCoopSupplierReport + def table_headers + [ + I18n.t(:report_header_supplier), + I18n.t(:report_header_product), + I18n.t(:report_header_bulk_unit_size), + I18n.t(:report_header_variant), + I18n.t(:report_header_variant_value), + I18n.t(:report_header_variant_unit), + I18n.t(:report_header_weight), + I18n.t(:report_header_sum_total), + I18n.t(:report_header_units_required), + I18n.t(:report_header_unallocated), + I18n.t(:report_header_max_quantity_excess), + ] + end + + def rules + [ + { group_by: proc { |line_item| line_item.product.supplier }, + sort_by: proc { |supplier| supplier.name } }, + { group_by: proc { |line_item| line_item.product }, + sort_by: proc { |product| product.name }, + summary_columns: [ + :variant_product_supplier_name, + :variant_product_name, + :variant_product_group_buy_unit_size_f, + :empty_cell, + :empty_cell, + :empty_cell, + :empty_cell, + :total_amount, + :units_required, + :remainder, + :max_quantity_excess + ] }, + { group_by: proc { |line_item| line_item.full_name }, + sort_by: proc { |full_name| full_name } } + ] + end + + def columns + [ + :variant_product_supplier_name, + :variant_product_name, + :variant_product_group_buy_unit_size_f, + :full_name, + :option_value_value, + :option_value_unit, + :weight_from_unit_value, + :total_amount, + :empty_cell, + :empty_cell, + :empty_cell + ] + end + end + end + end +end diff --git a/lib/reporting/reports/customers/customers_report.rb b/lib/reporting/reports/customers/customers_report.rb new file mode 100644 index 0000000000..0fa040646b --- /dev/null +++ b/lib/reporting/reports/customers/customers_report.rb @@ -0,0 +1,105 @@ +# frozen_string_literal: true + +module Reporting + module Reports + module Customers + class CustomersReport + attr_reader :params + + def initialize(user, params = {}, compile_table = false) + @params = params + @user = user + @compile_table = compile_table + end + + def table_headers + if is_mailing_list? + [I18n.t(:report_header_email), + I18n.t(:report_header_first_name), + I18n.t(:report_header_last_name), + I18n.t(:report_header_suburb)] + else + [I18n.t(:report_header_first_name), + I18n.t(:report_header_last_name), + I18n.t(:report_header_billing_address), + I18n.t(:report_header_email), + I18n.t(:report_header_phone), + I18n.t(:report_header_hub), + I18n.t(:report_header_hub_address), + I18n.t(:report_header_shipping_method)] + end + end + + def table_rows + return [] unless @compile_table + + orders.map do |order| + if is_mailing_list? + [order.email, + order.billing_address.firstname, + order.billing_address.lastname, + order.billing_address.city] + else + ba = order.billing_address + da = order.distributor&.address + [ba.firstname, + ba.lastname, + [ba.address1, ba.address2, ba.city].join(" "), + order.email, + ba.phone, + order.distributor&.name, + [da&.address1, da&.address2, da&.city].join(" "), + order.shipping_method&.name] + end + end + end + + def orders + filter Spree::Order.managed_by(@user) + .distributed_by_user(@user) + .complete.not_state(:canceled) + end + + def filter(orders) + filter_to_supplier filter_to_distributor filter_to_order_cycle orders + end + + def filter_to_supplier(orders) + if params[:supplier_id].to_i > 0 + orders.select do |order| + order.line_items.includes(:product) + .where("spree_products.supplier_id = ?", params[:supplier_id].to_i) + .references(:product) + .count + .positive? + end + else + orders + end + end + + def filter_to_distributor(orders) + if params[:distributor_id].to_i > 0 + orders.where(distributor_id: params[:distributor_id]) + else + orders + end + end + + def filter_to_order_cycle(orders) + if params[:order_cycle_id].to_i > 0 + orders.where(order_cycle_id: params[:order_cycle_id]) + else + orders + end + end + + private + + def is_mailing_list? + params[:report_subtype] == "mailing_list" + end + end + end + end +end diff --git a/engines/order_management/app/services/order_management/reports/enterprise_fee_summary/authorizer.rb b/lib/reporting/reports/enterprise_fee_summary/authorizer.rb similarity index 89% rename from engines/order_management/app/services/order_management/reports/enterprise_fee_summary/authorizer.rb rename to lib/reporting/reports/enterprise_fee_summary/authorizer.rb index b85fdcbb6f..65b67bc023 100644 --- a/engines/order_management/app/services/order_management/reports/enterprise_fee_summary/authorizer.rb +++ b/lib/reporting/reports/enterprise_fee_summary/authorizer.rb @@ -1,9 +1,9 @@ # frozen_string_literal: true -module OrderManagement +module Reporting module Reports module EnterpriseFeeSummary - class Authorizer < ::Reports::Authorizer + class Authorizer < Reporting::Reports::EnterpriseFeeSummary::Reports::Authorizer def authorize! authorize_by_distribution! authorize_by_fee! diff --git a/engines/order_management/app/services/order_management/reports/enterprise_fee_summary/data_representations/coordinator_fee.rb b/lib/reporting/reports/enterprise_fee_summary/data_representations/coordinator_fee.rb similarity index 97% rename from engines/order_management/app/services/order_management/reports/enterprise_fee_summary/data_representations/coordinator_fee.rb rename to lib/reporting/reports/enterprise_fee_summary/data_representations/coordinator_fee.rb index 9d11b94d26..de7fe47eeb 100644 --- a/engines/order_management/app/services/order_management/reports/enterprise_fee_summary/data_representations/coordinator_fee.rb +++ b/lib/reporting/reports/enterprise_fee_summary/data_representations/coordinator_fee.rb @@ -3,7 +3,7 @@ # This module provides EnterpriseFeeSummary::Scope DB result to report mappings for coordinator fees # in an order cycle. -module OrderManagement +module Reporting module Reports module EnterpriseFeeSummary module DataRepresentations diff --git a/engines/order_management/app/services/order_management/reports/enterprise_fee_summary/data_representations/exchange_order_fee.rb b/lib/reporting/reports/enterprise_fee_summary/data_representations/exchange_order_fee.rb similarity index 96% rename from engines/order_management/app/services/order_management/reports/enterprise_fee_summary/data_representations/exchange_order_fee.rb rename to lib/reporting/reports/enterprise_fee_summary/data_representations/exchange_order_fee.rb index efef74fd11..1bf26df688 100644 --- a/engines/order_management/app/services/order_management/reports/enterprise_fee_summary/data_representations/exchange_order_fee.rb +++ b/lib/reporting/reports/enterprise_fee_summary/data_representations/exchange_order_fee.rb @@ -3,7 +3,7 @@ # This module provides EnterpriseFeeSummary::Scope DB result to report mappings for exchange fees # that use order-based calculators. -module OrderManagement +module Reporting module Reports module EnterpriseFeeSummary module DataRepresentations diff --git a/engines/order_management/app/services/order_management/reports/enterprise_fee_summary/data_representations/incoming_exchange_line_item_fee.rb b/lib/reporting/reports/enterprise_fee_summary/data_representations/incoming_exchange_line_item_fee.rb similarity index 96% rename from engines/order_management/app/services/order_management/reports/enterprise_fee_summary/data_representations/incoming_exchange_line_item_fee.rb rename to lib/reporting/reports/enterprise_fee_summary/data_representations/incoming_exchange_line_item_fee.rb index 2ab859a193..176c400c9e 100644 --- a/engines/order_management/app/services/order_management/reports/enterprise_fee_summary/data_representations/incoming_exchange_line_item_fee.rb +++ b/lib/reporting/reports/enterprise_fee_summary/data_representations/incoming_exchange_line_item_fee.rb @@ -3,7 +3,7 @@ # This module provides EnterpriseFeeSummary::Scope DB result to report mappings for incoming # exchange fees that use line item -based calculators. -module OrderManagement +module Reporting module Reports module EnterpriseFeeSummary module DataRepresentations diff --git a/engines/order_management/app/services/order_management/reports/enterprise_fee_summary/data_representations/outgoing_exchange_line_item_fee.rb b/lib/reporting/reports/enterprise_fee_summary/data_representations/outgoing_exchange_line_item_fee.rb similarity index 96% rename from engines/order_management/app/services/order_management/reports/enterprise_fee_summary/data_representations/outgoing_exchange_line_item_fee.rb rename to lib/reporting/reports/enterprise_fee_summary/data_representations/outgoing_exchange_line_item_fee.rb index 3a547d659b..cfc2568045 100644 --- a/engines/order_management/app/services/order_management/reports/enterprise_fee_summary/data_representations/outgoing_exchange_line_item_fee.rb +++ b/lib/reporting/reports/enterprise_fee_summary/data_representations/outgoing_exchange_line_item_fee.rb @@ -3,7 +3,7 @@ # This module provides EnterpriseFeeSummary::Scope DB result to report mappings for outgoing # exchange fees that use line item -based calculators. -module OrderManagement +module Reporting module Reports module EnterpriseFeeSummary module DataRepresentations diff --git a/engines/order_management/app/services/order_management/reports/enterprise_fee_summary/data_representations/payment_method_fee.rb b/lib/reporting/reports/enterprise_fee_summary/data_representations/payment_method_fee.rb similarity index 97% rename from engines/order_management/app/services/order_management/reports/enterprise_fee_summary/data_representations/payment_method_fee.rb rename to lib/reporting/reports/enterprise_fee_summary/data_representations/payment_method_fee.rb index 4fab78ad56..ce1ca48aa7 100644 --- a/engines/order_management/app/services/order_management/reports/enterprise_fee_summary/data_representations/payment_method_fee.rb +++ b/lib/reporting/reports/enterprise_fee_summary/data_representations/payment_method_fee.rb @@ -3,7 +3,7 @@ # This module provides EnterpriseFeeSummary::Scope DB result to report mappings for payment method # fees. -module OrderManagement +module Reporting module Reports module EnterpriseFeeSummary module DataRepresentations diff --git a/engines/order_management/app/services/order_management/reports/enterprise_fee_summary/data_representations/shipping_method_fee.rb b/lib/reporting/reports/enterprise_fee_summary/data_representations/shipping_method_fee.rb similarity index 97% rename from engines/order_management/app/services/order_management/reports/enterprise_fee_summary/data_representations/shipping_method_fee.rb rename to lib/reporting/reports/enterprise_fee_summary/data_representations/shipping_method_fee.rb index 3863ff6d29..83b4096931 100644 --- a/engines/order_management/app/services/order_management/reports/enterprise_fee_summary/data_representations/shipping_method_fee.rb +++ b/lib/reporting/reports/enterprise_fee_summary/data_representations/shipping_method_fee.rb @@ -3,7 +3,7 @@ # This module provides EnterpriseFeeSummary::Scope DB result to report mappings for shipping method # fees. -module OrderManagement +module Reporting module Reports module EnterpriseFeeSummary module DataRepresentations diff --git a/engines/order_management/app/services/order_management/reports/enterprise_fee_summary/data_representations/using_enterprise_fee.rb b/lib/reporting/reports/enterprise_fee_summary/data_representations/using_enterprise_fee.rb similarity index 97% rename from engines/order_management/app/services/order_management/reports/enterprise_fee_summary/data_representations/using_enterprise_fee.rb rename to lib/reporting/reports/enterprise_fee_summary/data_representations/using_enterprise_fee.rb index 85e466c7ce..0e69499e1c 100644 --- a/engines/order_management/app/services/order_management/reports/enterprise_fee_summary/data_representations/using_enterprise_fee.rb +++ b/lib/reporting/reports/enterprise_fee_summary/data_representations/using_enterprise_fee.rb @@ -7,7 +7,7 @@ # fees. These mappings are not complete and should be supplemented with mappings that are specific # to the way that the enterprise fee is attached to the order cycle. -module OrderManagement +module Reporting module Reports module EnterpriseFeeSummary module DataRepresentations diff --git a/engines/order_management/app/services/order_management/reports/enterprise_fee_summary/data_representations/with_i18n.rb b/lib/reporting/reports/enterprise_fee_summary/data_representations/with_i18n.rb similarity index 94% rename from engines/order_management/app/services/order_management/reports/enterprise_fee_summary/data_representations/with_i18n.rb rename to lib/reporting/reports/enterprise_fee_summary/data_representations/with_i18n.rb index 4601efeaa3..3fe7b61637 100644 --- a/engines/order_management/app/services/order_management/reports/enterprise_fee_summary/data_representations/with_i18n.rb +++ b/lib/reporting/reports/enterprise_fee_summary/data_representations/with_i18n.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -module OrderManagement +module Reporting module Reports module EnterpriseFeeSummary module DataRepresentations diff --git a/engines/order_management/app/services/order_management/reports/enterprise_fee_summary/enterprise_fee_summary_report.rb b/lib/reporting/reports/enterprise_fee_summary/enterprise_fee_summary_report.rb similarity index 96% rename from engines/order_management/app/services/order_management/reports/enterprise_fee_summary/enterprise_fee_summary_report.rb rename to lib/reporting/reports/enterprise_fee_summary/enterprise_fee_summary_report.rb index c9b2b37852..c8bdd7895a 100644 --- a/engines/order_management/app/services/order_management/reports/enterprise_fee_summary/enterprise_fee_summary_report.rb +++ b/lib/reporting/reports/enterprise_fee_summary/enterprise_fee_summary_report.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -module OrderManagement +module Reporting module Reports module EnterpriseFeeSummary class EnterpriseFeeSummaryReport @@ -12,7 +12,7 @@ module OrderManagement p['start_at'] = p.delete('completed_at_gt') p['end_at'] = p.delete('completed_at_lt') end - @parameters = OrderManagement::Reports::EnterpriseFeeSummary::Parameters.new(p || {}) + @parameters = Reporting::Reports::EnterpriseFeeSummary::Parameters.new(p || {}) @parameters.validate! @user = user @render_table = render_table diff --git a/lib/reporting/reports/enterprise_fee_summary/parameter_not_allowed_error.rb b/lib/reporting/reports/enterprise_fee_summary/parameter_not_allowed_error.rb new file mode 100644 index 0000000000..6143e43e6f --- /dev/null +++ b/lib/reporting/reports/enterprise_fee_summary/parameter_not_allowed_error.rb @@ -0,0 +1,9 @@ +# frozen_string_literal: true + +module Reporting + module Reports + module EnterpriseFeeSummary + class ParameterNotAllowedError < StandardError; end + end + end +end diff --git a/engines/order_management/app/services/order_management/reports/enterprise_fee_summary/parameters.rb b/lib/reporting/reports/enterprise_fee_summary/parameters.rb similarity index 94% rename from engines/order_management/app/services/order_management/reports/enterprise_fee_summary/parameters.rb rename to lib/reporting/reports/enterprise_fee_summary/parameters.rb index bdacfab75a..59c97aeaa2 100644 --- a/engines/order_management/app/services/order_management/reports/enterprise_fee_summary/parameters.rb +++ b/lib/reporting/reports/enterprise_fee_summary/parameters.rb @@ -1,9 +1,9 @@ # frozen_string_literal: true -module OrderManagement +module Reporting module Reports module EnterpriseFeeSummary - class Parameters < ::Reports::Parameters::Base + class Parameters < Reporting::Reports::EnterpriseFeeSummary::Reports::Parameters::Base include ActiveModel::Validations attr_accessor :start_at, :end_at, :distributor_ids, :producer_ids, :order_cycle_ids, diff --git a/engines/order_management/app/services/order_management/reports/enterprise_fee_summary/permissions.rb b/lib/reporting/reports/enterprise_fee_summary/permissions.rb similarity index 98% rename from engines/order_management/app/services/order_management/reports/enterprise_fee_summary/permissions.rb rename to lib/reporting/reports/enterprise_fee_summary/permissions.rb index 0538068ed4..eef5d05498 100644 --- a/engines/order_management/app/services/order_management/reports/enterprise_fee_summary/permissions.rb +++ b/lib/reporting/reports/enterprise_fee_summary/permissions.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -module OrderManagement +module Reporting module Reports module EnterpriseFeeSummary class Permissions diff --git a/engines/order_management/app/services/order_management/reports/enterprise_fee_summary/report_data/enterprise_fee_type_total.rb b/lib/reporting/reports/enterprise_fee_summary/report_data/enterprise_fee_type_total.rb similarity index 86% rename from engines/order_management/app/services/order_management/reports/enterprise_fee_summary/report_data/enterprise_fee_type_total.rb rename to lib/reporting/reports/enterprise_fee_summary/report_data/enterprise_fee_type_total.rb index dddcd2ca46..8b090f9098 100644 --- a/engines/order_management/app/services/order_management/reports/enterprise_fee_summary/report_data/enterprise_fee_type_total.rb +++ b/lib/reporting/reports/enterprise_fee_summary/report_data/enterprise_fee_type_total.rb @@ -1,10 +1,10 @@ # frozen_string_literal: true -module OrderManagement +module Reporting module Reports module EnterpriseFeeSummary module ReportData - class EnterpriseFeeTypeTotal < ::Reports::ReportData::Base + class EnterpriseFeeTypeTotal < Reporting::Reports::EnterpriseFeeSummary::Reports::ReportData::Base attr_accessor :fee_type, :enterprise_name, :fee_name, :customer_name, :fee_placement, :fee_calculated_on_transfer_through_name, :tax_category_name, :total_amount diff --git a/lib/reporting/reports/enterprise_fee_summary/reports/authorizer.rb b/lib/reporting/reports/enterprise_fee_summary/reports/authorizer.rb new file mode 100644 index 0000000000..8a76271614 --- /dev/null +++ b/lib/reporting/reports/enterprise_fee_summary/reports/authorizer.rb @@ -0,0 +1,33 @@ +# frozen_string_literal: true + +module Reporting + module Reports + module EnterpriseFeeSummary + module Reports + class Authorizer + attr_accessor :parameters, :permissions + + def initialize(parameters, permissions) + @parameters = parameters + @permissions = permissions + end + + def self.parameter_not_allowed_error_message + i18n_scope = "order_management.reports.enterprise_fee_summary" + I18n.t("parameter_not_allowed_error", scope: i18n_scope) + end + + private + + def require_ids_allowed(array, allowed_objects) + error_klass = Reporting::Reports::EnterpriseFeeSummary::ParameterNotAllowedError + error_message = self.class.parameter_not_allowed_error_message + ids_allowed = (array - allowed_objects.map(&:id).map(&:to_s)).blank? + + raise error_klass, error_message unless ids_allowed + end + end + end + end + end +end diff --git a/lib/reporting/reports/enterprise_fee_summary/reports/parameters/base.rb b/lib/reporting/reports/enterprise_fee_summary/reports/parameters/base.rb new file mode 100644 index 0000000000..0906658d88 --- /dev/null +++ b/lib/reporting/reports/enterprise_fee_summary/reports/parameters/base.rb @@ -0,0 +1,41 @@ +# frozen_string_literal: true + +module Reporting + module Reports + module EnterpriseFeeSummary + module Reports + module Parameters + class Base + extend ActiveModel::Naming + extend ActiveModel::Translation + include ActiveModel::Validations + include ActiveModel::Validations::Callbacks + + def initialize(attributes = {}) + attributes.each do |key, value| + public_send("#{key}=", value) + end + end + + def self.date_end_before_start_error_message + i18n_scope = "order_management.reports.enterprise_fee_summary" + I18n.t("date_end_before_start_error", scope: i18n_scope) + end + + # The parameters are never persisted. + def to_key; end + + protected + + def require_valid_datetime_range + return if start_at.blank? || end_at.blank? + + error_message = self.class.date_end_before_start_error_message + errors.add(:end_at, error_message) unless start_at < end_at + end + end + end + end + end + end +end diff --git a/lib/reporting/reports/enterprise_fee_summary/reports/report_data/base.rb b/lib/reporting/reports/enterprise_fee_summary/reports/report_data/base.rb new file mode 100644 index 0000000000..f22729929b --- /dev/null +++ b/lib/reporting/reports/enterprise_fee_summary/reports/report_data/base.rb @@ -0,0 +1,19 @@ +# frozen_string_literal: true + +module Reporting + module Reports + module EnterpriseFeeSummary + module Reports + module ReportData + class Base + def initialize(attributes = {}) + attributes.each do |key, value| + public_send("#{key}=", value) + end + end + end + end + end + end + end +end diff --git a/engines/order_management/app/services/order_management/reports/enterprise_fee_summary/scope.rb b/lib/reporting/reports/enterprise_fee_summary/scope.rb similarity index 99% rename from engines/order_management/app/services/order_management/reports/enterprise_fee_summary/scope.rb rename to lib/reporting/reports/enterprise_fee_summary/scope.rb index 8c52d64f33..86a165ad08 100644 --- a/engines/order_management/app/services/order_management/reports/enterprise_fee_summary/scope.rb +++ b/lib/reporting/reports/enterprise_fee_summary/scope.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -module OrderManagement +module Reporting module Reports module EnterpriseFeeSummary class Scope diff --git a/engines/order_management/app/services/order_management/reports/enterprise_fee_summary/summarizer.rb b/lib/reporting/reports/enterprise_fee_summary/summarizer.rb similarity index 98% rename from engines/order_management/app/services/order_management/reports/enterprise_fee_summary/summarizer.rb rename to lib/reporting/reports/enterprise_fee_summary/summarizer.rb index 3f9b0098b5..dd223b72dd 100644 --- a/engines/order_management/app/services/order_management/reports/enterprise_fee_summary/summarizer.rb +++ b/lib/reporting/reports/enterprise_fee_summary/summarizer.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -module OrderManagement +module Reporting module Reports module EnterpriseFeeSummary class Summarizer diff --git a/lib/open_food_network/reports/list.rb b/lib/reporting/reports/list.rb similarity index 99% rename from lib/open_food_network/reports/list.rb rename to lib/reporting/reports/list.rb index 0f03512b68..d9ab57b945 100644 --- a/lib/open_food_network/reports/list.rb +++ b/lib/reporting/reports/list.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -module OpenFoodNetwork +module Reporting module Reports class List def self.all diff --git a/lib/reporting/reports/order_cycle_management/order_cycle_management_report.rb b/lib/reporting/reports/order_cycle_management/order_cycle_management_report.rb new file mode 100644 index 0000000000..1f095f4982 --- /dev/null +++ b/lib/reporting/reports/order_cycle_management/order_cycle_management_report.rb @@ -0,0 +1,170 @@ +# frozen_string_literal: true + +module Reporting + module Reports + module OrderCycleManagement + class OrderCycleManagementReport + DEFAULT_DATE_INTERVAL = { from: -1.month, to: 1.day }.freeze + + attr_reader :params + + def initialize(user, params = {}, render_table = false) + @params = sanitize_params(params) + @user = user + @render_table = render_table + end + + def table_headers + if is_payment_methods? + [ + I18n.t(:report_header_first_name), + I18n.t(:report_header_last_name), + I18n.t(:report_header_hub), + I18n.t(:report_header_hub_code), + I18n.t(:report_header_email), + I18n.t(:report_header_phone), + I18n.t(:report_header_shipping_method), + I18n.t(:report_header_payment_method), + I18n.t(:report_header_amount), + I18n.t(:report_header_balance), + ] + else + [ + I18n.t(:report_header_first_name), + I18n.t(:report_header_last_name), + I18n.t(:report_header_hub), + I18n.t(:report_header_hub_code), + I18n.t(:report_header_delivery_address), + I18n.t(:report_header_delivery_postcode), + I18n.t(:report_header_phone), + I18n.t(:report_header_shipping_method), + I18n.t(:report_header_payment_method), + I18n.t(:report_header_amount), + I18n.t(:report_header_balance), + I18n.t(:report_header_temp_controlled_items), + I18n.t(:report_header_special_instructions), + ] + end + end + + def search + Spree::Order. + finalized. + not_state(:canceled). + distributed_by_user(@user). + managed_by(@user). + ransack(params[:q]) + end + + def orders + search_result = search.result.order(:completed_at) + orders_with_balance = OutstandingBalance.new(search_result). + query. + select('spree_orders.*') + + filter(orders_with_balance) + end + + def table_rows + return [] unless @render_table + + if is_payment_methods? + orders.map { |o| payment_method_row o } + else + orders.map { |o| delivery_row o } + end + end + + def filter(search_result) + filter_to_payment_method filter_to_shipping_method filter_to_order_cycle search_result + end + + private + + # This method relies on `balance_value` as a computed DB column. See `CompleteOrdersWithBalance` + # for reference. + def balance(order) + order.balance_value + end + + def payment_method_row(order) + ba = order.billing_address + [ba&.firstname, + ba&.lastname, + order.distributor&.name, + customer_code(order.email), + order.email, + ba&.phone, + order.shipping_method&.name, + order.payments.last&.payment_method&.name, + order.total, + balance(order)] + end + + def delivery_row(order) + sa = order.shipping_address + [sa.firstname, + sa.lastname, + order.distributor&.name, + customer_code(order.email), + "#{sa.address1} #{sa.address2} #{sa.city}", + sa.zipcode, + sa.phone, + order.shipping_method&.name, + order.payments.first&.payment_method&.name, + order.total, + balance(order), + has_temperature_controlled_items?(order), + order.special_instructions] + end + + def filter_to_payment_method(orders) + if params[:payment_method_in].present? + orders.joins(payments: :payment_method).where(spree_payments: { payment_method_id: params[:payment_method_in] }) + else + orders + end + end + + def filter_to_shipping_method(orders) + if params[:shipping_method_in].present? + orders.joins(shipments: :shipping_rates).where(spree_shipping_rates: { selected: true, + shipping_method_id: params[:shipping_method_in] }) + else + orders + end + end + + def filter_to_order_cycle(orders) + if params[:order_cycle_id].present? + orders.where(order_cycle_id: params[:order_cycle_id]) + else + orders + end + end + + def has_temperature_controlled_items?(order) + order.line_items.any? { |line_item| + line_item.product.shipping_category&.temperature_controlled + } + end + + def is_payment_methods? + params[:report_subtype] == "payment_methods" + end + + def customer_code(email) + customer = Customer.where(email: email).first + customer.nil? ? "" : customer.code + end + + def sanitize_params(params) + params[:q] ||= {} + params[:q][:completed_at_gt] ||= Time.zone.today + DEFAULT_DATE_INTERVAL[:from] + params[:q][:completed_at_lt] ||= Time.zone.today + DEFAULT_DATE_INTERVAL[:to] + params + end + end + end + end +end diff --git a/lib/reporting/reports/orders_and_distributors/orders_and_distributors_report.rb b/lib/reporting/reports/orders_and_distributors/orders_and_distributors_report.rb new file mode 100644 index 0000000000..5f5ef599c1 --- /dev/null +++ b/lib/reporting/reports/orders_and_distributors/orders_and_distributors_report.rb @@ -0,0 +1,116 @@ +# frozen_string_literal: true + +module Reporting + module Reports + module OrdersAndDistributors + class OrdersAndDistributorsReport + def initialize(user, params = {}, render_table = false) + @params = params + @user = user + @render_table = render_table + + @permissions = ::Permissions::Order.new(user, @params[:q]) + end + + def table_headers + [ + I18n.t(:report_header_order_date), + I18n.t(:report_header_order_id), + I18n.t(:report_header_customer_name), + I18n.t(:report_header_customer_email), + I18n.t(:report_header_customer_phone), + I18n.t(:report_header_customer_city), + I18n.t(:report_header_sku), + I18n.t(:report_header_item_name), + I18n.t(:report_header_variant), + I18n.t(:report_header_quantity), + I18n.t(:report_header_max_quantity), + I18n.t(:report_header_cost), + I18n.t(:report_header_shipping_cost), + I18n.t(:report_header_payment_method), + I18n.t(:report_header_distributor), + I18n.t(:report_header_distributor_address), + I18n.t(:report_header_distributor_city), + I18n.t(:report_header_distributor_postcode), + I18n.t(:report_header_shipping_method), + I18n.t(:report_header_shipping_instructions) + ] + end + + def search + @permissions.visible_orders.select("DISTINCT spree_orders.*"). + complete.not_state(:canceled). + ransack(@params[:q]) + end + + def table_rows + return [] unless @render_table + + orders = search.result + + orders.select{ |order| orders_with_hidden_details(orders).include? order }.each do |order| + OrderDataMasker.new(order).call + end + + line_item_details orders + end + + private + + def orders_with_hidden_details(orders) + # If empty array is passed in, the where clause will return all line_items, which is bad + if @permissions.editable_orders.empty? + orders + else + orders. + where('spree_orders.id NOT IN (?)', + @permissions.editable_orders.select(&:id)) + end + end + + def line_item_details(orders) + order_and_distributor_details = [] + + orders.each do |order| + order.line_items.each do |line_item| + order_and_distributor_details << row_for(line_item, order) + end + end + + order_and_distributor_details + end + + # Returns a row with the data to display for the specified line_item and + # its order + # + # @param line_item [Spree::LineItem] + # @param order [Spree::Order] + # @return [Array] + def row_for(line_item, order) + [ + order.completed_at.strftime("%F %T"), + order.id, + order.bill_address.full_name, + order.email, + order.bill_address.phone, + order.bill_address.city, + line_item.product.sku, + line_item.product.name, + line_item.options_text, + line_item.quantity, + line_item.max_quantity, + line_item.price * line_item.quantity, + line_item.distribution_fee, + order.payments.first&.payment_method&.name, + order.distributor&.name, + order.distributor.address.address1, + order.distributor.address.city, + order.distributor.address.zipcode, + order.shipping_method&.name, + order.special_instructions + ] + end + end + end + end +end diff --git a/lib/reporting/reports/orders_and_fulfillment/customer_totals_report.rb b/lib/reporting/reports/orders_and_fulfillment/customer_totals_report.rb new file mode 100644 index 0000000000..ada0f6d533 --- /dev/null +++ b/lib/reporting/reports/orders_and_fulfillment/customer_totals_report.rb @@ -0,0 +1,223 @@ +# frozen_string_literal: true + +# rubocop:disable Metrics/ClassLength +module Reporting + module Reports + module OrdersAndFulfillment + class CustomerTotalsReport + REPORT_TYPE = "order_cycle_customer_totals" + + attr_reader :context + + delegate :line_item_name, to: :context + delegate :variant_scoper_for, to: :context + + def initialize(context) + @context = context + @scopers_by_distributor_id = {} + end + + # rubocop:disable Metrics/AbcSize + def table_headers + [I18n.t(:report_header_hub), I18n.t(:report_header_customer), I18n.t(:report_header_email), + I18n.t(:report_header_phone), I18n.t(:report_header_producer), + I18n.t(:report_header_product), I18n.t(:report_header_variant), + I18n.t(:report_header_quantity), + I18n.t(:report_header_item_price, currency: currency_symbol), + I18n.t(:report_header_item_fees_price, currency: currency_symbol), + I18n.t(:report_header_admin_handling_fees, currency: currency_symbol), + I18n.t(:report_header_ship_price, currency: currency_symbol), + I18n.t(:report_header_pay_fee_price, currency: currency_symbol), + I18n.t(:report_header_total_price, currency: currency_symbol), + I18n.t(:report_header_paid), I18n.t(:report_header_shipping), + I18n.t(:report_header_delivery), I18n.t(:report_header_ship_street), + I18n.t(:report_header_ship_street_2), I18n.t(:report_header_ship_city), + I18n.t(:report_header_ship_postcode), I18n.t(:report_header_ship_state), + I18n.t(:report_header_comments), I18n.t(:report_header_sku), + I18n.t(:report_header_order_cycle), I18n.t(:report_header_payment_method), + I18n.t(:report_header_customer_code), I18n.t(:report_header_tags), + I18n.t(:report_header_billing_street), I18n.t(:report_header_billing_street_2), + I18n.t(:report_header_billing_city), I18n.t(:report_header_billing_postcode), + I18n.t(:report_header_billing_state), + I18n.t(:report_header_order_number), + I18n.t(:report_header_date)] + end + + # rubocop:enable Metrics/AbcSize + # rubocop:disable Metrics/AbcSize + # rubocop:disable Metrics/MethodLength + def rules + [ + { + group_by: proc { |line_item| line_item.order.distributor }, + sort_by: proc { |distributor| distributor.name } + }, + { + group_by: proc { |line_item| line_item.order }, + sort_by: proc { |order| order.bill_address.full_name_reverse }, + summary_columns: [ + proc { |line_items| line_items.first.order.distributor.name }, + proc { |line_items| line_items.first.order.bill_address.full_name }, + proc { |_line_items| "" }, + proc { |_line_items| "" }, + proc { |_line_items| "" }, + proc { |_line_items| I18n.t('admin.reports.total') }, + proc { |_line_items| "" }, + + proc { |_line_items| "" }, + proc { |line_items| line_items.sum(&:amount) }, + proc { |line_items| line_items.sum(&:amount_with_adjustments) }, + proc { |line_items| line_items.first.order.admin_and_handling_total }, + proc { |line_items| line_items.first.order.ship_total }, + proc { |line_items| line_items.first.order.payment_fee }, + proc { |line_items| line_items.first.order.total }, + proc { |line_items| line_items.first.order.paid? ? I18n.t(:yes) : I18n.t(:no) }, + + proc { |_line_items| "" }, + proc { |_line_items| "" }, + + proc { |_line_items| "" }, + proc { |_line_items| "" }, + proc { |_line_items| "" }, + proc { |_line_items| "" }, + proc { |_line_items| "" }, + + proc { |line_items| line_items.first.order.special_instructions }, + proc { |_line_items| "" }, + + proc { |line_items| line_items.first.order.order_cycle&.name }, + proc { |line_items| + line_items.first.order.payments.first&.payment_method&.name + }, + proc { |_line_items| "" }, + proc { |_line_items| "" }, + proc { |_line_items| "" }, + proc { |_line_items| "" }, + proc { |_line_items| "" }, + proc { |_line_items| "" }, + proc { |_line_items| "" }, + proc { |line_items| line_items.first.order.number }, + proc { |line_items| line_items.first.order.completed_at.strftime("%F %T") }, + ] + }, + { + group_by: proc { |line_item| line_item.variant.product }, + sort_by: proc { |product| product.name } + }, + { + group_by: proc { |line_item| line_item.variant }, + sort_by: proc { |variant| variant.full_name } + }, + { + group_by: line_item_name, + sort_by: proc { |full_name| full_name } + } + ] + end + # rubocop:enable Metrics/AbcSize + # rubocop:enable Metrics/MethodLength + + # rubocop:disable Metrics/AbcSize + # rubocop:disable Metrics/MethodLength + # rubocop:disable Metrics/PerceivedComplexity + def columns + rsa = proc { |line_items| shipping_method(line_items)&.delivery? } + [ + proc { |line_items| line_items.first.order.distributor.name }, + proc { |line_items| + bill_address = line_items.first.order.bill_address + "#{bill_address.firstname} #{bill_address.lastname}" + }, + proc { |line_items| line_items.first.order.email }, + proc { |line_items| line_items.first.order.bill_address.phone }, + proc { |line_items| line_items.first.variant.product.supplier.name }, + proc { |line_items| line_items.first.variant.product.name }, + proc { |line_items| line_items.first.variant.full_name }, + + proc { |line_items| line_items.to_a.sum(&:quantity) }, + proc { |line_items| line_items.sum(&:amount) }, + proc { |line_items| line_items.sum(&:amount_with_adjustments) }, + proc { |_line_items| "" }, + proc { |_line_items| "" }, + proc { |_line_items| "" }, + proc { |_line_items| "" }, + proc { |line_items| + line_items.all? { |li| li.order.paid? } ? I18n.t(:yes) : I18n.t(:no) + }, + + proc { |line_items| shipping_method(line_items)&.name }, + proc { |line_items| rsa.call(line_items) ? I18n.t(:yes) : I18n.t(:no) }, + + proc { |line_items| + line_items.first.order.ship_address&.address1 if rsa.call(line_items) + }, + proc { |line_items| + line_items.first.order.ship_address&.address2 if rsa.call(line_items) + }, + proc { |line_items| + line_items.first.order.ship_address&.city if rsa.call(line_items) + }, + proc { |line_items| + line_items.first.order.ship_address&.zipcode if rsa.call(line_items) + }, + proc { |line_items| + line_items.first.order.ship_address&.state if rsa.call(line_items) + }, + + proc { |_line_items| "" }, + proc do |line_items| + line_item = line_items.first + variant_scoper_for(line_item.order.distributor_id).scope(line_item.variant) + line_item.variant.sku + end, + + proc { |line_items| line_items.first.order.order_cycle&.name }, + proc { |line_items| + payment = line_items.first.order.payments.first + payment&.payment_method&.name + }, + proc { |line_items| + distributor = line_items.first.order.distributor + user = line_items.first.order.user + user&.customer_of(distributor)&.code + }, + proc { |line_items| + distributor = line_items.first.order.distributor + user = line_items.first.order.user + user&.customer_of(distributor)&.tags&.join(', ') + }, + + proc { |line_items| line_items.first.order.bill_address&.address1 }, + proc { |line_items| line_items.first.order.bill_address&.address2 }, + proc { |line_items| line_items.first.order.bill_address&.city }, + proc { |line_items| line_items.first.order.bill_address&.zipcode }, + proc { |line_items| line_items.first.order.bill_address&.state }, + proc { |line_items| line_items.first.order.number }, + proc { |line_items| line_items.first.order.completed_at.strftime("%F %T") }, + ] + end + # rubocop:enable Metrics/AbcSize + # rubocop:enable Metrics/MethodLength + # rubocop:enable Metrics/PerceivedComplexity + + def line_item_includes + [{ variant: [{ option_values: :option_type }, { product: :supplier }], + order: [:bill_address, :ship_address, :order_cycle, :adjustments, :payments, + :user, :distributor, :shipments] }] + end + + private + + def shipping_method(line_items) + shipping_rates = line_items.first.order.shipments.first&.shipping_rates + + return unless shipping_rates + + shipping_rate = shipping_rates.find(&:selected) || shipping_rates.first + shipping_rate.try(:shipping_method) + end + end + end + end +end +# rubocop:enable Metrics/ClassLength diff --git a/lib/reporting/reports/orders_and_fulfillment/default_report.rb b/lib/reporting/reports/orders_and_fulfillment/default_report.rb new file mode 100644 index 0000000000..3bde81eca1 --- /dev/null +++ b/lib/reporting/reports/orders_and_fulfillment/default_report.rb @@ -0,0 +1,66 @@ +# frozen_string_literal: true + +module Reporting + module Reports + module OrdersAndFulfillment + class DefaultReport + delegate :line_item_name, :supplier_name, :product_name, :line_items_name, to: :context + + def initialize(context) + @context = context + end + + def table_headers + [ + I18n.t(:report_header_producer), + I18n.t(:report_header_product), + I18n.t(:report_header_variant), + I18n.t(:report_header_quantity), + I18n.t(:report_header_curr_cost_per_unit), + I18n.t(:report_header_total_cost), + I18n.t(:report_header_status), + I18n.t(:report_header_incoming_transport) + ] + end + + def rules + [ + { + group_by: proc { |line_item| line_item.variant.product.supplier }, + sort_by: proc { |supplier| supplier.name } + }, + { + group_by: proc { |line_item| line_item.variant.product }, + sort_by: proc { |product| product.name } + }, + { + group_by: line_item_name, + sort_by: proc { |full_name| full_name } + } + ] + end + + def columns + [ + supplier_name, + product_name, + line_items_name, + proc { |line_items| line_items.to_a.sum(&:quantity) }, + proc { |line_items| line_items.first.price }, + proc { |line_items| line_items.sum { |li| li.quantity * li.price } }, + proc { |_line_items| "" }, + proc { |_line_items| I18n.t(:report_header_incoming_transport) } + ] + end + + def line_item_includes + [] + end + + private + + attr_reader :context + end + end + end +end diff --git a/lib/reporting/reports/orders_and_fulfillment/distributor_totals_by_supplier_report.rb b/lib/reporting/reports/orders_and_fulfillment/distributor_totals_by_supplier_report.rb new file mode 100644 index 0000000000..88bf7ebee5 --- /dev/null +++ b/lib/reporting/reports/orders_and_fulfillment/distributor_totals_by_supplier_report.rb @@ -0,0 +1,80 @@ +# frozen_string_literal: true + +module Reporting + module Reports + module OrdersAndFulfillment + class DistributorTotalsBySupplierReport + REPORT_TYPE = "order_cycle_distributor_totals_by_supplier" + + attr_reader :context + + def initialize(context) + @context = context + end + + def table_headers + [I18n.t(:report_header_hub), I18n.t(:report_header_producer), + I18n.t(:report_header_product), I18n.t(:report_header_variant), + I18n.t(:report_header_quantity), I18n.t(:report_header_curr_cost_per_unit), + I18n.t(:report_header_total_cost), I18n.t(:report_header_total_shipping_cost), + I18n.t(:report_header_shipping_method)] + end + + # rubocop:disable Metrics/AbcSize + # rubocop:disable Metrics/MethodLength + def rules + [ + { + group_by: proc { |line_item| line_item.order.distributor }, + sort_by: proc { |distributor| distributor.name }, + summary_columns: [ + proc { |_line_items| "" }, + proc { |_line_items| I18n.t('admin.reports.total') }, + proc { |_line_items| "" }, + proc { |_line_items| "" }, + proc { |_line_items| "" }, + proc { |_line_items| "" }, + proc { |line_items| line_items.sum(&:amount) }, + proc { |line_items| line_items.map(&:order).uniq.sum(&:ship_total) }, + proc { |_line_items| "" } + ] + }, + { + group_by: proc { |line_item| line_item.variant.product.supplier }, + sort_by: proc { |supplier| supplier.name } + }, + { + group_by: proc { |line_item| line_item.variant.product }, + sort_by: proc { |product| product.name } + }, + { + group_by: proc { |line_item| line_item.variant.full_name }, + sort_by: proc { |full_name| full_name } + } + ] + end + # rubocop:enable Metrics/AbcSize + # rubocop:enable Metrics/MethodLength + + # rubocop:disable Metrics/AbcSize + def columns + [proc { |line_items| line_items.first.order.distributor.name }, + proc { |line_items| line_items.first.variant.product.supplier.name }, + proc { |line_items| line_items.first.variant.product.name }, + proc { |line_items| line_items.first.variant.full_name }, + proc { |line_items| line_items.to_a.sum(&:quantity) }, + proc { |line_items| line_items.first.price }, + proc { |line_items| line_items.sum(&:amount) }, + proc { |_line_items| "" }, + proc { |_line_items| I18n.t(:report_header_shipping_method) }] + end + # rubocop:enable Metrics/AbcSize + + def line_item_includes + [{ order: [:distributor, :adjustments, { shipments: { shipping_rates: :shipping_method } }], + variant: [{ option_values: :option_type }, { product: :supplier }] }] + end + end + end + end +end diff --git a/lib/reporting/reports/orders_and_fulfillment/orders_and_fulfillment_report.rb b/lib/reporting/reports/orders_and_fulfillment/orders_and_fulfillment_report.rb new file mode 100644 index 0000000000..27a5bfa1e5 --- /dev/null +++ b/lib/reporting/reports/orders_and_fulfillment/orders_and_fulfillment_report.rb @@ -0,0 +1,116 @@ +# frozen_string_literal: true + +include Spree::ReportsHelper + +module Reporting + module Reports + module OrdersAndFulfillment + class OrdersAndFulfillmentReport + attr_reader :options, :report_type + + delegate :table_headers, :rules, :columns, to: :report + + def initialize(user, options = {}, render_table = false) + @user = user + @options = options + @report_type = options[:report_subtype] + @render_table = render_table + @variant_scopers_by_distributor_id = {} + end + + def search + report_line_items.orders + end + + def table_items + return [] unless @render_table + + report_line_items.list(report.line_item_includes) + end + + def table_rows + order_grouper = Reporting::OrderGrouper.new report.rules, report.columns, report + order_grouper.table(table_items) + end + + def line_item_name + proc { |line_item| line_item.variant.full_name } + end + + def line_items_name + proc { |line_items| line_items.first.variant.full_name } + end + + def supplier_name + proc { |line_items| line_items.first.variant.product.supplier.name } + end + + def product_name + proc { |line_items| line_items.first.variant.product.name } + end + + def total_units(line_items) + return " " if not_all_have_unit?(line_items) + + total_units = line_items.sum do |li| + product = li.variant.product + li.quantity * li.unit_value / scale_factor(product) + end + + total_units.round(3) + end + + def variant_scoper_for(distributor_id) + @variant_scopers_by_distributor_id[distributor_id] ||= + OpenFoodNetwork::ScopeVariantToHub.new( + distributor_id, + report_variant_overrides[distributor_id] || {}, + ) + end + + private + + def report + @report ||= report_klass.new(self) + end + + def report_klass + case report_type + when SupplierTotalsReport::REPORT_TYPE then SupplierTotalsReport + when SupplierTotalsByDistributorReport::REPORT_TYPE then SupplierTotalsByDistributorReport + when DistributorTotalsBySupplierReport::REPORT_TYPE then DistributorTotalsBySupplierReport + when CustomerTotalsReport::REPORT_TYPE then CustomerTotalsReport + else + DefaultReport + end + end + + def not_all_have_unit?(line_items) + line_items.map { |li| li.unit_value.nil? }.any? + end + + def scale_factor(product) + product.variant_unit == 'weight' ? 1000 : 1 + end + + def order_permissions + return @order_permissions unless @order_permissions.nil? + + @order_permissions = ::Permissions::Order.new(@user, options[:q]) + end + + def report_line_items + @report_line_items ||= Reporting::LineItems.new(order_permissions, options) + end + + def report_variant_overrides + @report_variant_overrides ||= + VariantOverridesIndexed.new( + order_permissions.visible_line_items.select('DISTINCT variant_id'), + report_line_items.orders.result.select('DISTINCT distributor_id'), + ).indexed + end + end + end + end +end diff --git a/lib/reporting/reports/orders_and_fulfillment/supplier_totals_by_distributor_report.rb b/lib/reporting/reports/orders_and_fulfillment/supplier_totals_by_distributor_report.rb new file mode 100644 index 0000000000..275ab9b0a8 --- /dev/null +++ b/lib/reporting/reports/orders_and_fulfillment/supplier_totals_by_distributor_report.rb @@ -0,0 +1,79 @@ +# frozen_string_literal: true + +module Reporting + module Reports + module OrdersAndFulfillment + class SupplierTotalsByDistributorReport + REPORT_TYPE = "order_cycle_supplier_totals_by_distributor" + + attr_reader :context + + delegate :supplier_name, to: :context + + def initialize(context) + @context = context + end + + def table_headers + [I18n.t(:report_header_producer), I18n.t(:report_header_product), + I18n.t(:report_header_variant), I18n.t(:report_header_to_hub), + I18n.t(:report_header_quantity), I18n.t(:report_header_curr_cost_per_unit), + I18n.t(:report_header_total_cost), I18n.t(:report_header_shipping_method)] + end + + # rubocop:disable Metrics/AbcSize + # rubocop:disable Metrics/MethodLength + def rules + [ + { + group_by: proc { |line_item| line_item.variant.product.supplier }, + sort_by: proc { |supplier| supplier.name } + }, + { + group_by: proc { |line_item| line_item.variant.product }, + sort_by: proc { |product| product.name } + }, + { + group_by: proc { |line_item| line_item.variant.full_name }, + sort_by: proc { |full_name| full_name }, + summary_columns: [ + proc { |_line_items| "" }, + proc { |_line_items| "" }, + proc { |_line_items| "" }, + proc { |_line_items| I18n.t('admin.reports.total') }, + proc { |_line_items| "" }, + proc { |_line_items| "" }, + proc { |line_items| line_items.sum(&:amount) }, + proc { |_line_items| "" } + ] + }, + { + group_by: proc { |line_item| line_item.order.distributor }, + sort_by: proc { |distributor| distributor.name } + } + ] + end + # rubocop:enable Metrics/AbcSize + # rubocop:enable Metrics/MethodLength + + def columns + [ + supplier_name, + proc { |line_items| line_items.first.variant.product.name }, + proc { |line_items| line_items.first.variant.full_name }, + proc { |line_items| line_items.first.order.distributor.name }, + proc { |line_items| line_items.to_a.sum(&:quantity) }, + proc { |line_items| line_items.first.price }, + proc { |line_items| line_items.sum(&:amount) }, + proc { |_line_items| I18n.t(:report_header_shipping_method) } + ] + end + + def line_item_includes + [{ order: :distributor, + variant: [{ option_values: :option_type }, { product: :supplier }] }] + end + end + end + end +end diff --git a/lib/reporting/reports/orders_and_fulfillment/supplier_totals_report.rb b/lib/reporting/reports/orders_and_fulfillment/supplier_totals_report.rb new file mode 100644 index 0000000000..5e4f8c7ffe --- /dev/null +++ b/lib/reporting/reports/orders_and_fulfillment/supplier_totals_report.rb @@ -0,0 +1,62 @@ +# frozen_string_literal: true + +module Reporting + module Reports + module OrdersAndFulfillment + class SupplierTotalsReport + REPORT_TYPE = "order_cycle_supplier_totals" + + attr_reader :context + + delegate :supplier_name, :product_name, :line_items_name, :total_units, to: :context + + def initialize(context) + @context = context + end + + def table_headers + [I18n.t(:report_header_producer), I18n.t(:report_header_product), + I18n.t(:report_header_variant), I18n.t(:report_header_quantity), + I18n.t(:report_header_total_units), I18n.t(:report_header_curr_cost_per_unit), + I18n.t(:report_header_total_cost), I18n.t(:report_header_status), + I18n.t(:report_header_incoming_transport)] + end + + def rules + [ + { + group_by: proc { |line_item| line_item.variant.product.supplier }, + sort_by: proc { |supplier| supplier.name } + }, + { + group_by: proc { |line_item| line_item.variant.product }, + sort_by: proc { |product| product.name } + }, + { + group_by: proc { |line_item| line_item.variant.full_name }, + sort_by: proc { |full_name| full_name } + } + ] + end + + def columns + [ + supplier_name, + product_name, + line_items_name, + proc { |line_items| line_items.to_a.sum(&:quantity) }, + proc { |line_items| total_units(line_items) }, + proc { |line_items| line_items.first.price }, + proc { |line_items| line_items.sum(&:amount) }, + proc { |_line_items| "" }, + proc { |_line_items| I18n.t(:report_header_incoming_transport) } + ] + end + + def line_item_includes + [{ variant: [{ option_values: :option_type }, { product: :supplier }] }] + end + end + end + end +end diff --git a/lib/reporting/reports/payments/payments_report.rb b/lib/reporting/reports/payments/payments_report.rb new file mode 100644 index 0000000000..3d2a21a6b3 --- /dev/null +++ b/lib/reporting/reports/payments/payments_report.rb @@ -0,0 +1,150 @@ +# frozen_string_literal: true + +module Reporting + module Reports + module Payments + class PaymentsReport + attr_reader :params + + def initialize(user, params = {}, render_table = false) + @params = params + @user = user + @render_table = render_table + end + + def table_headers + case params[:report_subtype] + when "payments_by_payment_type" + I18n.t(:report_header_payment_type) + [I18n.t(:report_header_payment_state), I18n.t(:report_header_distributor), I18n.t(:report_header_payment_type), + I18n.t(:report_header_total_price, currency: currency_symbol)] + when "itemised_payment_totals" + [I18n.t(:report_header_payment_state), I18n.t(:report_header_distributor), + I18n.t(:report_header_product_total_price, currency: currency_symbol), + I18n.t(:report_header_shipping_total_price, currency: currency_symbol), + I18n.t(:report_header_outstanding_balance_price, currency: currency_symbol), + I18n.t(:report_header_total_price, currency: currency_symbol)] + when "payment_totals" + [I18n.t(:report_header_payment_state), I18n.t(:report_header_distributor), + I18n.t(:report_header_product_total_price, currency: currency_symbol), + I18n.t(:report_header_shipping_total_price, currency: currency_symbol), + I18n.t(:report_header_total_price, currency: currency_symbol), + I18n.t(:report_header_eft_price, currency: currency_symbol), + I18n.t(:report_header_paypal_price, currency: currency_symbol), + I18n.t(:report_header_outstanding_balance_price, currency: currency_symbol)] + else + [I18n.t(:report_header_payment_state), I18n.t(:report_header_distributor), I18n.t(:report_header_payment_type), + I18n.t(:report_header_total_price, currency: currency_symbol)] + end + end + + def search + Spree::Order.complete.not_state(:canceled).managed_by(@user).ransack(params[:q]) + end + + def table_items + return [] unless @render_table + + orders = search.result + payments = orders.includes(:payments).map do |order| + order.payments.select(&:completed?) + end.flatten + + case params[:report_subtype] + when "payments_by_payment_type" + payments + when "itemised_payment_totals" + orders + when "payment_totals" + orders + else + payments + end + end + + def table_rows + order_grouper = Reporting::OrderGrouper.new rules, columns, self + order_grouper.table(table_items) + end + + def rules + case params[:report_subtype] + when "payments_by_payment_type" + [{ group_by: proc { |payment| payment.order.payment_state }, + sort_by: proc { |payment_state| payment_state } }, + { group_by: proc { |payment| payment.order.distributor }, + sort_by: proc { |distributor| distributor.name } }, + { group_by: proc { |payment| + Spree::PaymentMethod.unscoped { + payment.payment_method + } + }, + sort_by: proc { |method| method.name } }] + when "itemised_payment_totals" + [{ group_by: proc { |order| order.payment_state }, + sort_by: proc { |payment_state| payment_state } }, + { group_by: proc { |order| order.distributor }, + sort_by: proc { |distributor| distributor.name } }] + when "payment_totals" + [{ group_by: proc { |order| order.payment_state }, + sort_by: proc { |payment_state| payment_state } }, + { group_by: proc { |order| order.distributor }, + sort_by: proc { |distributor| distributor.name } }] + else + [{ group_by: proc { |payment| payment.order.payment_state }, + sort_by: proc { |payment_state| payment_state } }, + { group_by: proc { |payment| payment.order.distributor }, + sort_by: proc { |distributor| distributor.name } }, + { group_by: proc { |payment| payment.payment_method }, + sort_by: proc { |method| method.name } }] + end + end + + def columns + case params[:report_subtype] + when "payments_by_payment_type" + [proc { |payments| payments.first.order.payment_state }, + proc { |payments| payments.first.order.distributor.name }, + proc { |payments| payments.first.payment_method.name }, + proc { |payments| payments.sum(&:amount) }] + when "itemised_payment_totals" + [proc { |orders| orders.first.payment_state }, + proc { |orders| orders.first.distributor.name }, + proc { |orders| orders.to_a.sum(&:item_total) }, + proc { |orders| orders.sum(&:ship_total) }, + proc { |orders| orders.sum{ |order| order.outstanding_balance.to_f } }, + proc { |orders| orders.map(&:total).sum }] + when "payment_totals" + [proc { |orders| orders.first.payment_state }, + proc { |orders| orders.first.distributor.name }, + proc { |orders| orders.to_a.sum(&:item_total) }, + proc { |orders| orders.sum(&:ship_total) }, + proc { |orders| orders.map(&:total).sum }, + proc { |orders| + orders.sum { |o| + o.payments.select { |payment| + payment.completed? && + (payment.payment_method.name.to_s.include? "EFT") + }.sum(&:amount) + } + }, + proc { |orders| + orders.sum { |o| + o.payments.select { |payment| + payment.completed? && + (payment.payment_method.name.to_s.include? "PayPal") + }.sum(&:amount) + } + }, + proc { |orders| orders.sum{ |order| order.outstanding_balance.to_f } }] + else + [proc { |payments| payments.first.order.payment_state }, + proc { |payments| payments.first.order.distributor.name }, + proc { |payments| payments.first.payment_method.name }, + proc { |payments| payments.sum(&:amount) }] + end + end + end + end + end +end diff --git a/lib/reporting/reports/products_and_inventory/lettuce_share_report.rb b/lib/reporting/reports/products_and_inventory/lettuce_share_report.rb new file mode 100644 index 0000000000..2c63e04a8c --- /dev/null +++ b/lib/reporting/reports/products_and_inventory/lettuce_share_report.rb @@ -0,0 +1,93 @@ +# frozen_string_literal: true + +# require 'variant_units/option_value_namer' + +module Reporting + module Reports + module ProductsAndInventory + class LettuceShareReport + attr_reader :context + + delegate :variants, :render_table, to: :context + + def initialize(context) + @context = context + end + + def table_headers + # NOTE: These are NOT to be translated, they need to be in this exact format to work with LettucShare + [ + "PRODUCT", + "Description", + "Qty", + "Pack Size", + "Unit", + "Unit Price", + "Total", + "GST incl.", + "Grower and growing method", + "Taxon" + ] + end + + def table_rows + return [] unless render_table + + variants.select(&:in_stock?) + .map do |variant| + [ + variant.product.name, + variant.full_name, + '', + VariantUnits::OptionValueNamer.new(variant).value, + VariantUnits::OptionValueNamer.new(variant).unit, + variant.price, + '', + gst(variant), + grower_and_method(variant), + variant.product.primary_taxon.name + ] + end + end + + private + + def gst(variant) + tax_category = variant.product.tax_category + if tax_category && tax_category.tax_rates.present? + tax_rate = tax_category.tax_rates.first + line_item = mock_line_item(variant) + tax_rate.calculator.compute line_item + else + 0 + end + end + + def mock_line_item(variant) + line_item = Spree::LineItem.new quantity: 1 + line_item.define_singleton_method(:product) { variant.product } + line_item.define_singleton_method(:price) { variant.price } + line_item + end + + def grower_and_method(variant) + cert = certification(variant) + + result = producer_name(variant) + result += " (#{cert})" if cert.present? + result + end + + def producer_name(variant) + variant.product.supplier.name + end + + def certification(variant) + variant.product.properties_including_inherited.map do |p| + "#{p[:name]} - #{p[:value]}" + end.join(', ') + end + end + end + end +end diff --git a/lib/reporting/reports/products_and_inventory/products_and_inventory_default_report.rb b/lib/reporting/reports/products_and_inventory/products_and_inventory_default_report.rb new file mode 100644 index 0000000000..a733fe24ea --- /dev/null +++ b/lib/reporting/reports/products_and_inventory/products_and_inventory_default_report.rb @@ -0,0 +1,57 @@ +# frozen_string_literal: true + +module Reporting + module Reports + module ProductsAndInventory + class ProductsAndInventoryDefaultReport + attr_reader :context + + delegate :variants, :render_table, to: :context + + def initialize(context) + @context = context + end + + def table_headers + [ + I18n.t(:report_header_supplier), + I18n.t(:report_header_producer_suburb), + I18n.t(:report_header_product), + I18n.t(:report_header_product_properties), + I18n.t(:report_header_taxons), + I18n.t(:report_header_variant_value), + I18n.t(:report_header_price), + I18n.t(:report_header_group_buy_unit_quantity), + I18n.t(:report_header_amount), + I18n.t(:report_header_sku) + ] + end + + def table_rows + return [] unless render_table + + variants.map do |variant| + [ + variant.product.supplier.name, + variant.product.supplier.address.city, + variant.product.name, + variant.product.properties.map(&:name).join(", "), + variant.product.taxons.map(&:name).join(", "), + variant.full_name, + variant.price, + variant.product.group_buy_unit_size, + "", + sku_for(variant) + ] + end + end + + def sku_for(variant) + return variant.sku if variant.sku.present? + + variant.product.sku + end + end + end + end +end diff --git a/lib/reporting/reports/products_and_inventory/products_and_inventory_report.rb b/lib/reporting/reports/products_and_inventory/products_and_inventory_report.rb new file mode 100644 index 0000000000..b6f972f3b7 --- /dev/null +++ b/lib/reporting/reports/products_and_inventory/products_and_inventory_report.rb @@ -0,0 +1,102 @@ +# frozen_string_literal: true + +require 'open_food_network/scope_variant_to_hub' + +module Reporting + module Reports + module ProductsAndInventory + class ProductsAndInventoryReport + attr_reader :params, :render_table + + delegate :table_rows, :table_headers, :rules, :columns, :sku_for, to: :report + + def initialize(user, params = {}, render_table = false) + @user = user + @params = params + @render_table = render_table + end + + def report + @report ||= report_klass.new(self) + end + + def report_type + params[:report_subtype] + end + + def report_klass + if report_type == 'lettuce_share' + LettuceShareReport + else + ProductsAndInventoryDefaultReport + end + end + + def permissions + @permissions ||= OpenFoodNetwork::Permissions.new(@user) + end + + def visible_products + @visible_products ||= permissions.visible_products + end + + def variants + filter(child_variants) + end + + def child_variants + Spree::Variant. + where(is_master: false). + includes(option_values: :option_type). + joins(:product). + merge(visible_products). + order('spree_products.name') + end + + def filter(variants) + filter_on_hand filter_to_distributor filter_to_order_cycle filter_to_supplier variants + end + + # Using the `in_stock?` method allows overrides by distributors. + def filter_on_hand(variants) + if report_type == 'inventory' + variants.select(&:in_stock?) + else + variants + end + end + + def filter_to_supplier(variants) + if params[:supplier_id].to_i > 0 + variants.where("spree_products.supplier_id = ?", params[:supplier_id]) + else + variants + end + end + + def filter_to_distributor(variants) + if params[:distributor_id].to_i > 0 + distributor = Enterprise.find params[:distributor_id] + scoper = OpenFoodNetwork::ScopeVariantToHub.new(distributor) + variants.in_distributor(distributor).each { |v| scoper.scope(v) } + else + variants + end + end + + def filter_to_order_cycle(variants) + if params[:order_cycle_id].to_i > 0 + order_cycle = OrderCycle.find params[:order_cycle_id] + variant_ids = Exchange.in_order_cycle(order_cycle). + joins("INNER JOIN exchange_variants ON exchanges.id = exchange_variants.exchange_id"). + select("DISTINCT exchange_variants.variant_id") + + variants.where("spree_variants.id IN (#{variant_ids.to_sql})") + else + variants + end + end + end + end + end +end diff --git a/lib/reporting/reports/sales_tax/sales_tax_report.rb b/lib/reporting/reports/sales_tax/sales_tax_report.rb new file mode 100644 index 0000000000..9bf0248415 --- /dev/null +++ b/lib/reporting/reports/sales_tax/sales_tax_report.rb @@ -0,0 +1,114 @@ +# frozen_string_literal: true + +module Reporting + module Reports + module SalesTax + class SalesTaxReport + include Spree::ReportsHelper + attr_accessor :user, :params + + def initialize(user, params, render_table) + @user = user + @params = params + @render_table = render_table + end + + def table_headers + case params[:report_subtype] + when "tax_rates" + [I18n.t(:report_header_order_number), + I18n.t(:report_header_total_excl_vat, currency_symbol: currency_symbol)] + + relevant_rates.map { |rate| + "%.1f%% (%s)" % [rate.amount.to_f * 100, currency_symbol] + } + + [I18n.t(:report_header_total_tax, currency_symbol: currency_symbol), + I18n.t(:report_header_total_incl_vat, currency_symbol: currency_symbol)] + else + [I18n.t(:report_header_order_number), + I18n.t(:report_header_date), + I18n.t(:report_header_items), + I18n.t(:report_header_items_total, currency_symbol: currency_symbol), + I18n.t(:report_header_taxable_items_total, currency_symbol: currency_symbol), + I18n.t(:report_header_sales_tax, currency_symbol: currency_symbol), + I18n.t(:report_header_delivery_charge, currency_symbol: currency_symbol), + I18n.t(:report_header_tax_on_delivery, currency_symbol: currency_symbol), + I18n.t(:report_header_tax_on_fees, currency_symbol: currency_symbol), + I18n.t(:report_header_total_tax, currency_symbol: currency_symbol), + I18n.t(:report_header_customer), + I18n.t(:report_header_distributor)] + end + end + + def search + permissions = ::Permissions::Order.new(user) + permissions.editable_orders.complete.not_state(:canceled).ransack(params[:q]) + end + + def orders + search.result + end + + def table_rows + return [] unless @render_table + + case params[:report_subtype] + when "tax_rates" + orders.map do |order| + [order.number, order.total - order.total_tax] + + relevant_rates.map { |rate| + OrderTaxAdjustmentsFetcher.new(order).totals.fetch(rate, 0) + } + [order.total_tax, order.total] + end + else + orders.map do |order| + totals = totals_of order.line_items + shipping_cost = shipping_cost_for order + + [order.number, order.completed_at.strftime("%F %T"), totals[:items], totals[:items_total], + totals[:taxable_total], totals[:sales_tax], shipping_cost, order.shipping_tax, order.enterprise_fee_tax, order.total_tax, + order.bill_address.full_name, order.distributor&.name] + end + end + end + + private + + def relevant_rates + return @relevant_rates unless @relevant_rates.nil? + + @relevant_rates = Spree::TaxRate.distinct + end + + def totals_of(line_items) + totals = { items: 0, items_total: 0.0, taxable_total: 0.0, sales_tax: 0.0 } + + line_items.each do |line_item| + totals[:items] += line_item.quantity + totals[:items_total] += line_item.amount + + sales_tax = tax_included_in line_item + + if sales_tax > 0 + totals[:taxable_total] += line_item.amount + totals[:sales_tax] += sales_tax + end + end + + totals.each_pair do |k, _v| + totals[k] = totals[k].round(2) + end + + totals + end + + def shipping_cost_for(order) + order.shipments.first&.cost || 0.0 + end + + def tax_included_in(line_item) + line_item.adjustments.tax.inclusive.sum(:amount) + end + end + end + end +end diff --git a/lib/reporting/reports/users_and_enterprises/users_and_enterprises_report.rb b/lib/reporting/reports/users_and_enterprises/users_and_enterprises_report.rb new file mode 100644 index 0000000000..34c86e97f2 --- /dev/null +++ b/lib/reporting/reports/users_and_enterprises/users_and_enterprises_report.rb @@ -0,0 +1,143 @@ +# frozen_string_literal: true + +module Reporting + module Reports + module UsersAndEnterprises + class UsersAndEnterprisesReport + attr_reader :params + + def initialize(user, params = {}, compile_table = false) + @user = user + @params = params + @compile_table = compile_table + + # Convert arrays of ids to comma delimited strings + if @params[:enterprise_id_in].is_a? Array + @params[:enterprise_id_in] = @params[:enterprise_id_in].join(',') + end + @params[:user_id_in] = @params[:user_id_in].join(',') if @params[:user_id_in].is_a? Array + end + + def table_headers + [ + I18n.t(:report_header_user), + I18n.t(:report_header_relationship), + I18n.t(:report_header_enterprise), + I18n.t(:report_header_is_producer), + I18n.t(:report_header_sells), + I18n.t(:report_header_visible), + I18n.t(:report_header_confirmation_date), + ] + end + + def table_rows + return [] unless @compile_table + + users_and_enterprises.map do |uae| + [ + uae["user_email"], + uae["relationship_type"], + uae["name"], + to_bool(uae["is_primary_producer"]), + uae["sells"], + uae["visible"], + to_local_datetime(uae["created_at"]) + ] + end + end + + def owners_and_enterprises + query = Enterprise.joins("LEFT JOIN spree_users AS owner ON enterprises.owner_id = owner.id") + .where("enterprises.id IS NOT NULL") + + query = filter_by_int_list_if_present(query, "enterprises.id", params[:enterprise_id_in]) + query = filter_by_int_list_if_present(query, "owner.id", params[:user_id_in]) + + query_helper(query, :owner, :owns) + end + + def managers_and_enterprises + query = Enterprise + .joins("LEFT JOIN enterprise_roles ON enterprises.id = enterprise_roles.enterprise_id") + .joins("LEFT JOIN spree_users AS managers ON enterprise_roles.user_id = managers.id") + .where("enterprise_id IS NOT NULL") + .where("user_id IS NOT NULL") + + query = filter_by_int_list_if_present(query, "enterprise_id", params[:enterprise_id_in]) + query = filter_by_int_list_if_present(query, "user_id", params[:user_id_in]) + + query_helper(query, :managers, :manages) + end + + def query_helper(query, email_user, relationship_type) + query.order("enterprises.created_at DESC") + .select(["enterprises.name", + "enterprises.sells", + "enterprises.visible", + "enterprises.is_primary_producer", + "enterprises.created_at", + "#{email_user}.email AS user_email"]) + .to_a + .map { |x| + { + name: x.name, + sells: x.sells, + visible: (x.visible ? 't' : 'f'), + is_primary_producer: (x.is_primary_producer ? 't' : 'f'), + created_at: x.created_at.utc.iso8601, + relationship_type: relationship_type, + user_email: x.user_email + }.stringify_keys + } + end + + def users_and_enterprises + sort( owners_and_enterprises.concat(managers_and_enterprises) ) + end + + def filter_by_int_list_if_present(query, filtered_field_name, int_list) + if int_list.present? + query = query.where("#{filtered_field_name} IN (?)", split_int_list(int_list)) + end + query + end + + def split_int_list(int_list) + int_list.split(',').map(&:to_i) + end + + def sort(results) + results.sort do |a, b| + if a["created_at"].nil? || b["created_at"].nil? + [(a["created_at"].nil? ? 0 : 1), a["name"], b["relationship_type"], + a["user_email"]] <=> + [(b["created_at"].nil? ? 0 : 1), b["name"], a["relationship_type"], b["user_email"]] + else + [ + DateTime.parse(b["created_at"]).in_time_zone, + a["name"], + b["relationship_type"], + a["user_email"] + ] <=> [ + DateTime.parse(a["created_at"]).in_time_zone, + b["name"], + a["relationship_type"], + b["user_email"] + ] + end + end + end + + def to_bool(value) + ActiveRecord::Type::Boolean.new.cast(value) + end + + def to_local_datetime(date) + return "" if date.nil? + + date.to_datetime.in_time_zone.strftime "%Y-%m-%d %H:%M" + end + end + end + end +end diff --git a/lib/reporting/reports/xero_invoices/xero_invoices_report.rb b/lib/reporting/reports/xero_invoices/xero_invoices_report.rb new file mode 100644 index 0000000000..186dd2e830 --- /dev/null +++ b/lib/reporting/reports/xero_invoices/xero_invoices_report.rb @@ -0,0 +1,239 @@ +# frozen_string_literal: true + +module Reporting + module Reports + module XeroInvoices + class XeroInvoicesReport + def initialize(user, opts = {}, compile_table = false) + @user = user + + @opts = opts. + symbolize_keys. + reject { |_k, v| v.blank? }. + reverse_merge( report_subtype: 'summary', + invoice_date: Time.zone.today, + due_date: Time.zone.today + 1.month, + account_code: 'food sales' ) + @compile_table = compile_table + end + + def table_headers + # NOTE: These are NOT to be translated, they need to be in this exact format to work with Xero + %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 search + permissions = ::Permissions::Order.new(@user) + permissions.editable_orders.complete.not_state(:canceled).ransack(@opts[:q]) + end + + def orders + search.result.reorder('id DESC') + end + + def table_rows + return [] unless @compile_table + + rows = [] + + orders.each_with_index do |order, i| + invoice_number = invoice_number_for(order, i) + rows += detail_rows_for_order(order, invoice_number, @opts) if detail? + rows += summary_rows_for_order(order, invoice_number, @opts) + end + + rows.compact + end + + private + + def report_options + @opts.merge(line_item_includes: line_item_includes) + end + + def line_item_includes + [:bill_address, :adjustments, + { line_items: { variant: [{ option_values: :option_type }, { product: :supplier }] } }] + end + + def detail_rows_for_order(order, invoice_number, opts) + rows = [] + + rows += line_item_detail_rows(order, invoice_number, opts) + rows += adjustment_detail_rows(order, invoice_number, opts) + + rows + end + + def line_item_detail_rows(order, invoice_number, opts) + order.line_items.map do |line_item| + line_item_detail_row(line_item, invoice_number, opts) + end + end + + def line_item_detail_row(line_item, invoice_number, opts) + row(line_item.order, + line_item.variant.sku, + line_item.product_and_full_name, + line_item.quantity.to_s, + line_item.price.to_s, + invoice_number, + tax_type(line_item), + opts) + end + + def adjustment_detail_rows(order, invoice_number, opts) + admin_adjustments(order).map do |adjustment| + adjustment_detail_row(adjustment, invoice_number, opts) + end + end + + def adjustment_detail_row(adjustment, invoice_number, opts) + row(adjustment_order(adjustment), + '', + adjustment.label, + 1, + adjustment.amount, + invoice_number, + tax_type(adjustment), + opts) + end + + def summary_rows_for_order(order, invoice_number, opts) + rows = [] + + rows += produce_summary_rows(order, invoice_number, opts) unless detail? + rows += fee_summary_rows(order, invoice_number, opts) + rows += shipping_summary_rows(order, invoice_number, opts) + rows += payment_summary_rows(order, invoice_number, opts) + rows += admin_adjustment_summary_rows(order, invoice_number, opts) unless detail? + + rows + end + + def produce_summary_rows(order, invoice_number, opts) + [summary_row(order, I18n.t(:report_header_total_untaxable_produce), total_untaxable_products(order), invoice_number, I18n.t(:report_header_gst_free_income), opts), + summary_row(order, I18n.t(:report_header_total_taxable_produce), + total_taxable_products(order), invoice_number, I18n.t(:report_header_gst_on_income), opts)] + end + + def fee_summary_rows(order, invoice_number, opts) + [summary_row(order, I18n.t(:report_header_total_untaxable_fees), total_untaxable_fees(order), invoice_number, I18n.t(:report_header_gst_free_income), opts), + summary_row(order, I18n.t(:report_header_total_taxable_fees), total_taxable_fees(order), + invoice_number, I18n.t(:report_header_gst_on_income), opts)] + end + + def shipping_summary_rows(order, invoice_number, opts) + [summary_row(order, I18n.t(:report_header_delivery_shipping_cost), total_shipping(order), + invoice_number, tax_on_shipping_s(order), opts)] + end + + def payment_summary_rows(order, invoice_number, opts) + [summary_row(order, I18n.t(:report_header_transaction_fee), total_transaction(order), + invoice_number, I18n.t(:report_header_gst_free_income), opts)] + end + + def admin_adjustment_summary_rows(order, invoice_number, opts) + [summary_row(order, I18n.t(:report_header_total_untaxable_admin), total_untaxable_admin_adjustments(order), invoice_number, I18n.t(:report_header_gst_free_income), opts), + summary_row(order, I18n.t(:report_header_total_taxable_admin), + total_taxable_admin_adjustments(order), invoice_number, I18n.t(:report_header_gst_on_income), opts)] + end + + def summary_row(order, description, amount, invoice_number, tax_type, opts = {}) + row order, '', description, '1', amount, invoice_number, tax_type, opts + end + + def row(order, sku, description, quantity, amount, invoice_number, tax_type, opts = {}) + return nil if amount == 0 + + [order.bill_address&.full_name, + order.email, + order.bill_address&.address1, + order.bill_address&.address2, + '', + '', + order.bill_address&.city, + order.bill_address&.state, + order.bill_address&.zipcode, + order.bill_address&.country&.name, + invoice_number, + order.number, + opts[:invoice_date], + opts[:due_date], + sku, + description, + quantity, + amount, + '', + opts[:account_code], + tax_type, + '', + '', + '', + '', + Spree::Config.currency, + '', + order.paid? ? I18n.t(:y) : I18n.t(:n)] + end + + def admin_adjustments(order) + order.adjustments.admin + end + + def adjustment_order(adjustment) + adjustment.adjustable.is_a?(Spree::Order) ? adjustment.adjustable : nil + end + + def invoice_number_for(order, idx) + @opts[:initial_invoice_number] ? @opts[:initial_invoice_number].to_i + idx : order.number + end + + def total_untaxable_products(order) + order.line_items.without_tax.to_a.sum(&:amount) + end + + def total_taxable_products(order) + order.line_items.with_tax.to_a.sum(&:amount) + end + + def total_untaxable_fees(order) + order.all_adjustments.enterprise_fee.where(tax_category: nil).sum(:amount) + end + + def total_taxable_fees(order) + order.all_adjustments.enterprise_fee.where.not(tax_category: nil).sum(:amount) + end + + def total_shipping(order) + order.all_adjustments.shipping.sum(:amount) + end + + def total_transaction(order) + order.all_adjustments.payment_fee.sum(:amount) + end + + def tax_on_shipping_s(order) + tax_on_shipping = order.shipments.sum("additional_tax_total + included_tax_total").positive? + tax_on_shipping ? I18n.t(:report_header_gst_on_income) : I18n.t(:report_header_gst_free_income) + end + + def total_untaxable_admin_adjustments(order) + order.adjustments.admin.where(tax_category: nil).sum(:amount) + end + + def total_taxable_admin_adjustments(order) + order.adjustments.admin.where.not(tax_category: nil).sum(:amount) + end + + def detail? + @opts[:report_subtype] == 'detailed' + end + + def tax_type(taxable) + taxable.has_tax? ? I18n.t(:report_header_gst_on_income) : I18n.t(:report_header_gst_free_income) + end + end + end + end +end diff --git a/spec/controllers/spree/admin/reports_controller_spec.rb b/spec/controllers/spree/admin/reports_controller_spec.rb index 48af5f02fe..50bb50cac4 100644 --- a/spec/controllers/spree/admin/reports_controller_spec.rb +++ b/spec/controllers/spree/admin/reports_controller_spec.rb @@ -237,7 +237,7 @@ describe Spree::Admin::ReportsController, type: :controller do end it "creates a ProductAndInventoryReport" do - expect(OpenFoodNetwork::ProductsAndInventoryReport).to receive(:new) + expect(Reporting::Reports::ProductsAndInventory::ProductsAndInventoryReport).to receive(:new) .with(@admin_user, { "test" => "foo", "controller" => "spree/admin/reports", "report" => {}, "action" => "products_and_inventory", "use_route" => "main_app" }, false) @@ -290,7 +290,7 @@ describe Spree::Admin::ReportsController, type: :controller do end it "creates a CustomersReport" do - expect(OpenFoodNetwork::CustomersReport).to receive(:new) + expect(Reporting::Reports::Customers::CustomersReport).to receive(:new) .with(@admin_user, { "test" => "foo", "controller" => "spree/admin/reports", "action" => "customers", "use_route" => "main_app", "report" => {} }, false) diff --git a/spec/lib/open_food_network/customers_report_spec.rb b/spec/lib/open_food_network/customers_report_spec.rb deleted file mode 100644 index e59633f2f5..0000000000 --- a/spec/lib/open_food_network/customers_report_spec.rb +++ /dev/null @@ -1,163 +0,0 @@ -# frozen_string_literal: true - -require 'spec_helper' -require 'open_food_network/customers_report' - -module OpenFoodNetwork - describe CustomersReport do - context "as a site admin" do - let(:user) do - user = create(:user) - user.spree_roles << Spree::Role.find_or_create_by!(name: 'admin') - user - end - subject { CustomersReport.new user, {}, true } - - describe "mailing list report" do - before do - allow(subject).to receive(:params).and_return(report_subtype: "mailing_list") - end - - it "returns headers for mailing_list" do - expect(subject.table_headers).to eq(["Email", "First Name", "Last Name", "Suburb"]) - end - - it "builds a table from a list of variants" do - order = double(:order, email: "test@test.com") - address = double(:billing_address, firstname: "Firsty", - lastname: "Lasty", city: "Suburbia") - allow(order).to receive(:billing_address).and_return address - allow(subject).to receive(:orders).and_return [order] - - expect(subject.table_rows).to eq([[ - "test@test.com", "Firsty", "Lasty", "Suburbia" - ]]) - end - end - - describe "addresses report" do - before do - allow(subject).to receive(:params).and_return(report_subtype: "addresses") - end - - it "returns headers for addresses" do - expect(subject.table_headers).to eq(["First Name", "Last Name", "Billing Address", "Email", - "Phone", "Hub", "Hub Address", "Shipping Method"]) - end - - it "builds a table from a list of variants" do - a = create(:address) - d = create(:distributor_enterprise) - o = create(:order, distributor: d, bill_address: a) - o.shipments << create(:shipment) - - allow(subject).to receive(:orders).and_return [o] - expect(subject.table_rows).to eq([[ - a.firstname, a.lastname, - [a.address1, a.address2, a.city].join(" "), - o.email, a.phone, d.name, - [d.address.address1, d.address.address2, d.address.city].join(" "), - o.shipping_method.name - ]]) - end - end - - describe "fetching orders" do - it "fetches completed orders" do - o1 = create(:order) - o2 = create(:order, completed_at: 1.day.ago) - expect(subject.orders).to eq([o2]) - end - - it "does not show cancelled orders" do - o1 = create(:order, state: "canceled", completed_at: 1.day.ago) - o2 = create(:order, completed_at: 1.day.ago) - expect(subject.orders).to eq([o2]) - end - end - end - - context "as an enterprise user" do - let(:user) do - user = create(:user) - user.spree_roles = [] - user.save! - user - end - - subject { CustomersReport.new user, {}, true } - - describe "fetching orders" do - let(:supplier) { create(:supplier_enterprise) } - let(:product) { create(:simple_product, supplier: supplier) } - let(:order) { create(:order, completed_at: 1.day.ago) } - - it "only shows orders managed by the current user" do - d1 = create(:distributor_enterprise) - d1.enterprise_roles.build(user: user).save - d2 = create(:distributor_enterprise) - d2.enterprise_roles.build(user: create(:user)).save - - o1 = create(:order, distributor: d1, completed_at: 1.day.ago) - o2 = create(:order, distributor: d2, completed_at: 1.day.ago) - - expect(subject).to receive(:filter).with([o1]).and_return([o1]) - expect(subject.orders).to eq([o1]) - end - - it "does not show orders through a hub that the current user does not manage" do - # Given a supplier enterprise with an order for one of its products - supplier.enterprise_roles.build(user: user).save - order.line_items << create(:line_item_with_shipment, product: product) - - # When I fetch orders, I should see no orders - expect(subject).to receive(:filter).with([]).and_return([]) - expect(subject.orders).to eq([]) - end - end - - describe "filtering orders" do - let(:orders) { Spree::Order.where(nil) } - let(:supplier) { create(:supplier_enterprise) } - - it "returns all orders sans-params" do - expect(subject.filter(orders)).to eq(orders) - end - - it "returns orders with a specific supplier" do - supplier = create(:supplier_enterprise) - supplier2 = create(:supplier_enterprise) - product1 = create(:simple_product, supplier: supplier) - product2 = create(:simple_product, supplier: supplier2) - order1 = create(:order) - order2 = create(:order) - order1.line_items << create(:line_item, product: product1) - order2.line_items << create(:line_item, product: product2) - - allow(subject).to receive(:params).and_return(supplier_id: supplier.id) - expect(subject.filter(orders)).to eq([order1]) - end - - it "filters to a specific distributor" do - d1 = create(:distributor_enterprise) - d2 = create(:distributor_enterprise) - order1 = create(:order, distributor: d1) - order2 = create(:order, distributor: d2) - - allow(subject).to receive(:params).and_return(distributor_id: d1.id) - expect(subject.filter(orders)).to eq([order1]) - end - - it "filters to a specific cycle" do - oc1 = create(:simple_order_cycle) - oc2 = create(:simple_order_cycle) - order1 = create(:order, order_cycle: oc1) - order2 = create(:order, order_cycle: oc2) - - allow(subject).to receive(:params).and_return(order_cycle_id: oc1.id) - expect(subject.filter(orders)).to eq([order1]) - end - end - end - end -end diff --git a/spec/lib/open_food_network/lettuce_share_report_spec.rb b/spec/lib/open_food_network/lettuce_share_report_spec.rb deleted file mode 100644 index a8e7da5b95..0000000000 --- a/spec/lib/open_food_network/lettuce_share_report_spec.rb +++ /dev/null @@ -1,90 +0,0 @@ -# frozen_string_literal: true - -require 'spec_helper' - -require 'open_food_network/products_and_inventory_report' - -module OpenFoodNetwork - describe LettuceShareReport do - let(:user) { create(:user) } - let(:base_report) { - ProductsAndInventoryReport.new(user, { report_subtype: 'lettuce_share' }, true) - } - let(:report) { base_report.report } - let(:variant) { create(:variant) } - - describe "grower and method" do - it "shows just the producer when there is no certification" do - allow(report).to receive(:producer_name) { "Producer" } - allow(report).to receive(:certification) { "" } - - expect(report.send(:grower_and_method, variant)).to eq("Producer") - end - - it "shows producer and certification when a certification is present" do - allow(report).to receive(:producer_name) { "Producer" } - allow(report).to receive(:certification) { "Method" } - - expect(report.send(:grower_and_method, variant)).to eq("Producer (Method)") - end - end - - describe "gst" do - it "handles tax category without rates" do - expect(report.send(:gst, variant)).to eq(0) - end - end - - describe "table" do - it "handles no items" do - expect(report.table_rows).to eq [] - end - - describe "lists" do - let(:variant2) { create(:variant) } - let(:variant3) { create(:variant) } - let(:variant4) { create(:variant, on_hand: 0, on_demand: true) } - let(:hub_address) { - create(:address, address1: "distributor address", city: 'The Shire', zipcode: "1234") - } - let(:hub) { create(:distributor_enterprise, address: hub_address) } - let(:variant2_override) { create(:variant_override, hub: hub, variant: variant2) } - let(:variant3_override) { - create(:variant_override, hub: hub, variant: variant3, count_on_hand: 0) - } - - it "all items" do - allow(base_report).to receive(:child_variants) { - Spree::Variant.where(id: [variant, variant2, variant3]) - } - expect(report.table_rows.count).to eq 3 - end - - it "only available items" do - variant.on_hand = 0 - allow(base_report).to receive(:child_variants) { - Spree::Variant.where(id: [variant, variant2, variant3, variant4]) - } - expect(report.table_rows.count).to eq 3 - end - - it "only available items considering overrides" do - create(:exchange, incoming: false, receiver_id: hub.id, - variants: [variant, variant2, variant3]) - # create the overrides - variant2_override - variant3_override - allow(base_report).to receive(:child_variants) { - Spree::Variant.where(id: [variant, variant2, variant3]) - } - allow(base_report).to receive(:params) { - { distributor_id: hub.id, report_subtype: 'lettuce_share' } - } - rows = report.table_rows - expect(rows.count).to eq 2 - expect(rows.map{ |row| row[0] }).to include variant.product.name, variant2.product.name - end - end - end - end -end diff --git a/spec/lib/open_food_network/order_cycle_management_report_spec.rb b/spec/lib/open_food_network/order_cycle_management_report_spec.rb deleted file mode 100644 index 56d7781c5f..0000000000 --- a/spec/lib/open_food_network/order_cycle_management_report_spec.rb +++ /dev/null @@ -1,213 +0,0 @@ -# frozen_string_literal: true - -require 'spec_helper' -require 'open_food_network/order_cycle_management_report' - -module OpenFoodNetwork - describe OrderCycleManagementReport do - context "as a site admin" do - subject { OrderCycleManagementReport.new(user, params, true) } - let(:params) { {} } - - let(:user) do - user = create(:user) - user.spree_roles << Spree::Role.find_or_create_by!(name: "admin") - user - end - - describe "fetching orders" do - let(:customers_with_balance) { instance_double(CustomersWithBalance) } - - it 'calls the OutstandingBalance query object' do - outstanding_balance = instance_double(OutstandingBalance, query: Spree::Order.none) - expect(OutstandingBalance).to receive(:new).and_return(outstanding_balance) - - subject.orders - end - - it "fetches completed orders" do - o1 = create(:order) - o2 = create(:order, completed_at: 1.day.ago, state: 'complete') - expect(subject.orders).to eq([o2]) - end - - it 'fetches resumed orders' do - order = create(:order, state: 'resumed', completed_at: 1.day.ago) - expect(subject.orders).to eq([order]) - end - - it 'orders them by id' do - order1 = create(:order, completed_at: 1.day.ago, state: 'complete') - order2 = create(:order, completed_at: 2.days.ago, state: 'complete') - - expect(subject.orders.pluck(:id)).to eq([order2.id, order1.id]) - end - - it "does not show cancelled orders" do - o1 = create(:order, state: 'canceled', completed_at: 1.day.ago) - o2 = create(:order, state: 'complete', completed_at: 1.day.ago) - expect(subject.orders).to eq([o2]) - end - - context "default date range" do - it "fetches orders completed in the past month" do - o1 = create(:order, state: 'complete', completed_at: 1.month.ago - 1.day) - o2 = create(:order, state: 'complete', completed_at: 1.month.ago + 1.day) - expect(subject.orders).to eq([o2]) - end - end - end - end - - context "as an enterprise user" do - let!(:user) { create(:user) } - - subject { OrderCycleManagementReport.new user, {}, true } - - describe "fetching orders" do - let(:supplier) { create(:supplier_enterprise) } - let(:product) { create(:simple_product, supplier: supplier) } - let(:order) { create(:order, completed_at: 1.day.ago) } - - it "only shows orders managed by the current user" do - d1 = create(:distributor_enterprise) - d1.enterprise_roles.create!(user: user) - d2 = create(:distributor_enterprise) - d2.enterprise_roles.create!(user: create(:user)) - - o1 = create(:order, distributor: d1, state: 'complete', completed_at: 1.day.ago) - o2 = create(:order, distributor: d2, state: 'complete', completed_at: 1.day.ago) - - expect(subject).to receive(:filter).with([o1]).and_return([o1]) - expect(subject.orders).to eq([o1]) - end - - it "does not show orders through a hub that the current user does not manage" do - # Given a supplier enterprise with an order for one of its products - supplier.enterprise_roles.create!(user: user) - order.line_items << create(:line_item_with_shipment, product: product) - - # When I fetch orders, I should see no orders - expect(subject).to receive(:filter).with([]).and_return([]) - expect(subject.orders).to eq([]) - end - end - - describe "filtering orders" do - let!(:orders) { Spree::Order.where(nil) } - let!(:supplier) { create(:supplier_enterprise) } - - let!(:oc1) { create(:simple_order_cycle) } - let!(:pm1) { create(:payment_method, name: "PM1") } - let!(:sm1) { create(:shipping_method, name: "ship1") } - let!(:s1) { create(:shipment_with, :shipping_method, shipping_method: sm1) } - let!(:order1) { create(:order, shipments: [s1], order_cycle: oc1) } - let!(:payment1) { create(:payment, order: order1, payment_method: pm1) } - - it "returns all orders sans-params" do - expect(subject.filter(orders)).to eq(orders) - end - - it "filters to a specific order cycle" do - oc2 = create(:simple_order_cycle) - order2 = create(:order, order_cycle: oc2) - - allow(subject).to receive(:params).and_return(order_cycle_id: oc1.id) - expect(subject.filter(orders)).to eq([order1]) - end - - it "filters to a payment method" do - pm2 = create(:payment_method, name: "PM2") - pm3 = create(:payment_method, name: "PM3") - order2 = create(:order, payments: [create(:payment, payment_method: pm2)]) - order3 = create(:order, payments: [create(:payment, payment_method: pm3)]) - - allow(subject).to receive(:params).and_return(payment_method_in: [pm1.id, pm3.id] ) - expect(subject.filter(orders)).to match_array [order1, order3] - end - - it "filters to a shipping method" do - sm2 = create(:shipping_method, name: "ship2") - sm3 = create(:shipping_method, name: "ship3") - s2 = create(:shipment_with, :shipping_method, shipping_method: sm2) - s3 = create(:shipment_with, :shipping_method, shipping_method: sm3) - order2 = create(:order, shipments: [s2]) - order3 = create(:order, shipments: [s3]) - - allow(subject).to receive(:params).and_return(shipping_method_in: [sm1.id, sm3.id]) - expect(subject.filter(orders)).to match_array [order1, order3] - end - - it "should do all the filters at once" do - allow(subject).to receive(:params).and_return(order_cycle_id: oc1.id, - shipping_method_name: sm1.name, - payment_method_name: pm1.name) - expect(subject.filter(orders)).to eq([order1]) - end - end - - describe '#table_rows' do - subject { OrderCycleManagementReport.new(user, params, true) } - - let(:distributor) { create(:distributor_enterprise) } - before { distributor.enterprise_roles.create!(user: user) } - - context 'when the report type is payment_methods' do - let(:params) { { report_subtype: 'payment_methods' } } - - let!(:order) do - create( - :completed_order_with_totals, - distributor: distributor, - completed_at: 1.day.ago - ) - end - - it 'returns rows with payment information' do - expect(subject.table_rows).to eq([[ - order.billing_address.firstname, - order.billing_address.lastname, - order.distributor.name, - '', - order.email, - order.billing_address.phone, - order.shipment.shipping_method.name, - nil, - order.total, - -order.total - ]]) - end - end - - context 'when the report type is not payment_methods' do - let(:params) { {} } - let!(:order) do - create( - :completed_order_with_totals, - distributor: distributor, - completed_at: 1.day.ago - ) - end - - it 'returns rows with delivery information' do - expect(subject.table_rows).to eq([[ - order.ship_address.firstname, - order.ship_address.lastname, - order.distributor.name, - "", - "#{order.ship_address.address1} #{order.ship_address.address2} #{order.ship_address.city}", - order.ship_address.zipcode, - order.ship_address.phone, - order.shipment.shipping_method.name, - nil, - order.total, - -order.total, - false, - order.special_instructions - ]]) - end - end - end - end - end -end diff --git a/spec/lib/open_food_network/orders_and_distributors_report_spec.rb b/spec/lib/open_food_network/orders_and_distributors_report_spec.rb deleted file mode 100644 index 4f97d9d848..0000000000 --- a/spec/lib/open_food_network/orders_and_distributors_report_spec.rb +++ /dev/null @@ -1,87 +0,0 @@ -# frozen_string_literal: true - -require 'spec_helper' -require 'open_food_network/orders_and_distributors_report' - -module OpenFoodNetwork - describe OrdersAndDistributorsReport do - describe 'orders and distributors report' do - it 'should return a header row describing the report' do - subject = OrdersAndDistributorsReport.new nil - - expect(subject.table_headers).to eq( - [ - 'Order date', 'Order Id', - 'Customer Name', 'Customer Email', 'Customer Phone', 'Customer City', - 'SKU', 'Item name', 'Variant', 'Quantity', 'Max Quantity', 'Cost', 'Shipping Cost', - 'Payment Method', - 'Distributor', 'Distributor address', 'Distributor city', 'Distributor postcode', - 'Shipping Method', 'Shipping instructions' - ] - ) - end - - context 'with completed order' do - let(:bill_address) { create(:address) } - let(:distributor) { create(:distributor_enterprise) } - let(:product) { create(:product) } - let(:shipping_method) { create(:shipping_method) } - let(:shipping_instructions) { 'pick up on thursday please!' } - let(:order) { - create(:order, - state: 'complete', completed_at: Time.zone.now, - distributor: distributor, bill_address: bill_address, - special_instructions: shipping_instructions) - } - let(:payment_method) { create(:payment_method, distributors: [distributor]) } - let(:payment) { create(:payment, payment_method: payment_method, order: order) } - let(:line_item) { create(:line_item_with_shipment, product: product, order: order) } - - before do - order.select_shipping_method(shipping_method.id) - order.payments << payment - order.line_items << line_item - end - - it 'should denormalise order and distributor details for display as csv' do - subject = OrdersAndDistributorsReport.new create(:admin_user), {}, true - - table = subject.table_rows - - expect(table.size).to eq 1 - expect(table[0]).to eq([ - order.reload.completed_at.strftime("%F %T"), - order.id, - bill_address.full_name, - order.email, - bill_address.phone, - bill_address.city, - line_item.product.sku, - line_item.product.name, - line_item.options_text, - line_item.quantity, - line_item.max_quantity, - line_item.price * line_item.quantity, - line_item.distribution_fee, - payment_method.name, - distributor.name, - distributor.address.address1, - distributor.address.city, - distributor.address.zipcode, - shipping_method.name, - shipping_instructions - ]) - end - - it "prints one row per line item" do - create(:line_item_with_shipment, order: order) - - subject = OrdersAndDistributorsReport.new(create(:admin_user), {}, true) - - table = subject.table_rows - expect(table.size).to eq 2 - end - end - end - end -end diff --git a/spec/lib/open_food_network/orders_and_fulfillment_report/customer_totals_report_spec.rb b/spec/lib/open_food_network/orders_and_fulfillment_report/customer_totals_report_spec.rb deleted file mode 100644 index 966cf6e05e..0000000000 --- a/spec/lib/open_food_network/orders_and_fulfillment_report/customer_totals_report_spec.rb +++ /dev/null @@ -1,127 +0,0 @@ -# frozen_string_literal: true - -require "spec_helper" -require 'open_food_network/orders_and_fulfillment_report' -require 'open_food_network/orders_and_fulfillment_report/customer_totals_report' - -RSpec.describe OpenFoodNetwork::OrdersAndFulfillmentReport::CustomerTotalsReport do - let!(:distributor) { create(:distributor_enterprise) } - let!(:customer) { create(:customer, enterprise: distributor) } - let(:current_user) { distributor.owner } - - let(:report) do - report_options = { report_subtype: described_class::REPORT_TYPE } - OpenFoodNetwork::OrdersAndFulfillmentReport.new(current_user, report_options, true) - end - - let(:report_table) do - report.table_rows - end - - context "viewing the report" do - let!(:order) do - create(:completed_order_with_totals, line_items_count: 1, user: customer.user, - customer: customer, distributor: distributor) - end - - it "generates the report" do - expect(report_table.length).to eq(2) - end - - it "has a line item row" do - distributor_name_field = report_table.first[0] - expect(distributor_name_field).to eq distributor.name - - customer_name_field = report_table.first[1] - expect(customer_name_field).to eq order.bill_address.full_name - - total_field = report_table.last[5] - expect(total_field).to eq I18n.t("admin.reports.total") - end - - it 'includes the order number and date in item rows' do - order_number_and_date_fields = report_table.first[33..34] - expect(order_number_and_date_fields).to eq([ - order.number, - order.completed_at.strftime("%F %T"), - ]) - end - - it 'includes the order number and date in total rows' do - order_number_and_date_fields = report_table.last[33..34] - expect(order_number_and_date_fields).to eq([ - order.number, - order.completed_at.strftime("%F %T"), - ]) - end - end - - context "loading shipping methods" do - let!(:shipping_method1) { - create(:shipping_method, distributors: [distributor], name: "First") - } - let!(:shipping_method2) { - create(:shipping_method, distributors: [distributor], name: "Second") - } - let!(:shipping_method3) { - create(:shipping_method, distributors: [distributor], name: "Third") - } - let!(:order) do - create(:completed_order_with_totals, line_items_count: 1, user: customer.user, - customer: customer, distributor: distributor) - end - - before do - order.shipments.each(&:refresh_rates) - order.select_shipping_method(shipping_method2.id) - end - - it "displays the correct shipping_method" do - shipping_method_name_field = report_table.first[15] - expect(shipping_method_name_field).to eq shipping_method2.name - end - end - - context "displaying payment fees" do - context "with both failed and completed payments present" do - let!(:order) { - create(:order_ready_to_ship, user: customer.user, - customer: customer, distributor: distributor) - } - let(:completed_payment) { order.payments.completed.first } - let!(:failed_payment) { create(:payment, order: order, state: "failed") } - - before do - completed_payment.adjustment.update amount: 123.00 - failed_payment.adjustment.update amount: 456.00, eligible: false, state: "finalized" - end - - it "shows the correct payment fee amount for the order" do - payment_fee_field = report_table.last[12] - expect(payment_fee_field).to eq completed_payment.adjustment.amount - end - end - end - - context 'when a variant override applies' do - let!(:order) do - create(:completed_order_with_totals, line_items_count: 1, user: customer.user, - customer: customer, distributor: distributor) - end - let(:overidden_sku) { 'magical_sku' } - - before do - create( - :variant_override, - hub: distributor, - variant: order.line_items.first.variant, - sku: overidden_sku - ) - end - - it 'uses the sku from the variant override' do - sku_field = report_table.first[23] - expect(sku_field).to eq overidden_sku - end - end -end diff --git a/spec/lib/open_food_network/orders_and_fulfillment_report/distributor_totals_by_supplier_report_spec.rb b/spec/lib/open_food_network/orders_and_fulfillment_report/distributor_totals_by_supplier_report_spec.rb deleted file mode 100644 index 3da55893ad..0000000000 --- a/spec/lib/open_food_network/orders_and_fulfillment_report/distributor_totals_by_supplier_report_spec.rb +++ /dev/null @@ -1,39 +0,0 @@ -# frozen_string_literal: true - -require 'spec_helper' -require 'open_food_network/orders_and_fulfillment_report/distributor_totals_by_supplier_report' - -RSpec.describe OpenFoodNetwork::OrdersAndFulfillmentReport::DistributorTotalsBySupplierReport do - let!(:distributor) { create(:distributor_enterprise) } - - let!(:order) do - create(:completed_order_with_totals, line_items_count: 1, distributor: distributor) - end - - let(:current_user) { distributor.owner } - - let(:report) do - report_options = { report_subtype: described_class::REPORT_TYPE } - OpenFoodNetwork::OrdersAndFulfillmentReport.new(current_user, report_options, true) - end - - let(:report_table) do - report.table_rows - end - - it "generates the report" do - expect(report_table.length).to eq(2) - end - - it "has a variant row under the distributor" do - distributor_name_field = report_table.first[0] - expect(distributor_name_field).to eq distributor.name - - supplier = order.line_items.first.variant.product.supplier - supplier_name_field = report_table.first[1] - expect(supplier_name_field).to eq supplier.name - - total_field = report_table.last[1] - expect(total_field).to eq I18n.t("admin.reports.total") - end -end diff --git a/spec/lib/open_food_network/orders_and_fulfillment_report/supplier_totals_by_distributor_report_spec.rb b/spec/lib/open_food_network/orders_and_fulfillment_report/supplier_totals_by_distributor_report_spec.rb deleted file mode 100644 index 337f64ed45..0000000000 --- a/spec/lib/open_food_network/orders_and_fulfillment_report/supplier_totals_by_distributor_report_spec.rb +++ /dev/null @@ -1,39 +0,0 @@ -# frozen_string_literal: true - -require 'spec_helper' -require 'open_food_network/orders_and_fulfillment_report/supplier_totals_by_distributor_report' - -RSpec.describe OpenFoodNetwork::OrdersAndFulfillmentReport::SupplierTotalsByDistributorReport do - let!(:distributor) { create(:distributor_enterprise) } - - let!(:order) do - create(:completed_order_with_totals, line_items_count: 1, distributor: distributor) - end - - let(:current_user) { distributor.owner } - - let(:report) do - report_options = { report_subtype: described_class::REPORT_TYPE } - OpenFoodNetwork::OrdersAndFulfillmentReport.new(current_user, report_options, true) - end - - let(:report_table) do - report.table_rows - end - - it "generates the report" do - expect(report_table.length).to eq(2) - end - - it "has a variant row under the distributor" do - supplier = order.line_items.first.variant.product.supplier - supplier_name_field = report_table.first[0] - expect(supplier_name_field).to eq supplier.name - - distributor_name_field = report_table.first[3] - expect(distributor_name_field).to eq distributor.name - - total_field = report_table.last[3] - expect(total_field).to eq I18n.t("admin.reports.total") - end -end diff --git a/spec/lib/open_food_network/orders_and_fulfillment_report/supplier_totals_report_spec.rb b/spec/lib/open_food_network/orders_and_fulfillment_report/supplier_totals_report_spec.rb deleted file mode 100644 index f93c088866..0000000000 --- a/spec/lib/open_food_network/orders_and_fulfillment_report/supplier_totals_report_spec.rb +++ /dev/null @@ -1,33 +0,0 @@ -# frozen_string_literal: true - -require 'spec_helper' -require 'open_food_network/orders_and_fulfillment_report/supplier_totals_report' - -RSpec.describe OpenFoodNetwork::OrdersAndFulfillmentReport::SupplierTotalsReport do - let!(:distributor) { create(:distributor_enterprise) } - - let!(:order) do - create(:completed_order_with_totals, line_items_count: 1, distributor: distributor) - end - - let(:current_user) { distributor.owner } - - let(:report) do - report_options = { report_subtype: described_class::REPORT_TYPE } - OpenFoodNetwork::OrdersAndFulfillmentReport.new(current_user, report_options, true) - end - - let(:report_table) do - report.table_rows - end - - it "generates the report" do - expect(report_table.length).to eq(1) - end - - it "has a variant row" do - supplier = order.line_items.first.variant.product.supplier - supplier_name_field = report_table.first[0] - expect(supplier_name_field).to eq supplier.name - end -end diff --git a/spec/lib/open_food_network/orders_and_fulfillments_report_spec.rb b/spec/lib/open_food_network/orders_and_fulfillments_report_spec.rb deleted file mode 100644 index 249ac1c61c..0000000000 --- a/spec/lib/open_food_network/orders_and_fulfillments_report_spec.rb +++ /dev/null @@ -1,277 +0,0 @@ -# frozen_string_literal: true - -require 'spec_helper' -require 'open_food_network/orders_and_fulfillment_report' -require 'open_food_network/order_grouper' - -describe OpenFoodNetwork::OrdersAndFulfillmentReport do - include AuthenticationHelper - - let(:distributor) { create(:distributor_enterprise) } - let(:order_cycle) { create(:simple_order_cycle) } - let(:address) { create(:address) } - let(:order) { - create( - :order, - completed_at: 1.day.ago, - order_cycle: order_cycle, - distributor: distributor, - bill_address: address - ) - } - let(:line_item) { build(:line_item_with_shipment) } - let(:user) { create(:user) } - let(:admin_user) { create(:admin_user) } - - describe "fetching orders" do - before { order.line_items << line_item } - - context "as a site admin" do - subject { described_class.new(admin_user, {}, true) } - - it "fetches completed orders" do - o2 = create(:order) - o2.line_items << build(:line_item) - expect(subject.table_items).to eq([line_item]) - end - - it "does not show cancelled orders" do - o2 = create(:order, state: "canceled", completed_at: 1.day.ago) - o2.line_items << build(:line_item_with_shipment) - expect(subject.table_items).to eq([line_item]) - end - end - - context "as a manager of a supplier" do - subject { described_class.new(user, {}, true) } - - let(:s1) { create(:supplier_enterprise) } - - before do - s1.enterprise_roles.create!(user: user) - end - - context "that has granted P-OC to the distributor" do - let(:o2) { - create( - :order, - distributor: distributor, - completed_at: 1.day.ago, - bill_address: create(:address), - ship_address: create(:address) - ) - } - let(:li2) { - build(:line_item_with_shipment, product: create(:simple_product, supplier: s1)) - } - - before do - o2.line_items << li2 - create( - :enterprise_relationship, - parent: s1, - child: distributor, - permissions_list: [:add_to_order_cycle] - ) - end - - it "shows line items supplied by my producers, with names hidden" do - expect(subject.table_items).to eq([li2]) - expect(subject.table_items.first.order.bill_address.firstname).to eq("HIDDEN") - end - - context "where the distributor allows suppliers to see customer names" do - before do - distributor.update_columns show_customer_names_to_suppliers: true - end - - it "shows line items supplied by my producers, with names shown" do - expect(subject.table_items).to eq([li2]) - expect(subject.table_items.first.order.bill_address.firstname). - to eq(order.bill_address.firstname) - end - end - end - - context "that has not granted P-OC to the distributor" do - let(:o2) { - create( - :order, - distributor: distributor, - completed_at: 1.day.ago, - bill_address: create(:address), - ship_address: create(:address) - ) - } - let(:li2) { - build(:line_item_with_shipment, product: create(:simple_product, supplier: s1)) - } - - before do - o2.line_items << li2 - end - - it "does not show line items supplied by my producers" do - expect(subject.table_items).to eq([]) - end - - context "where the distributor allows suppliers to see customer names" do - before do - distributor.show_customer_names_to_suppliers = true - end - - it "does not show line items supplied by my producers" do - expect(subject.table_items).to eq([]) - end - end - end - end - - context "as a manager of a distributor" do - subject { described_class.new(user, {}, true) } - - before do - distributor.enterprise_roles.create!(user: user) - end - - it "only shows line items distributed by enterprises managed by the current user" do - d2 = create(:distributor_enterprise) - d2.enterprise_roles.create!(user: create(:user)) - o2 = create(:order, distributor: d2, completed_at: 1.day.ago) - o2.line_items << build(:line_item_with_shipment) - expect(subject.table_items).to eq([line_item]) - end - - it "only shows the selected order cycle" do - oc2 = create(:simple_order_cycle) - o2 = create(:order, distributor: distributor, order_cycle: oc2) - o2.line_items << build(:line_item) - allow(subject).to receive(:params).and_return(order_cycle_id_in: order_cycle.id) - expect(subject.table_items).to eq([line_item]) - end - end - end - - describe "columns are aligned" do - it 'has aligned columsn' do - report_types = [ - "", - "order_cycle_supplier_totals", - "order_cycle_supplier_totals_by_distributor", - "order_cycle_distributor_totals_by_supplier", - "order_cycle_customer_totals" - ] - - report_types.each do |report_type| - report = described_class.new(admin_user, report_subtype: report_type) - expect(report.table_headers.size).to eq(report.columns.size) - end - end - end - - describe "order_cycle_customer_totals" do - let!(:product) { line_item.product } - let!(:fuji) do - create(:variant, product: product, display_name: "Fuji", sku: "FUJI", on_hand: 100) - end - let!(:gala) do - create(:variant, product: product, display_name: "Gala", sku: "GALA", on_hand: 100) - end - - let(:items) { - described_class.new(admin_user, { report_subtype: "order_cycle_customer_totals" }, true) - .table_rows - } - - before do - # Clear price so it will be computed based on quantity and variant price. - order.line_items << build(:line_item_with_shipment, variant: fuji, price: nil, quantity: 1) - order.line_items << build(:line_item_with_shipment, variant: gala, price: nil, quantity: 2) - end - - it "has a product row" do - product_name_field = items.first[5] - expect(product_name_field).to eq product.name - end - - it "has a summary row" do - product_name_field = items.last[5] - expect(product_name_field).to eq "TOTAL" - end - - # Expected Report for Scenario: - # - # Row 1: Armstrong Amari, Fuji Apple, price: 8 - # Row 2: SUMMARY - # Row 3: Bartoletti Brooklyn, Fuji Apple, price: 1 + 4 - # Row 4: Bartoletti Brooklyn, Gala Apple, price: 2 - # Row 5: SUMMARY - describe "grouping of line items" do - let!(:address) { create(:address, last_name: "Bartoletti", first_name: "Brooklyn") } - - let!(:second_address) { create(:address, last_name: "Armstrong", first_name: "Amari") } - let!(:second_order) do - create(:order, completed_at: 1.day.ago, order_cycle: order_cycle, distributor: distributor, - bill_address: second_address) - end - - before do - # Add a second line item for Fuji variant to the order, to test grouping in this edge case. - order.line_items << build(:line_item_with_shipment, variant: fuji, price: nil, quantity: 4) - - second_order.line_items << build(:line_item_with_shipment, variant: fuji, price: nil, - quantity: 8) - end - - it "groups line items by variant and order" do - expect(items.length).to eq(5) - - # Row 1: Armstrong Amari, Fuji Apple, price: 8 - row_data = items[0] - expect(customer_name(row_data)).to eq(second_address.full_name) - expect(amount(row_data)).to eq(fuji.price * 8) - expect(variant_sku(row_data)).to eq(fuji.sku) - - # Row 2: SUMMARY - row_data = items[1] - expect(totals_row?(row_data)).to eq(true) - expect(customer_name(row_data)).to eq(second_address.full_name) - expect(amount(row_data)).to eq(fuji.price * 8) - - # Row 3: Bartoletti Brooklyn, Fuji Apple, price: 1 + 4 - row_data = items[2] - expect(customer_name(row_data)).to eq(address.full_name) - expect(amount(row_data)).to eq(fuji.price * 5) - expect(variant_sku(row_data)).to eq(fuji.sku) - - # Row 4: Bartoletti Brooklyn, Gala Apple, price: 2 - row_data = items[3] - expect(customer_name(row_data)).to eq(address.full_name) - expect(amount(row_data)).to eq(gala.price * 2) - expect(variant_sku(row_data)).to eq(gala.sku) - - # Row 5: SUMMARY - row_data = items[4] - expect(totals_row?(row_data)).to eq(true) - expect(customer_name(row_data)).to eq(address.full_name) - expect(amount(row_data)).to eq(fuji.price * 5 + gala.price * 2) - end - end - - def totals_row?(row_data) - row_data[5] == I18n.t("admin.reports.total") - end - - def customer_name(row_data) - row_data[1] - end - - def amount(row_data) - row_data[8] - end - - def variant_sku(row_data) - row_data[23] - end - end -end diff --git a/spec/lib/open_food_network/products_and_inventory_default_report_spec.rb b/spec/lib/open_food_network/products_and_inventory_default_report_spec.rb deleted file mode 100644 index 2de38c53cc..0000000000 --- a/spec/lib/open_food_network/products_and_inventory_default_report_spec.rb +++ /dev/null @@ -1,261 +0,0 @@ -# frozen_string_literal: true - -require 'spec_helper' -require 'open_food_network/products_and_inventory_report' - -describe OpenFoodNetwork::ProductsAndInventoryDefaultReport do - context "As a site admin" do - let(:user) do - user = create(:user) - user.spree_roles << Spree::Role.find_or_create_by!(name: 'admin') - user - end - subject do - OpenFoodNetwork::ProductsAndInventoryReport.new user, {}, true - end - - it "Should return headers" do - expect(subject.table_headers).to eq([ - "Supplier", - "Producer Suburb", - "Product", - "Product Properties", - "Taxons", - "Variant Value", - "Price", - "Group Buy Unit Quantity", - "Amount", - "SKU" - ]) - end - - it "should build a table from a list of variants" do - variant = double(:variant, sku: "sku", - full_name: "Variant Name", - count_on_hand: 10, - price: 100) - allow(variant).to receive_message_chain(:product, :supplier, :name).and_return("Supplier") - allow(variant).to receive_message_chain(:product, :supplier, :address, - :city).and_return("A city") - allow(variant).to receive_message_chain(:product, :name).and_return("Product Name") - allow(variant).to receive_message_chain(:product, - :properties).and_return [double(name: "property1"), - double(name: "property2")] - allow(variant).to receive_message_chain(:product, - :taxons).and_return [double(name: "taxon1"), - double(name: "taxon2")] - allow(variant).to receive_message_chain(:product, :group_buy_unit_size).and_return(21) - allow(subject).to receive(:variants).and_return [variant] - - expect(subject.table_rows).to eq([[ - "Supplier", - "A city", - "Product Name", - "property1, property2", - "taxon1, taxon2", - "Variant Name", - 100, - 21, - "", - "sku" - ]]) - end - - it "fetches variants for some params" do - expect(subject).to receive(:child_variants).and_return ["children"] - expect(subject).to receive(:filter).with(['children']).and_return ["filter_children"] - expect(subject.variants).to eq(["filter_children"]) - end - end - - context "As an enterprise user" do - let(:supplier) { create(:supplier_enterprise) } - let(:enterprise_user) do - user = create(:user) - user.enterprise_roles.create(enterprise: supplier) - user.spree_roles = [] - user.save! - user - end - - subject do - OpenFoodNetwork::ProductsAndInventoryReport.new enterprise_user, {}, true - end - - describe "fetching child variants" do - it "returns some variants" do - product1 = create(:simple_product, supplier: supplier) - variant1 = product1.variants.first - variant2 = create(:variant, product: product1) - - expect(subject.child_variants).to match_array [variant1, variant2] - end - - it "should only return variants managed by the user" do - product1 = create(:simple_product, supplier: create(:supplier_enterprise)) - product2 = create(:simple_product, supplier: supplier) - variant1 = product1.variants.first - variant2 = product2.variants.first - - expect(subject.child_variants).to eq([variant2]) - end - end - - describe "Filtering variants" do - let(:variants) { Spree::Variant.where(nil).joins(:product).where(is_master: false) } - - describe "based on report type" do - it "returns only variants on hand" do - product1 = create(:simple_product, supplier: supplier, on_hand: 99) - product2 = create(:simple_product, supplier: supplier, on_hand: 0) - - allow(subject).to receive(:params).and_return(report_subtype: 'inventory') - expect(subject.filter(variants)).to eq([product1.variants.first]) - end - end - it "filters to a specific supplier" do - supplier2 = create(:supplier_enterprise) - product1 = create(:simple_product, supplier: supplier) - product2 = create(:simple_product, supplier: supplier2) - - allow(subject).to receive(:params).and_return(supplier_id: supplier.id) - expect(subject.filter(variants)).to eq([product1.variants.first]) - end - it "filters to a specific distributor" do - distributor = create(:distributor_enterprise) - product1 = create(:simple_product, supplier: supplier) - product2 = create(:simple_product, supplier: supplier) - order_cycle = create(:simple_order_cycle, suppliers: [supplier], - distributors: [distributor], - variants: [product2.variants.first]) - - allow(subject).to receive(:params).and_return(distributor_id: distributor.id) - expect(subject.filter(variants)).to eq([product2.variants.first]) - end - - it "ignores variant overrides without filter" do - distributor = create(:distributor_enterprise) - product = create(:simple_product, supplier: supplier, price: 5) - variant = product.variants.first - order_cycle = create(:simple_order_cycle, suppliers: [supplier], - distributors: [distributor], - variants: [product.variants.first]) - create(:variant_override, hub: distributor, variant: variant, price: 2) - - result = subject.filter(variants) - - expect(result.first.price).to eq 5 - end - - it "considers variant overrides with distributor" do - distributor = create(:distributor_enterprise) - product = create(:simple_product, supplier: supplier, price: 5) - variant = product.variants.first - order_cycle = create(:simple_order_cycle, suppliers: [supplier], - distributors: [distributor], - variants: [product.variants.first]) - create(:variant_override, hub: distributor, variant: variant, price: 2) - - allow(subject).to receive(:params).and_return(distributor_id: distributor.id) - result = subject.filter(variants) - - expect(result.first.price).to eq 2 - end - - it "filters to a specific order cycle" do - distributor = create(:distributor_enterprise) - product1 = create(:simple_product, supplier: supplier) - product2 = create(:simple_product, supplier: supplier) - order_cycle = create(:simple_order_cycle, suppliers: [supplier], - distributors: [distributor], - variants: [product1.variants.first]) - - allow(subject).to receive(:params).and_return(order_cycle_id: order_cycle.id) - expect(subject.filter(variants)).to eq([product1.variants.first]) - end - - it "should do all the filters at once" do - # The following data ensures that this spec fails if any of the - # filters fail. It's testing the filters are not impacting each other. - distributor = create(:distributor_enterprise) - other_distributor = create(:distributor_enterprise) - other_supplier = create(:supplier_enterprise) - not_filtered_variant = create(:simple_product, supplier: supplier).variants.first - variant_filtered_by_order_cycle = create(:simple_product, - supplier: supplier).variants.first - variant_filtered_by_distributor = create(:simple_product, - supplier: supplier).variants.first - variant_filtered_by_supplier = create(:simple_product, - supplier: other_supplier).variants.first - variant_filtered_by_stock = create(:simple_product, supplier: supplier, - on_hand: 0).variants.first - - # This OC contains all products except the one that should be filtered - # by order cycle. We create a separate OC further down to proof that - # the product is passing all other filters. - order_cycle = create( - :simple_order_cycle, - suppliers: [supplier, other_supplier], - distributors: [distributor, other_distributor], - variants: [ - not_filtered_variant, - variant_filtered_by_distributor, - variant_filtered_by_supplier, - variant_filtered_by_stock - ] - ) - - # Remove the distribution of one product for one distributor but still - # sell it through the other distributor. - order_cycle.exchanges.outgoing.find_by(receiver_id: distributor.id). - exchange_variants. - find_by(variant_id: variant_filtered_by_distributor). - destroy - - # Make product available to be filtered later. See OC comment above. - create( - :simple_order_cycle, - suppliers: [supplier], - distributors: [distributor, other_distributor], - variants: [ - variant_filtered_by_order_cycle - ] - ) - - allow(subject).to receive(:params).and_return( - order_cycle_id: order_cycle.id, - supplier_id: supplier.id, - distributor_id: distributor.id, - report_subtype: 'inventory' - ) - - expect(subject.filter(variants)).to match_array [not_filtered_variant] - - # And it integrates with the ordering of the `variants` method. - expect(subject.variants).to match_array [not_filtered_variant] - end - end - - describe "fetching SKU for a variant" do - let(:variant) { create(:variant) } - let(:product) { variant.product } - - before { product.update_attribute(:sku, "Product SKU") } - - context "when the variant has an SKU set" do - before { variant.update_attribute(:sku, "Variant SKU") } - it "returns it" do - expect(subject.__send__(:sku_for, variant)).to eq "Variant SKU" - end - end - - context "when the variant has bo SKU set" do - before { variant.update_attribute(:sku, "") } - - it "returns the product's SKU" do - expect(subject.__send__(:sku_for, variant)).to eq "Product SKU" - end - end - end - end -end diff --git a/spec/lib/open_food_network/sales_tax_report_spec.rb b/spec/lib/open_food_network/sales_tax_report_spec.rb deleted file mode 100644 index 72c68d5c05..0000000000 --- a/spec/lib/open_food_network/sales_tax_report_spec.rb +++ /dev/null @@ -1,64 +0,0 @@ -# frozen_string_literal: true - -require 'spec_helper' -require 'open_food_network/sales_tax_report' - -module OpenFoodNetwork - describe SalesTaxReport do - let(:user) { create(:user) } - let(:report) { SalesTaxReport.new(user, {}, true) } - - describe "calculating totals for line items" do - let(:li1) { double(:line_item, quantity: 1, amount: 12) } - let(:li2) { double(:line_item, quantity: 2, amount: 24) } - let(:totals) { report.send(:totals_of, [li1, li2]) } - - before do - allow(report).to receive(:tax_included_in).and_return(2, 4) - end - - it "calculates total quantity" do - expect(totals[:items]).to eq(3) - end - - it "calculates total price" do - expect(totals[:items_total]).to eq(36) - end - - context "when floating point math would result in fractional cents" do - let(:li1) { double(:line_item, quantity: 1, amount: 0.11) } - let(:li2) { double(:line_item, quantity: 2, amount: 0.12) } - - it "rounds to the nearest cent" do - expect(totals[:items_total]).to eq(0.23) - end - end - - it "calculates the taxable total price" do - expect(totals[:taxable_total]).to eq(36) - end - - it "calculates sales tax" do - expect(totals[:sales_tax]).to eq(6) - end - - context "when there is no tax on a line item" do - before do - allow(report).to receive(:tax_included_in) { 0 } - end - - it "does not appear in taxable total" do - expect(totals[:taxable_total]).to eq(0) - end - - it "still appears on items total" do - expect(totals[:items_total]).to eq(36) - end - - it "does not register sales tax" do - expect(totals[:sales_tax]).to eq(0) - end - end - end - end -end diff --git a/spec/lib/open_food_network/users_and_enterprises_report_spec.rb b/spec/lib/open_food_network/users_and_enterprises_report_spec.rb deleted file mode 100644 index ca9b9f4a23..0000000000 --- a/spec/lib/open_food_network/users_and_enterprises_report_spec.rb +++ /dev/null @@ -1,125 +0,0 @@ -# frozen_string_literal: true - -require 'spec_helper' -require 'open_food_network/users_and_enterprises_report' - -module OpenFoodNetwork - describe UsersAndEnterprisesReport do - describe "users_and_enterprises" do - let!(:owners_and_enterprises) { double(:owners_and_enterprises) } - let!(:managers_and_enterprises) { double(:managers_and_enterprises) } - let!(:subject) { OpenFoodNetwork::UsersAndEnterprisesReport.new(nil, {}, true) } - - before do - allow(subject).to receive(:owners_and_enterprises) { owners_and_enterprises } - allow(subject).to receive(:managers_and_enterprises) { managers_and_enterprises } - end - - it "should concatenate owner and manager queries" do - expect(subject).to receive(:owners_and_enterprises).once - expect(subject).to receive(:managers_and_enterprises).once - expect(owners_and_enterprises).to receive(:concat).with(managers_and_enterprises).and_return [] - expect(subject).to receive(:sort).with [] - subject.users_and_enterprises - end - end - - describe "sorting results" do - let!(:subject) { OpenFoodNetwork::UsersAndEnterprisesReport.new(nil, {}, true) } - - it "sorts by creation date" do - uae_mock = [ - { "created_at" => "2015-01-01", "name" => "bbb" }, - { "created_at" => "2015-01-02", "name" => "aaa" } - ] - expect(subject.sort(uae_mock)).to eq [uae_mock[1], uae_mock[0]] - end - - it "then sorts by name" do - uae_mock = [ - { "name" => "aaa", "relationship_type" => "bbb", "user_email" => "bbb" }, - { "name" => "bbb", "relationship_type" => "aaa", "user_email" => "aaa" } - ] - expect(subject.sort(uae_mock)).to eq [uae_mock[0], uae_mock[1]] - end - - it "then sorts by relationship type (reveresed)" do - uae_mock = [ - { "name" => "aaa", "relationship_type" => "bbb", "user_email" => "bbb" }, - { "name" => "aaa", "relationship_type" => "aaa", "user_email" => "aaa" }, - { "name" => "aaa", "relationship_type" => "bbb", "user_email" => "aaa" } - ] - expect(subject.sort(uae_mock)).to eq [uae_mock[2], uae_mock[0], uae_mock[1]] - end - - it "then sorts by user_email" do - uae_mock = [ - { "name" => "aaa", "relationship_type" => "bbb", "user_email" => "aaa" }, - { "name" => "aaa", "relationship_type" => "aaa", "user_email" => "aaa" }, - { "name" => "aaa", "relationship_type" => "aaa", "user_email" => "bbb" } - ] - expect(subject.sort(uae_mock)).to eq [uae_mock[0], uae_mock[1], uae_mock[2]] - end - end - - describe "filtering results" do - let!(:enterprise1) { create(:enterprise, owner: create(:user) ) } - let!(:enterprise2) { create(:enterprise, owner: create(:user) ) } - - describe "for owners and enterprises" do - describe "by enterprise id" do - let!(:params) { { enterprise_id_in: [enterprise1.id.to_s] } } - let!(:subject) { OpenFoodNetwork::UsersAndEnterprisesReport.new nil, params, true } - - it "excludes enterprises that are not explicitly requested" do - results = subject.owners_and_enterprises.to_a.map{ |oae| oae["name"] } - expect(results).to include enterprise1.name - expect(results).to_not include enterprise2.name - end - end - - describe "by user id" do - let!(:params) { { user_id_in: [enterprise1.owner.id.to_s] } } - let!(:subject) { OpenFoodNetwork::UsersAndEnterprisesReport.new nil, params, true } - - it "excludes enterprises that are not explicitly requested" do - results = subject.owners_and_enterprises.to_a.map{ |oae| oae["name"] } - expect(results).to include enterprise1.name - expect(results).to_not include enterprise2.name - end - end - end - - describe "for managers and enterprises" do - describe "by enterprise id" do - let!(:params) { { enterprise_id_in: [enterprise1.id.to_s] } } - let!(:subject) { OpenFoodNetwork::UsersAndEnterprisesReport.new nil, params, true } - - it "excludes enterprises that are not explicitly requested" do - results = subject.managers_and_enterprises.to_a.map{ |mae| mae["name"] } - expect(results).to include enterprise1.name - expect(results).to_not include enterprise2.name - end - end - - describe "by user id" do - let!(:manager1) { create(:user) } - let!(:manager2) { create(:user) } - let!(:params) { { user_id_in: [manager1.id.to_s] } } - let!(:subject) { OpenFoodNetwork::UsersAndEnterprisesReport.new nil, params, true } - - before do - enterprise1.enterprise_roles.build(user: manager1).save - enterprise2.enterprise_roles.build(user: manager2).save - end - - it "excludes enterprises whose managers are not explicitly requested" do - results = subject.managers_and_enterprises.to_a.map{ |mae| mae["name"] } - expect(results).to include enterprise1.name - expect(results).to_not include enterprise2.name - end - end - end - end - end -end diff --git a/spec/lib/open_food_network/xero_invoices_report_spec.rb b/spec/lib/open_food_network/xero_invoices_report_spec.rb deleted file mode 100644 index 5235edd972..0000000000 --- a/spec/lib/open_food_network/xero_invoices_report_spec.rb +++ /dev/null @@ -1,96 +0,0 @@ -# frozen_string_literal: true - -require 'spec_helper' -require 'open_food_network/xero_invoices_report' - -module OpenFoodNetwork - describe XeroInvoicesReport do - subject { XeroInvoicesReport.new user, {}, true } - - let(:user) { create(:user) } - - describe "option defaults" do - let(:report) { - XeroInvoicesReport.new user, initial_invoice_number: '', invoice_date: '', due_date: '', - account_code: '' - } - - around { |example| Timecop.travel(Time.zone.local(2015, 5, 5, 14, 0, 0)) { example.run } } - - it "uses defaults when blank params are passed" do - expect(report.instance_variable_get(:@opts)).to eq( invoice_date: Date.civil(2015, 5, 5), - due_date: Date.civil(2015, 6, 5), - account_code: 'food sales', - report_subtype: 'summary' ) - end - end - - describe "summary rows" do - let(:report) { - XeroInvoicesReport.new user, initial_invoice_number: '', invoice_date: '', due_date: '', - account_code: '' - } - let(:order) { double(:order) } - let(:summary_rows) { report.send(:summary_rows_for_order, order, 1, {}) } - - before do - allow(report).to receive(:produce_summary_rows) { ['produce'] } - allow(report).to receive(:fee_summary_rows) { ['fee'] } - allow(report).to receive(:shipping_summary_rows) { ['shipping'] } - allow(report).to receive(:payment_summary_rows) { ['payment'] } - allow(report).to receive(:admin_adjustment_summary_rows) { ['admin'] } - end - - it "displays produce summary rows when summary report" do - allow(report).to receive(:detail?) { false } - expect(summary_rows).to include 'produce' - end - - it "does not display produce summary rows when detail report" do - allow(report).to receive(:detail?) { true } - expect(summary_rows).not_to include 'produce' - end - - it "displays fee summary rows when summary report" do - allow(report).to receive(:detail?) { false } - expect(summary_rows).to include 'fee' - end - - it "displays fee summary rows when detail report" do - allow(report).to receive(:detail?) { true } - expect(summary_rows).to include 'fee' - end - - it "always displays shipping summary rows" do - expect(summary_rows).to include 'shipping' - end - - it "displays admin adjustment summary rows when summary report" do - expect(summary_rows).to include 'admin' - end - - it "does not display admin adjustment summary rows when detail report" do - allow(report).to receive(:detail?) { true } - expect(summary_rows).not_to include 'admin' - end - end - - describe "generating invoice numbers" do - let(:order) { double(:order, number: 'R731032860') } - - describe "when no initial invoice number is given" do - it "returns the order number" do - expect(subject.send(:invoice_number_for, order, 123)).to eq('R731032860') - end - end - - describe "when an initial invoice number is given" do - subject { XeroInvoicesReport.new user, initial_invoice_number: '123' } - - it "increments the number by the index" do - expect(subject.send(:invoice_number_for, order, 456)).to eq(579) - end - end - end - end -end diff --git a/spec/lib/open_food_network/bulk_coop_report_spec.rb b/spec/lib/reports/bulk_coop_report_spec.rb similarity index 88% rename from spec/lib/open_food_network/bulk_coop_report_spec.rb rename to spec/lib/reports/bulk_coop_report_spec.rb index 6fe1f36809..80f118a03a 100644 --- a/spec/lib/open_food_network/bulk_coop_report_spec.rb +++ b/spec/lib/reports/bulk_coop_report_spec.rb @@ -1,10 +1,9 @@ # frozen_string_literal: true require 'spec_helper' -require 'open_food_network/bulk_coop_report' -describe OpenFoodNetwork::BulkCoopReport do - subject { OpenFoodNetwork::BulkCoopReport.new user, params, true } +describe Reporting::Reports::BulkCoop::BulkCoopReport do + subject { Reporting::Reports::BulkCoop::BulkCoopReport.new user, params, true } let(:user) { create(:admin_user) } describe '#table_items' do @@ -62,15 +61,15 @@ describe OpenFoodNetwork::BulkCoopReport do li2 = build(:line_item_with_shipment) o2.line_items << li2 - report = OpenFoodNetwork::BulkCoopReport.new user, {}, true + report = Reporting::Reports::BulkCoop::BulkCoopReport.new user, {}, true expect(report.table_items).to match_array [li1, li2] - report = OpenFoodNetwork::BulkCoopReport.new( + report = Reporting::Reports::BulkCoop::BulkCoopReport.new( user, { q: { completed_at_gt: 2.days.ago } }, true ) expect(report.table_items).to eq([li1]) - report = OpenFoodNetwork::BulkCoopReport.new( + report = Reporting::Reports::BulkCoop::BulkCoopReport.new( user, { q: { completed_at_lt: 2.days.ago } }, true ) expect(report.table_items).to eq([li2]) @@ -86,15 +85,15 @@ describe OpenFoodNetwork::BulkCoopReport do li2 = build(:line_item_with_shipment) o2.line_items << li2 - report = OpenFoodNetwork::BulkCoopReport.new user, {}, true + report = Reporting::Reports::BulkCoop::BulkCoopReport.new user, {}, true expect(report.table_items).to match_array [li1, li2] - report = OpenFoodNetwork::BulkCoopReport.new( + report = Reporting::Reports::BulkCoop::BulkCoopReport.new( user, { q: { distributor_id_in: [d1.id] } }, true ) expect(report.table_items).to eq([li1]) - report = OpenFoodNetwork::BulkCoopReport.new( + report = Reporting::Reports::BulkCoop::BulkCoopReport.new( user, { q: { distributor_id_in: [d2.id] } }, true ) expect(report.table_items).to eq([li2]) @@ -103,7 +102,7 @@ describe OpenFoodNetwork::BulkCoopReport do context "as a manager of a supplier" do let!(:user) { create(:user) } - subject { OpenFoodNetwork::BulkCoopReport.new user, {}, true } + subject { Reporting::Reports::BulkCoop::BulkCoopReport.new user, {}, true } let(:s1) { create(:supplier_enterprise) } @@ -171,7 +170,7 @@ describe OpenFoodNetwork::BulkCoopReport do end # Yes, I know testing a private method is bad practice but report's design, tighly coupling - # OpenFoodNetwork::OrderGrouper and OpenFoodNetwork::BulkCoopReport, makes it + # Reporting::OrderGrouper and Reporting::Reports::BulkCoop::BulkCoopReport, makes it # very hard to make things testeable without ending up in a wormwhole. This is a trade-off. describe '#customer_payments_amount_owed' do let(:params) { {} } diff --git a/spec/lib/reports/customers_report_spec.rb b/spec/lib/reports/customers_report_spec.rb new file mode 100644 index 0000000000..2ed1e950b5 --- /dev/null +++ b/spec/lib/reports/customers_report_spec.rb @@ -0,0 +1,166 @@ +# frozen_string_literal: true + +require 'spec_helper' + +module Reporting + module Reports + module Customers + describe CustomersReport do + context "as a site admin" do + let(:user) do + user = create(:user) + user.spree_roles << Spree::Role.find_or_create_by!(name: 'admin') + user + end + subject { CustomersReport.new user, {}, true } + + describe "mailing list report" do + before do + allow(subject).to receive(:params).and_return(report_subtype: "mailing_list") + end + + it "returns headers for mailing_list" do + expect(subject.table_headers).to eq(["Email", "First Name", "Last Name", "Suburb"]) + end + + it "builds a table from a list of variants" do + order = double(:order, email: "test@test.com") + address = double(:billing_address, firstname: "Firsty", + lastname: "Lasty", city: "Suburbia") + allow(order).to receive(:billing_address).and_return address + allow(subject).to receive(:orders).and_return [order] + + expect(subject.table_rows).to eq([[ + "test@test.com", "Firsty", "Lasty", "Suburbia" + ]]) + end + end + + describe "addresses report" do + before do + allow(subject).to receive(:params).and_return(report_subtype: "addresses") + end + + it "returns headers for addresses" do + expect(subject.table_headers).to eq(["First Name", "Last Name", "Billing Address", "Email", + "Phone", "Hub", "Hub Address", "Shipping Method"]) + end + + it "builds a table from a list of variants" do + a = create(:address) + d = create(:distributor_enterprise) + o = create(:order, distributor: d, bill_address: a) + o.shipments << create(:shipment) + + allow(subject).to receive(:orders).and_return [o] + expect(subject.table_rows).to eq([[ + a.firstname, a.lastname, + [a.address1, a.address2, a.city].join(" "), + o.email, a.phone, d.name, + [d.address.address1, d.address.address2, d.address.city].join(" "), + o.shipping_method.name + ]]) + end + end + + describe "fetching orders" do + it "fetches completed orders" do + o1 = create(:order) + o2 = create(:order, completed_at: 1.day.ago) + expect(subject.orders).to eq([o2]) + end + + it "does not show cancelled orders" do + o1 = create(:order, state: "canceled", completed_at: 1.day.ago) + o2 = create(:order, completed_at: 1.day.ago) + expect(subject.orders).to eq([o2]) + end + end + end + + context "as an enterprise user" do + let(:user) do + user = create(:user) + user.spree_roles = [] + user.save! + user + end + + subject { CustomersReport.new user, {}, true } + + describe "fetching orders" do + let(:supplier) { create(:supplier_enterprise) } + let(:product) { create(:simple_product, supplier: supplier) } + let(:order) { create(:order, completed_at: 1.day.ago) } + + it "only shows orders managed by the current user" do + d1 = create(:distributor_enterprise) + d1.enterprise_roles.build(user: user).save + d2 = create(:distributor_enterprise) + d2.enterprise_roles.build(user: create(:user)).save + + o1 = create(:order, distributor: d1, completed_at: 1.day.ago) + o2 = create(:order, distributor: d2, completed_at: 1.day.ago) + + expect(subject).to receive(:filter).with([o1]).and_return([o1]) + expect(subject.orders).to eq([o1]) + end + + it "does not show orders through a hub that the current user does not manage" do + # Given a supplier enterprise with an order for one of its products + supplier.enterprise_roles.build(user: user).save + order.line_items << create(:line_item_with_shipment, product: product) + + # When I fetch orders, I should see no orders + expect(subject).to receive(:filter).with([]).and_return([]) + expect(subject.orders).to eq([]) + end + end + + describe "filtering orders" do + let(:orders) { Spree::Order.where(nil) } + let(:supplier) { create(:supplier_enterprise) } + + it "returns all orders sans-params" do + expect(subject.filter(orders)).to eq(orders) + end + + it "returns orders with a specific supplier" do + supplier = create(:supplier_enterprise) + supplier2 = create(:supplier_enterprise) + product1 = create(:simple_product, supplier: supplier) + product2 = create(:simple_product, supplier: supplier2) + order1 = create(:order) + order2 = create(:order) + order1.line_items << create(:line_item, product: product1) + order2.line_items << create(:line_item, product: product2) + + allow(subject).to receive(:params).and_return(supplier_id: supplier.id) + expect(subject.filter(orders)).to eq([order1]) + end + + it "filters to a specific distributor" do + d1 = create(:distributor_enterprise) + d2 = create(:distributor_enterprise) + order1 = create(:order, distributor: d1) + order2 = create(:order, distributor: d2) + + allow(subject).to receive(:params).and_return(distributor_id: d1.id) + expect(subject.filter(orders)).to eq([order1]) + end + + it "filters to a specific cycle" do + oc1 = create(:simple_order_cycle) + oc2 = create(:simple_order_cycle) + order1 = create(:order, order_cycle: oc1) + order2 = create(:order, order_cycle: oc2) + + allow(subject).to receive(:params).and_return(order_cycle_id: oc1.id) + expect(subject.filter(orders)).to eq([order1]) + end + end + end + end + end + end +end diff --git a/spec/lib/reports/enterprise_fee_summary/authorizer_spec.rb b/spec/lib/reports/enterprise_fee_summary/authorizer_spec.rb new file mode 100644 index 0000000000..f629c84fae --- /dev/null +++ b/spec/lib/reports/enterprise_fee_summary/authorizer_spec.rb @@ -0,0 +1,179 @@ +# frozen_string_literal: true + +require "spec_helper" + +module Reporting + module Reports + module EnterpriseFeeSummary + describe Authorizer do + let(:user) { create(:user) } + + let(:parameters) { Parameters.new(params) } + let(:permissions) { Permissions.new(user) } + let(:authorizer) { Authorizer.new(parameters, permissions) } + + context "for distributors" do + before do + allow(permissions).to receive(:allowed_distributors) do + stub_model_collection(Enterprise, :id, ["1", "2", "3"]) + end + end + + context "when distributors are allowed" do + let(:params) { { distributor_ids: ["1", "3"] } } + + it "does not raise error" do + expect { authorizer.authorize! }.not_to raise_error + end + end + + context "when a distributor is not allowed" do + let(:params) { { distributor_ids: ["1", "4"] } } + + it "raises ParameterNotAllowedError" do + expect { authorizer.authorize! } + .to raise_error(ParameterNotAllowedError) + end + end + end + + context "for producers" do + before do + allow(permissions).to receive(:allowed_producers) do + stub_model_collection(Enterprise, :id, ["1", "2", "3"]) + end + end + + context "when producers are allowed" do + let(:params) { { producer_ids: ["1", "3"] } } + + it "does not raise error" do + expect { authorizer.authorize! }.not_to raise_error + end + end + + context "when a producer is not allowed" do + let(:params) { { producer_ids: ["1", "4"] } } + + it "raises ParameterNotAllowedError" do + expect { authorizer.authorize! } + .to raise_error(ParameterNotAllowedError) + end + end + end + + context "for order cycles" do + before do + allow(permissions).to receive(:allowed_order_cycles) do + stub_model_collection(OrderCycle, :id, ["1", "2", "3"]) + end + end + + context "when order cycles are allowed" do + let(:params) { { order_cycle_ids: ["1", "3"] } } + + it "does not raise error" do + expect { authorizer.authorize! }.not_to raise_error + end + end + + context "when an order cycle is not allowed" do + let(:params) { { order_cycle_ids: ["1", "4"] } } + + it "raises ParameterNotAllowedError" do + expect { authorizer.authorize! } + .to raise_error(ParameterNotAllowedError) + end + end + end + + context "for enterprise fees" do + before do + allow(permissions).to receive(:allowed_enterprise_fees) do + stub_model_collection(EnterpriseFee, :id, ["1", "2", "3"]) + end + end + + context "when enterprise fees are allowed" do + let(:params) { { enterprise_fee_ids: ["1", "3"] } } + + it "does not raise error" do + expect { authorizer.authorize! }.not_to raise_error + end + end + + context "when an enterprise fee is not allowed" do + let(:params) { { enterprise_fee_ids: ["1", "4"] } } + + it "raises ParameterNotAllowedError" do + expect { authorizer.authorize! } + .to raise_error(ParameterNotAllowedError) + end + end + end + + context "for shipping methods" do + before do + allow(permissions).to receive(:allowed_shipping_methods) do + stub_model_collection(Spree::ShippingMethod, :id, ["1", "2", "3"]) + end + end + + context "when shipping methods are allowed" do + let(:params) { { shipping_method_ids: ["1", "3"] } } + + it "does not raise error" do + expect { authorizer.authorize! }.not_to raise_error + end + end + + context "when a shipping method is not allowed" do + let(:params) { { shipping_method_ids: ["1", "4"] } } + + it "raises ParameterNotAllowedError" do + expect { authorizer.authorize! } + .to raise_error(ParameterNotAllowedError) + end + end + end + + context "for payment methods" do + before do + allow(permissions).to receive(:allowed_payment_methods) do + stub_model_collection(Spree::PaymentMethod, :id, ["1", "2", "3"]) + end + end + + context "when payment methods are allowed" do + let(:params) { { payment_method_ids: ["1", "3"] } } + + it "does not raise error" do + expect { authorizer.authorize! }.not_to raise_error + end + end + + context "when a payment method is not allowed" do + let(:params) { { payment_method_ids: ["1", "4"] } } + + it "raises ParameterNotAllowedError" do + expect { authorizer.authorize! } + .to raise_error(ParameterNotAllowedError) + end + end + end + + def stub_model_collection(model, attribute_name, attribute_list) + attribute_list.map do |attribute_value| + stub_model(model, attribute_name => attribute_value) + end + end + + def stub_model(model, params) + model.new.tap do |instance| + instance.stub(params) + end + end + end + end + end +end diff --git a/engines/order_management/spec/services/order_management/reports/enterprise_fee_summary/enterprise_fee_summary_report_spec.rb b/spec/lib/reports/enterprise_fee_summary/enterprise_fee_summary_report_spec.rb similarity index 99% rename from engines/order_management/spec/services/order_management/reports/enterprise_fee_summary/enterprise_fee_summary_report_spec.rb rename to spec/lib/reports/enterprise_fee_summary/enterprise_fee_summary_report_spec.rb index d18a21b83e..c5db21860a 100644 --- a/engines/order_management/spec/services/order_management/reports/enterprise_fee_summary/enterprise_fee_summary_report_spec.rb +++ b/spec/lib/reports/enterprise_fee_summary/enterprise_fee_summary_report_spec.rb @@ -3,8 +3,8 @@ require "spec_helper" # rubocop:disable Layout/LineLength -# describe OrderManagement::Reports::EnterpriseFeeSummary::EnterpriseFeeSummaryReport do -# let(:report_klass) { OrderManagement::Reports::EnterpriseFeeSummary } +# describe Reporting::Reports::EnterpriseFeeSummary::EnterpriseFeeSummaryReport do +# let(:report_klass) { Reporting::Reports::EnterpriseFeeSummary } # # Basic data. # let!(:shipping_method) do diff --git a/spec/lib/reports/enterprise_fee_summary/parameters_spec.rb b/spec/lib/reports/enterprise_fee_summary/parameters_spec.rb new file mode 100644 index 0000000000..85689cab90 --- /dev/null +++ b/spec/lib/reports/enterprise_fee_summary/parameters_spec.rb @@ -0,0 +1,92 @@ +# frozen_string_literal: true + +require "spec_helper" + +require "date_time_string_validator" + +module Reporting + module Reports + module EnterpriseFeeSummary + describe Parameters do + describe "validation" do + let(:parameters) { described_class.new } + + it "allows all parameters to be blank" do + expect(parameters).to be_valid + end + + context "for type of parameters" do + it { is_expected.to validate_date_time_format_of(:start_at) } + it { is_expected.to validate_date_time_format_of(:end_at) } + it { is_expected.to validate_integer_array(:distributor_ids) } + it { is_expected.to validate_integer_array(:producer_ids) } + it { is_expected.to validate_integer_array(:order_cycle_ids) } + it { is_expected.to validate_integer_array(:enterprise_fee_ids) } + it { is_expected.to validate_integer_array(:shipping_method_ids) } + it { is_expected.to validate_integer_array(:payment_method_ids) } + + it "allows integer arrays to include blank string and cleans it up" do + subject.distributor_ids = ["", "1"] + subject.producer_ids = ["", "1"] + subject.order_cycle_ids = ["", "1"] + subject.enterprise_fee_ids = ["", "1"] + subject.shipping_method_ids = ["", "1"] + subject.payment_method_ids = ["", "1"] + + expect(subject).to be_valid + + expect(subject.distributor_ids).to eq(["1"]) + expect(subject.producer_ids).to eq(["1"]) + expect(subject.order_cycle_ids).to eq(["1"]) + expect(subject.enterprise_fee_ids).to eq(["1"]) + expect(subject.shipping_method_ids).to eq(["1"]) + expect(subject.payment_method_ids).to eq(["1"]) + end + + describe "requiring start_at to be before end_at" do + let(:now) { Time.zone.now.utc } + + it "adds error when start_at is after end_at" do + allow(subject).to receive(:start_at) { now.to_s } + allow(subject).to receive(:end_at) { (now - 1.hour).to_s } + + expect(subject).not_to be_valid + error_message = described_class.date_end_before_start_error_message + expect(subject.errors[:end_at]).to eq([error_message]) + end + + it "does not add error when start_at is before end_at" do + allow(subject).to receive(:start_at) { now.to_s } + allow(subject).to receive(:end_at) { (now + 1.hour).to_s } + + expect(subject).to be_valid + end + end + end + end + + describe "smoke authorization" do + let!(:order_cycle) { create(:order_cycle) } + let!(:user) { create(:user) } + + let(:permissions) do + Permissions.new(nil).tap do |instance| + instance.stub(allowed_order_cycles: [order_cycle]) + end + end + + it "does not raise error when the parameters are allowed" do + parameters = described_class.new(order_cycle_ids: [order_cycle.id.to_s]) + expect { parameters.authorize!(permissions) }.not_to raise_error + end + + it "raises error when the parameters are not allowed" do + parameters = described_class.new(order_cycle_ids: [(order_cycle.id + 1).to_s]) + expect { parameters.authorize!(permissions) } + .to raise_error(ParameterNotAllowedError) + end + end + end + end + end +end diff --git a/engines/order_management/spec/services/order_management/reports/enterprise_fee_summary/permissions_spec.rb b/spec/lib/reports/enterprise_fee_summary/permissions_spec.rb similarity index 99% rename from engines/order_management/spec/services/order_management/reports/enterprise_fee_summary/permissions_spec.rb rename to spec/lib/reports/enterprise_fee_summary/permissions_spec.rb index 39334f6dc0..11530314d5 100644 --- a/engines/order_management/spec/services/order_management/reports/enterprise_fee_summary/permissions_spec.rb +++ b/spec/lib/reports/enterprise_fee_summary/permissions_spec.rb @@ -2,7 +2,7 @@ require "spec_helper" -describe OrderManagement::Reports::EnterpriseFeeSummary::Permissions do +describe Reporting::Reports::EnterpriseFeeSummary::Permissions do let!(:order_cycle) { create(:simple_order_cycle) } let!(:incoming_exchange) { create(:exchange, incoming: true, order_cycle: order_cycle) } let!(:outgoing_exchange) { create(:exchange, incoming: false, order_cycle: order_cycle) } diff --git a/engines/order_management/spec/services/order_management/reports/enterprise_fee_summary/renderers/csv_renderer_spec.rb b/spec/lib/reports/enterprise_fee_summary/renderers/csv_renderer_spec.rb similarity index 95% rename from engines/order_management/spec/services/order_management/reports/enterprise_fee_summary/renderers/csv_renderer_spec.rb rename to spec/lib/reports/enterprise_fee_summary/renderers/csv_renderer_spec.rb index f21f44c472..91046372fd 100644 --- a/engines/order_management/spec/services/order_management/reports/enterprise_fee_summary/renderers/csv_renderer_spec.rb +++ b/spec/lib/reports/enterprise_fee_summary/renderers/csv_renderer_spec.rb @@ -2,8 +2,8 @@ require "spec_helper" -# describe OrderManagement::Reports::EnterpriseFeeSummary::Renderers::CsvRenderer do -# let(:report_klass) { OrderManagement::Reports::EnterpriseFeeSummary } +# describe Reporting::Reports::EnterpriseFeeSummary::Renderers::CsvRenderer do +# let(:report_klass) { Reporting::Reports::EnterpriseFeeSummary } # let!(:permissions) { report_klass::Permissions.new(current_user) } # let!(:parameters) { report_klass::Parameters.new } diff --git a/engines/order_management/spec/services/order_management/reports/enterprise_fee_summary/renderers/html_renderer_spec.rb b/spec/lib/reports/enterprise_fee_summary/renderers/html_renderer_spec.rb similarity index 89% rename from engines/order_management/spec/services/order_management/reports/enterprise_fee_summary/renderers/html_renderer_spec.rb rename to spec/lib/reports/enterprise_fee_summary/renderers/html_renderer_spec.rb index dd5a4a301c..b396458e0f 100644 --- a/engines/order_management/spec/services/order_management/reports/enterprise_fee_summary/renderers/html_renderer_spec.rb +++ b/spec/lib/reports/enterprise_fee_summary/renderers/html_renderer_spec.rb @@ -2,12 +2,12 @@ require "spec_helper" -# describe OrderManagement::Reports::EnterpriseFeeSummary::Renderers::HtmlRenderer do -# let(:report_klass) { OrderManagement::Reports::EnterpriseFeeSummary } +# describe Reporting::Reports::EnterpriseFeeSummary::Renderers::HtmlRenderer do +# let(:report_klass) { Reporting::Reports::EnterpriseFeeSummary } # let!(:permissions) { report_klass::Permissions.new(current_user) } # let!(:parameters) { report_klass::Parameters.new } -# let!(:controller) { OrderManagement::Reports::EnterpriseFeeSummariesController.new } +# let!(:controller) { Reporting::Reports::EnterpriseFeeSummariesController.new } # let!(:service) { report_klass::ReportService.new(permissions, parameters) } # let!(:renderer) { described_class.new(service) } diff --git a/engines/order_management/spec/services/order_management/reports/enterprise_fee_summary/report_data/enterprise_fee_type_total_spec.rb b/spec/lib/reports/enterprise_fee_summary/report_data/enterprise_fee_type_total_spec.rb similarity index 92% rename from engines/order_management/spec/services/order_management/reports/enterprise_fee_summary/report_data/enterprise_fee_type_total_spec.rb rename to spec/lib/reports/enterprise_fee_summary/report_data/enterprise_fee_type_total_spec.rb index a091896a3d..5f26970c00 100644 --- a/engines/order_management/spec/services/order_management/reports/enterprise_fee_summary/report_data/enterprise_fee_type_total_spec.rb +++ b/spec/lib/reports/enterprise_fee_summary/report_data/enterprise_fee_type_total_spec.rb @@ -2,7 +2,7 @@ require "spec_helper" -describe OrderManagement::Reports::EnterpriseFeeSummary::ReportData::EnterpriseFeeTypeTotal do +describe Reporting::Reports::EnterpriseFeeSummary::ReportData::EnterpriseFeeTypeTotal do it "sorts instances according to their attributes" do instance_a = described_class.new( fee_type: "sales", diff --git a/spec/lib/reports/lettuce_share_report_spec.rb b/spec/lib/reports/lettuce_share_report_spec.rb new file mode 100644 index 0000000000..6fd5b8fca5 --- /dev/null +++ b/spec/lib/reports/lettuce_share_report_spec.rb @@ -0,0 +1,95 @@ +# frozen_string_literal: true + +require 'spec_helper' + +module Reporting + module Reports + module ProductsAndInventory + describe LettuceShareReport do + let(:user) { create(:user) } + let(:base_report) { + ProductsAndInventoryReport.new(user, { report_subtype: 'lettuce_share' }, true) + } + let(:report) { base_report.report } + let(:variant) { create(:variant) } + + describe "grower and method" do + it "shows just the producer when there is no certification" do + allow(report).to receive(:producer_name) { "Producer" } + allow(report).to receive(:certification) { "" } + + expect(report.__send__(:grower_and_method, variant)).to eq("Producer") + end + + it "shows producer and certification when a certification is present" do + allow(report).to receive(:producer_name) { "Producer" } + allow(report).to receive(:certification) { "Method" } + + expect(report.__send__(:grower_and_method, variant)).to eq("Producer (Method)") + end + end + + describe "gst" do + it "handles tax category without rates" do + expect(report.__send__(:gst, variant)).to eq(0) + end + end + + describe "table" do + it "handles no items" do + expect(report.table_rows).to eq [] + end + + describe "lists" do + let(:variant2) { create(:variant) } + let(:variant3) { create(:variant) } + let(:variant4) { create(:variant, on_hand: 0, on_demand: true) } + let(:hub_address) { + create(:address, address1: "distributor address", city: 'The Shire', zipcode: "1234") + } + let(:hub) { create(:distributor_enterprise, address: hub_address) } + let(:variant2_override) { create(:variant_override, hub: hub, variant: variant2) } + let(:variant3_override) { + create(:variant_override, hub: hub, variant: variant3, count_on_hand: 0) + } + + it "all items" do + allow(base_report).to receive(:child_variants) { + Spree::Variant.where(id: [variant, variant2, variant3]) + } + expect(report.table_rows.count).to eq 3 + end + + it "only available items" do + variant.on_hand = 0 + allow(base_report).to receive(:child_variants) { + Spree::Variant.where(id: [variant, variant2, variant3, + variant4]) + } + expect(report.table_rows.count).to eq 3 + end + + it "only available items considering overrides" do + create(:exchange, incoming: false, receiver_id: hub.id, + variants: [variant, variant2, variant3]) + # create the overrides + variant2_override + variant3_override + allow(base_report).to receive(:child_variants) { + Spree::Variant.where(id: [variant, variant2, variant3]) + } + allow(base_report).to receive(:params) { + { distributor_id: hub.id, report_subtype: 'lettuce_share' } + } + rows = report.table_rows + expect(rows.count).to eq 2 + expect(rows.map{ |row| + row[0] + } ).to include variant.product.name, variant2.product.name + end + end + end + end + end + end +end diff --git a/spec/lib/open_food_network/reports/line_items_spec.rb b/spec/lib/reports/line_items_spec.rb similarity index 93% rename from spec/lib/open_food_network/reports/line_items_spec.rb rename to spec/lib/reports/line_items_spec.rb index 34f8f359bf..523d2234bc 100644 --- a/spec/lib/open_food_network/reports/line_items_spec.rb +++ b/spec/lib/reports/line_items_spec.rb @@ -1,9 +1,8 @@ # frozen_string_literal: true require 'spec_helper' -require 'open_food_network/reports/line_items' -describe OpenFoodNetwork::Reports::LineItems do +describe Reporting::LineItems do subject(:reports_line_items) { described_class.new(order_permissions, params) } # This object lets us add some test coverage despite the very deep coupling between the class diff --git a/spec/lib/reports/order_cycle_management_report_spec.rb b/spec/lib/reports/order_cycle_management_report_spec.rb new file mode 100644 index 0000000000..7cd8160ab0 --- /dev/null +++ b/spec/lib/reports/order_cycle_management_report_spec.rb @@ -0,0 +1,216 @@ +# frozen_string_literal: true + +require 'spec_helper' + +module Reporting + module Reports + module OrderCycleManagement + describe OrderCycleManagementReport do + context "as a site admin" do + subject { OrderCycleManagementReport.new(user, params, true) } + let(:params) { {} } + + let(:user) do + user = create(:user) + user.spree_roles << Spree::Role.find_or_create_by!(name: "admin") + user + end + + describe "fetching orders" do + let(:customers_with_balance) { instance_double(CustomersWithBalance) } + + it 'calls the OutstandingBalance query object' do + outstanding_balance = instance_double(OutstandingBalance, query: Spree::Order.none) + expect(OutstandingBalance).to receive(:new).and_return(outstanding_balance) + + subject.orders + end + + it "fetches completed orders" do + o1 = create(:order) + o2 = create(:order, completed_at: 1.day.ago, state: 'complete') + expect(subject.orders).to eq([o2]) + end + + it 'fetches resumed orders' do + order = create(:order, state: 'resumed', completed_at: 1.day.ago) + expect(subject.orders).to eq([order]) + end + + it 'orders them by id' do + order1 = create(:order, completed_at: 1.day.ago, state: 'complete') + order2 = create(:order, completed_at: 2.days.ago, state: 'complete') + + expect(subject.orders.pluck(:id)).to eq([order2.id, order1.id]) + end + + it "does not show cancelled orders" do + o1 = create(:order, state: 'canceled', completed_at: 1.day.ago) + o2 = create(:order, state: 'complete', completed_at: 1.day.ago) + expect(subject.orders).to eq([o2]) + end + + context "default date range" do + it "fetches orders completed in the past month" do + o1 = create(:order, state: 'complete', completed_at: 1.month.ago - 1.day) + o2 = create(:order, state: 'complete', completed_at: 1.month.ago + 1.day) + expect(subject.orders).to eq([o2]) + end + end + end + end + + context "as an enterprise user" do + let!(:user) { create(:user) } + + subject { OrderCycleManagementReport.new user, {}, true } + + describe "fetching orders" do + let(:supplier) { create(:supplier_enterprise) } + let(:product) { create(:simple_product, supplier: supplier) } + let(:order) { create(:order, completed_at: 1.day.ago) } + + it "only shows orders managed by the current user" do + d1 = create(:distributor_enterprise) + d1.enterprise_roles.create!(user: user) + d2 = create(:distributor_enterprise) + d2.enterprise_roles.create!(user: create(:user)) + + o1 = create(:order, distributor: d1, state: 'complete', completed_at: 1.day.ago) + o2 = create(:order, distributor: d2, state: 'complete', completed_at: 1.day.ago) + + expect(subject).to receive(:filter).with([o1]).and_return([o1]) + expect(subject.orders).to eq([o1]) + end + + it "does not show orders through a hub that the current user does not manage" do + # Given a supplier enterprise with an order for one of its products + supplier.enterprise_roles.create!(user: user) + order.line_items << create(:line_item_with_shipment, product: product) + + # When I fetch orders, I should see no orders + expect(subject).to receive(:filter).with([]).and_return([]) + expect(subject.orders).to eq([]) + end + end + + describe "filtering orders" do + let!(:orders) { Spree::Order.where(nil) } + let!(:supplier) { create(:supplier_enterprise) } + + let!(:oc1) { create(:simple_order_cycle) } + let!(:pm1) { create(:payment_method, name: "PM1") } + let!(:sm1) { create(:shipping_method, name: "ship1") } + let!(:s1) { create(:shipment_with, :shipping_method, shipping_method: sm1) } + let!(:order1) { create(:order, shipments: [s1], order_cycle: oc1) } + let!(:payment1) { create(:payment, order: order1, payment_method: pm1) } + + it "returns all orders sans-params" do + expect(subject.filter(orders)).to eq(orders) + end + + it "filters to a specific order cycle" do + oc2 = create(:simple_order_cycle) + order2 = create(:order, order_cycle: oc2) + + allow(subject).to receive(:params).and_return(order_cycle_id: oc1.id) + expect(subject.filter(orders)).to eq([order1]) + end + + it "filters to a payment method" do + pm2 = create(:payment_method, name: "PM2") + pm3 = create(:payment_method, name: "PM3") + order2 = create(:order, payments: [create(:payment, payment_method: pm2)]) + order3 = create(:order, payments: [create(:payment, payment_method: pm3)]) + + allow(subject).to receive(:params).and_return(payment_method_in: [pm1.id, pm3.id] ) + expect(subject.filter(orders)).to match_array [order1, order3] + end + + it "filters to a shipping method" do + sm2 = create(:shipping_method, name: "ship2") + sm3 = create(:shipping_method, name: "ship3") + s2 = create(:shipment_with, :shipping_method, shipping_method: sm2) + s3 = create(:shipment_with, :shipping_method, shipping_method: sm3) + order2 = create(:order, shipments: [s2]) + order3 = create(:order, shipments: [s3]) + + allow(subject).to receive(:params).and_return(shipping_method_in: [sm1.id, sm3.id]) + expect(subject.filter(orders)).to match_array [order1, order3] + end + + it "should do all the filters at once" do + allow(subject).to receive(:params).and_return(order_cycle_id: oc1.id, + shipping_method_name: sm1.name, + payment_method_name: pm1.name) + expect(subject.filter(orders)).to eq([order1]) + end + end + + describe '#table_rows' do + subject { OrderCycleManagementReport.new(user, params, true) } + + let(:distributor) { create(:distributor_enterprise) } + before { distributor.enterprise_roles.create!(user: user) } + + context 'when the report type is payment_methods' do + let(:params) { { report_subtype: 'payment_methods' } } + + let!(:order) do + create( + :completed_order_with_totals, + distributor: distributor, + completed_at: 1.day.ago + ) + end + + it 'returns rows with payment information' do + expect(subject.table_rows).to eq([[ + order.billing_address.firstname, + order.billing_address.lastname, + order.distributor.name, + '', + order.email, + order.billing_address.phone, + order.shipment.shipping_method.name, + nil, + order.total, + -order.total + ]]) + end + end + + context 'when the report type is not payment_methods' do + let(:params) { {} } + let!(:order) do + create( + :completed_order_with_totals, + distributor: distributor, + completed_at: 1.day.ago + ) + end + + it 'returns rows with delivery information' do + expect(subject.table_rows).to eq([[ + order.ship_address.firstname, + order.ship_address.lastname, + order.distributor.name, + "", + "#{order.ship_address.address1} #{order.ship_address.address2} #{order.ship_address.city}", + order.ship_address.zipcode, + order.ship_address.phone, + order.shipment.shipping_method.name, + nil, + order.total, + -order.total, + false, + order.special_instructions + ]]) + end + end + end + end + end + end + end +end diff --git a/spec/lib/open_food_network/order_grouper_spec.rb b/spec/lib/reports/order_grouper_spec.rb similarity index 97% rename from spec/lib/open_food_network/order_grouper_spec.rb rename to spec/lib/reports/order_grouper_spec.rb index db6538efec..630b5de196 100644 --- a/spec/lib/open_food_network/order_grouper_spec.rb +++ b/spec/lib/reports/order_grouper_spec.rb @@ -1,9 +1,8 @@ # frozen_string_literal: true require 'spec_helper' -require 'open_food_network/order_grouper' -module OpenFoodNetwork +module Reporting describe OrderGrouper do before(:each) do @items = [1, 2, 3, 4] @@ -84,7 +83,7 @@ module OpenFoodNetwork subject = OrderGrouper.new @rules, @columns grouped_tree = double(:grouped_tree) - expect(subject).to receive(:group_and_sort).with(@rule1, @rules[1..-1], + expect(subject).to receive(:group_and_sort).with(@rule1, @rules[1..], @items).and_return(grouped_tree) expect(subject.build_tree(@items, @rules)).to eq(grouped_tree) @@ -101,7 +100,7 @@ module OpenFoodNetwork expect(@items).to receive(:group_by).and_return(groups) sorted_groups = {} 1.upto(number_of_categories) { |i| - sorted_groups[i] = double(:group, name: "Group " + i.to_s ) + sorted_groups[i] = double(:group, name: "Group #{i}" ) } expect(groups).to receive(:sort_by).and_return(sorted_groups) group = { group1: 1, group2: 2, group3: 3 } diff --git a/spec/lib/reports/orders_and_distributors_report_spec.rb b/spec/lib/reports/orders_and_distributors_report_spec.rb new file mode 100644 index 0000000000..ca9a8eaaa6 --- /dev/null +++ b/spec/lib/reports/orders_and_distributors_report_spec.rb @@ -0,0 +1,90 @@ +# frozen_string_literal: true + +require 'spec_helper' + +module Reporting + module Reports + module OrdersAndDistributors + describe OrdersAndDistributorsReport do + describe 'orders and distributors report' do + it 'should return a header row describing the report' do + subject = OrdersAndDistributorsReport.new nil + + expect(subject.table_headers).to eq( + [ + 'Order date', 'Order Id', + 'Customer Name', 'Customer Email', 'Customer Phone', 'Customer City', + 'SKU', 'Item name', 'Variant', 'Quantity', 'Max Quantity', 'Cost', 'Shipping Cost', + 'Payment Method', + 'Distributor', 'Distributor address', 'Distributor city', 'Distributor postcode', + 'Shipping Method', 'Shipping instructions' + ] + ) + end + + context 'with completed order' do + let(:bill_address) { create(:address) } + let(:distributor) { create(:distributor_enterprise) } + let(:product) { create(:product) } + let(:shipping_method) { create(:shipping_method) } + let(:shipping_instructions) { 'pick up on thursday please!' } + let(:order) { + create(:order, + state: 'complete', completed_at: Time.zone.now, + distributor: distributor, bill_address: bill_address, + special_instructions: shipping_instructions) + } + let(:payment_method) { create(:payment_method, distributors: [distributor]) } + let(:payment) { create(:payment, payment_method: payment_method, order: order) } + let(:line_item) { create(:line_item_with_shipment, product: product, order: order) } + + before do + order.select_shipping_method(shipping_method.id) + order.payments << payment + order.line_items << line_item + end + + it 'should denormalise order and distributor details for display as csv' do + subject = OrdersAndDistributorsReport.new create(:admin_user), {}, true + + table = subject.table_rows + + expect(table.size).to eq 1 + expect(table[0]).to eq([ + order.reload.completed_at.strftime("%F %T"), + order.id, + bill_address.full_name, + order.email, + bill_address.phone, + bill_address.city, + line_item.product.sku, + line_item.product.name, + line_item.options_text, + line_item.quantity, + line_item.max_quantity, + line_item.price * line_item.quantity, + line_item.distribution_fee, + payment_method.name, + distributor.name, + distributor.address.address1, + distributor.address.city, + distributor.address.zipcode, + shipping_method.name, + shipping_instructions + ]) + end + + it "prints one row per line item" do + create(:line_item_with_shipment, order: order) + + subject = OrdersAndDistributorsReport.new(create(:admin_user), {}, true) + + table = subject.table_rows + expect(table.size).to eq 2 + end + end + end + end + end + end +end diff --git a/spec/lib/reports/orders_and_fulfillment/customer_totals_report_spec.rb b/spec/lib/reports/orders_and_fulfillment/customer_totals_report_spec.rb new file mode 100644 index 0000000000..6c990d4bc0 --- /dev/null +++ b/spec/lib/reports/orders_and_fulfillment/customer_totals_report_spec.rb @@ -0,0 +1,131 @@ +# frozen_string_literal: true + +require "spec_helper" + +module Reporting + module Reports + module OrdersAndFulfillment + describe CustomerTotalsReport do + let!(:distributor) { create(:distributor_enterprise) } + let!(:customer) { create(:customer, enterprise: distributor) } + let(:current_user) { distributor.owner } + + let(:report) do + report_options = { report_subtype: described_class::REPORT_TYPE } + OrdersAndFulfillmentReport.new(current_user, report_options, true) + end + + let(:report_table) do + report.table_rows + end + + context "viewing the report" do + let!(:order) do + create(:completed_order_with_totals, line_items_count: 1, user: customer.user, + customer: customer, distributor: distributor) + end + + it "generates the report" do + expect(report_table.length).to eq(2) + end + + it "has a line item row" do + distributor_name_field = report_table.first[0] + expect(distributor_name_field).to eq distributor.name + + customer_name_field = report_table.first[1] + expect(customer_name_field).to eq order.bill_address.full_name + + total_field = report_table.last[5] + expect(total_field).to eq I18n.t("admin.reports.total") + end + + it 'includes the order number and date in item rows' do + order_number_and_date_fields = report_table.first[33..34] + expect(order_number_and_date_fields).to eq([ + order.number, + order.completed_at.strftime("%F %T"), + ]) + end + + it 'includes the order number and date in total rows' do + order_number_and_date_fields = report_table.last[33..34] + expect(order_number_and_date_fields).to eq([ + order.number, + order.completed_at.strftime("%F %T"), + ]) + end + end + + context "loading shipping methods" do + let!(:shipping_method1) { + create(:shipping_method, distributors: [distributor], name: "First") + } + let!(:shipping_method2) { + create(:shipping_method, distributors: [distributor], name: "Second") + } + let!(:shipping_method3) { + create(:shipping_method, distributors: [distributor], name: "Third") + } + let!(:order) do + create(:completed_order_with_totals, line_items_count: 1, user: customer.user, + customer: customer, distributor: distributor) + end + + before do + order.shipments.each(&:refresh_rates) + order.select_shipping_method(shipping_method2.id) + end + + it "displays the correct shipping_method" do + shipping_method_name_field = report_table.first[15] + expect(shipping_method_name_field).to eq shipping_method2.name + end + end + + context "displaying payment fees" do + context "with both failed and completed payments present" do + let!(:order) { + create(:order_ready_to_ship, user: customer.user, + customer: customer, distributor: distributor) + } + let(:completed_payment) { order.payments.completed.first } + let!(:failed_payment) { create(:payment, order: order, state: "failed") } + + before do + completed_payment.adjustment.update amount: 123.00 + failed_payment.adjustment.update amount: 456.00, eligible: false, state: "finalized" + end + + it "shows the correct payment fee amount for the order" do + payment_fee_field = report_table.last[12] + expect(payment_fee_field).to eq completed_payment.adjustment.amount + end + end + end + + context 'when a variant override applies' do + let!(:order) do + create(:completed_order_with_totals, line_items_count: 1, user: customer.user, + customer: customer, distributor: distributor) + end + let(:overidden_sku) { 'magical_sku' } + + before do + create( + :variant_override, + hub: distributor, + variant: order.line_items.first.variant, + sku: overidden_sku + ) + end + + it 'uses the sku from the variant override' do + sku_field = report_table.first[23] + expect(sku_field).to eq overidden_sku + end + end + end + end + end +end diff --git a/spec/lib/reports/orders_and_fulfillment/distributor_totals_by_supplier_report_spec.rb b/spec/lib/reports/orders_and_fulfillment/distributor_totals_by_supplier_report_spec.rb new file mode 100644 index 0000000000..665cce0c67 --- /dev/null +++ b/spec/lib/reports/orders_and_fulfillment/distributor_totals_by_supplier_report_spec.rb @@ -0,0 +1,44 @@ +# frozen_string_literal: true + +require 'spec_helper' + +module Reporting + module Reports + module OrdersAndFulfillment + describe DistributorTotalsBySupplierReport do + let!(:distributor) { create(:distributor_enterprise) } + + let!(:order) do + create(:completed_order_with_totals, line_items_count: 1, distributor: distributor) + end + + let(:current_user) { distributor.owner } + + let(:report) do + report_options = { report_subtype: described_class::REPORT_TYPE } + OrdersAndFulfillmentReport.new(current_user, report_options, true) + end + + let(:report_table) do + report.table_rows + end + + it "generates the report" do + expect(report_table.length).to eq(2) + end + + it "has a variant row under the distributor" do + distributor_name_field = report_table.first[0] + expect(distributor_name_field).to eq distributor.name + + supplier = order.line_items.first.variant.product.supplier + supplier_name_field = report_table.first[1] + expect(supplier_name_field).to eq supplier.name + + total_field = report_table.last[1] + expect(total_field).to eq I18n.t("admin.reports.total") + end + end + end + end +end diff --git a/spec/lib/reports/orders_and_fulfillment/orders_and_fulfillment_report_spec.rb b/spec/lib/reports/orders_and_fulfillment/orders_and_fulfillment_report_spec.rb new file mode 100644 index 0000000000..7d20f85638 --- /dev/null +++ b/spec/lib/reports/orders_and_fulfillment/orders_and_fulfillment_report_spec.rb @@ -0,0 +1,284 @@ +# frozen_string_literal: true + +require 'spec_helper' + +module Reporting + module Reports + module OrdersAndFulfillment + describe OrdersAndFulfillmentReport do + include AuthenticationHelper + + let(:distributor) { create(:distributor_enterprise) } + let(:order_cycle) { create(:simple_order_cycle) } + let(:address) { create(:address) } + let(:order) { + create( + :order, + completed_at: 1.day.ago, + order_cycle: order_cycle, + distributor: distributor, + bill_address: address + ) + } + let(:line_item) { build(:line_item_with_shipment) } + let(:user) { create(:user) } + let(:admin_user) { create(:admin_user) } + + describe "fetching orders" do + before { order.line_items << line_item } + + context "as a site admin" do + subject { described_class.new(admin_user, {}, true) } + + it "fetches completed orders" do + o2 = create(:order) + o2.line_items << build(:line_item) + expect(subject.table_items).to eq([line_item]) + end + + it "does not show cancelled orders" do + o2 = create(:order, state: "canceled", completed_at: 1.day.ago) + o2.line_items << build(:line_item_with_shipment) + expect(subject.table_items).to eq([line_item]) + end + end + + context "as a manager of a supplier" do + subject { described_class.new(user, {}, true) } + + let(:s1) { create(:supplier_enterprise) } + + before do + s1.enterprise_roles.create!(user: user) + end + + context "that has granted P-OC to the distributor" do + let(:o2) { + create( + :order, + distributor: distributor, + completed_at: 1.day.ago, + bill_address: create(:address), + ship_address: create(:address) + ) + } + let(:li2) { + build(:line_item_with_shipment, product: create(:simple_product, supplier: s1)) + } + + before do + o2.line_items << li2 + create( + :enterprise_relationship, + parent: s1, + child: distributor, + permissions_list: [:add_to_order_cycle] + ) + end + + it "shows line items supplied by my producers, with names hidden" do + expect(subject.table_items).to eq([li2]) + expect(subject.table_items.first.order.bill_address.firstname).to eq("HIDDEN") + end + + context "where the distributor allows suppliers to see customer names" do + before do + distributor.update_columns show_customer_names_to_suppliers: true + end + + it "shows line items supplied by my producers, with names shown" do + expect(subject.table_items).to eq([li2]) + expect(subject.table_items.first.order.bill_address.firstname). + to eq(order.bill_address.firstname) + end + end + end + + context "that has not granted P-OC to the distributor" do + let(:o2) { + create( + :order, + distributor: distributor, + completed_at: 1.day.ago, + bill_address: create(:address), + ship_address: create(:address) + ) + } + let(:li2) { + build(:line_item_with_shipment, product: create(:simple_product, supplier: s1)) + } + + before do + o2.line_items << li2 + end + + it "does not show line items supplied by my producers" do + expect(subject.table_items).to eq([]) + end + + context "where the distributor allows suppliers to see customer names" do + before do + distributor.show_customer_names_to_suppliers = true + end + + it "does not show line items supplied by my producers" do + expect(subject.table_items).to eq([]) + end + end + end + end + + context "as a manager of a distributor" do + subject { described_class.new(user, {}, true) } + + before do + distributor.enterprise_roles.create!(user: user) + end + + it "only shows line items distributed by enterprises managed by the current user" do + d2 = create(:distributor_enterprise) + d2.enterprise_roles.create!(user: create(:user)) + o2 = create(:order, distributor: d2, completed_at: 1.day.ago) + o2.line_items << build(:line_item_with_shipment) + expect(subject.table_items).to eq([line_item]) + end + + it "only shows the selected order cycle" do + oc2 = create(:simple_order_cycle) + o2 = create(:order, distributor: distributor, order_cycle: oc2) + o2.line_items << build(:line_item) + allow(subject).to receive(:params).and_return(order_cycle_id_in: order_cycle.id) + expect(subject.table_items).to eq([line_item]) + end + end + end + + describe "columns are aligned" do + it 'has aligned columsn' do + report_types = [ + "", + "order_cycle_supplier_totals", + "order_cycle_supplier_totals_by_distributor", + "order_cycle_distributor_totals_by_supplier", + "order_cycle_customer_totals" + ] + + report_types.each do |report_type| + report = described_class.new(admin_user, report_subtype: report_type) + expect(report.table_headers.size).to eq(report.columns.size) + end + end + end + + describe "order_cycle_customer_totals" do + let!(:product) { line_item.product } + let!(:fuji) do + create(:variant, product: product, display_name: "Fuji", sku: "FUJI", on_hand: 100) + end + let!(:gala) do + create(:variant, product: product, display_name: "Gala", sku: "GALA", on_hand: 100) + end + + let(:items) { + described_class.new(admin_user, { report_subtype: "order_cycle_customer_totals" }, true) + .table_rows + } + + before do + # Clear price so it will be computed based on quantity and variant price. + order.line_items << build(:line_item_with_shipment, variant: fuji, price: nil, + quantity: 1) + order.line_items << build(:line_item_with_shipment, variant: gala, price: nil, + quantity: 2) + end + + it "has a product row" do + product_name_field = items.first[5] + expect(product_name_field).to eq product.name + end + + it "has a summary row" do + product_name_field = items.last[5] + expect(product_name_field).to eq "TOTAL" + end + + # Expected Report for Scenario: + # + # Row 1: Armstrong Amari, Fuji Apple, price: 8 + # Row 2: SUMMARY + # Row 3: Bartoletti Brooklyn, Fuji Apple, price: 1 + 4 + # Row 4: Bartoletti Brooklyn, Gala Apple, price: 2 + # Row 5: SUMMARY + describe "grouping of line items" do + let!(:address) { create(:address, last_name: "Bartoletti", first_name: "Brooklyn") } + + let!(:second_address) { create(:address, last_name: "Armstrong", first_name: "Amari") } + let!(:second_order) do + create(:order, completed_at: 1.day.ago, order_cycle: order_cycle, distributor: distributor, + bill_address: second_address) + end + + before do + # Add a second line item for Fuji variant to the order, to test grouping in this edge case. + order.line_items << build(:line_item_with_shipment, variant: fuji, price: nil, + quantity: 4) + + second_order.line_items << build(:line_item_with_shipment, variant: fuji, price: nil, + quantity: 8) + end + + it "groups line items by variant and order" do + expect(items.length).to eq(5) + + # Row 1: Armstrong Amari, Fuji Apple, price: 8 + row_data = items[0] + expect(customer_name(row_data)).to eq(second_address.full_name) + expect(amount(row_data)).to eq(fuji.price * 8) + expect(variant_sku(row_data)).to eq(fuji.sku) + + # Row 2: SUMMARY + row_data = items[1] + expect(totals_row?(row_data)).to eq(true) + expect(customer_name(row_data)).to eq(second_address.full_name) + expect(amount(row_data)).to eq(fuji.price * 8) + + # Row 3: Bartoletti Brooklyn, Fuji Apple, price: 1 + 4 + row_data = items[2] + expect(customer_name(row_data)).to eq(address.full_name) + expect(amount(row_data)).to eq(fuji.price * 5) + expect(variant_sku(row_data)).to eq(fuji.sku) + + # Row 4: Bartoletti Brooklyn, Gala Apple, price: 2 + row_data = items[3] + expect(customer_name(row_data)).to eq(address.full_name) + expect(amount(row_data)).to eq(gala.price * 2) + expect(variant_sku(row_data)).to eq(gala.sku) + + # Row 5: SUMMARY + row_data = items[4] + expect(totals_row?(row_data)).to eq(true) + expect(customer_name(row_data)).to eq(address.full_name) + expect(amount(row_data)).to eq(fuji.price * 5 + gala.price * 2) + end + end + + def totals_row?(row_data) + row_data[5] == I18n.t("admin.reports.total") + end + + def customer_name(row_data) + row_data[1] + end + + def amount(row_data) + row_data[8] + end + + def variant_sku(row_data) + row_data[23] + end + end + end + end + end +end diff --git a/spec/lib/reports/orders_and_fulfillment/supplier_totals_by_distributor_report_spec.rb b/spec/lib/reports/orders_and_fulfillment/supplier_totals_by_distributor_report_spec.rb new file mode 100644 index 0000000000..26fd0b7d31 --- /dev/null +++ b/spec/lib/reports/orders_and_fulfillment/supplier_totals_by_distributor_report_spec.rb @@ -0,0 +1,44 @@ +# frozen_string_literal: true + +require 'spec_helper' + +module Reporting + module Reports + module OrdersAndFulfillment + describe Reporting::Reports::OrdersAndFulfillment::SupplierTotalsByDistributorReport do + let!(:distributor) { create(:distributor_enterprise) } + + let!(:order) do + create(:completed_order_with_totals, line_items_count: 1, distributor: distributor) + end + + let(:current_user) { distributor.owner } + + let(:report) do + report_options = { report_subtype: described_class::REPORT_TYPE } + OrdersAndFulfillmentReport.new(current_user, report_options, true) + end + + let(:report_table) do + report.table_rows + end + + it "generates the report" do + expect(report_table.length).to eq(2) + end + + it "has a variant row under the distributor" do + supplier = order.line_items.first.variant.product.supplier + supplier_name_field = report_table.first[0] + expect(supplier_name_field).to eq supplier.name + + distributor_name_field = report_table.first[3] + expect(distributor_name_field).to eq distributor.name + + total_field = report_table.last[3] + expect(total_field).to eq I18n.t("admin.reports.total") + end + end + end + end +end diff --git a/spec/lib/reports/orders_and_fulfillment/supplier_totals_report_spec.rb b/spec/lib/reports/orders_and_fulfillment/supplier_totals_report_spec.rb new file mode 100644 index 0000000000..d21839bed7 --- /dev/null +++ b/spec/lib/reports/orders_and_fulfillment/supplier_totals_report_spec.rb @@ -0,0 +1,38 @@ +# frozen_string_literal: true + +require 'spec_helper' + +module Reporting + module Reports + module OrdersAndFulfillment + describe SupplierTotalsReport do + let!(:distributor) { create(:distributor_enterprise) } + + let!(:order) do + create(:completed_order_with_totals, line_items_count: 1, distributor: distributor) + end + + let(:current_user) { distributor.owner } + + let(:report) do + report_options = { report_subtype: described_class::REPORT_TYPE } + OrdersAndFulfillmentReport.new(current_user, report_options, true) + end + + let(:report_table) do + report.table_rows + end + + it "generates the report" do + expect(report_table.length).to eq(1) + end + + it "has a variant row" do + supplier = order.line_items.first.variant.product.supplier + supplier_name_field = report_table.first[0] + expect(supplier_name_field).to eq supplier.name + end + end + end + end +end diff --git a/spec/lib/reports/products_and_inventory_default_report_spec.rb b/spec/lib/reports/products_and_inventory_default_report_spec.rb new file mode 100644 index 0000000000..32f42f0c8d --- /dev/null +++ b/spec/lib/reports/products_and_inventory_default_report_spec.rb @@ -0,0 +1,267 @@ +# frozen_string_literal: true + +require 'spec_helper' + +module Reporting + module Reports + module ProductsAndInventory + describe ProductsAndInventoryDefaultReport do + context "As a site admin" do + let(:user) do + user = create(:user) + user.spree_roles << Spree::Role.find_or_create_by!(name: 'admin') + user + end + subject do + ProductsAndInventoryReport.new user, {}, true + end + + it "Should return headers" do + expect(subject.table_headers).to eq([ + "Supplier", + "Producer Suburb", + "Product", + "Product Properties", + "Taxons", + "Variant Value", + "Price", + "Group Buy Unit Quantity", + "Amount", + "SKU" + ]) + end + + it "should build a table from a list of variants" do + variant = double(:variant, sku: "sku", + full_name: "Variant Name", + count_on_hand: 10, + price: 100) + allow(variant).to receive_message_chain(:product, :supplier, + :name).and_return("Supplier") + allow(variant).to receive_message_chain(:product, :supplier, :address, + :city).and_return("A city") + allow(variant).to receive_message_chain(:product, :name).and_return("Product Name") + allow(variant).to receive_message_chain(:product, + :properties).and_return [double(name: "property1"), + double(name: "property2")] + allow(variant).to receive_message_chain(:product, + :taxons).and_return [double(name: "taxon1"), + double(name: "taxon2")] + allow(variant).to receive_message_chain(:product, :group_buy_unit_size).and_return(21) + allow(subject).to receive(:variants).and_return [variant] + + expect(subject.table_rows).to eq([[ + "Supplier", + "A city", + "Product Name", + "property1, property2", + "taxon1, taxon2", + "Variant Name", + 100, + 21, + "", + "sku" + ]]) + end + + it "fetches variants for some params" do + expect(subject).to receive(:child_variants).and_return ["children"] + expect(subject).to receive(:filter).with(['children']).and_return ["filter_children"] + expect(subject.variants).to eq(["filter_children"]) + end + end + + context "As an enterprise user" do + let(:supplier) { create(:supplier_enterprise) } + let(:enterprise_user) do + user = create(:user) + user.enterprise_roles.create(enterprise: supplier) + user.spree_roles = [] + user.save! + user + end + + subject do + ProductsAndInventoryReport.new enterprise_user, {}, true + end + + describe "fetching child variants" do + it "returns some variants" do + product1 = create(:simple_product, supplier: supplier) + variant1 = product1.variants.first + variant2 = create(:variant, product: product1) + + expect(subject.child_variants).to match_array [variant1, variant2] + end + + it "should only return variants managed by the user" do + product1 = create(:simple_product, supplier: create(:supplier_enterprise)) + product2 = create(:simple_product, supplier: supplier) + variant1 = product1.variants.first + variant2 = product2.variants.first + + expect(subject.child_variants).to eq([variant2]) + end + end + + describe "Filtering variants" do + let(:variants) { Spree::Variant.where(nil).joins(:product).where(is_master: false) } + + describe "based on report type" do + it "returns only variants on hand" do + product1 = create(:simple_product, supplier: supplier, on_hand: 99) + product2 = create(:simple_product, supplier: supplier, on_hand: 0) + + allow(subject).to receive(:params).and_return(report_subtype: 'inventory') + expect(subject.filter(variants)).to eq([product1.variants.first]) + end + end + it "filters to a specific supplier" do + supplier2 = create(:supplier_enterprise) + product1 = create(:simple_product, supplier: supplier) + product2 = create(:simple_product, supplier: supplier2) + + allow(subject).to receive(:params).and_return(supplier_id: supplier.id) + expect(subject.filter(variants)).to eq([product1.variants.first]) + end + it "filters to a specific distributor" do + distributor = create(:distributor_enterprise) + product1 = create(:simple_product, supplier: supplier) + product2 = create(:simple_product, supplier: supplier) + order_cycle = create(:simple_order_cycle, suppliers: [supplier], + distributors: [distributor], + variants: [product2.variants.first]) + + allow(subject).to receive(:params).and_return(distributor_id: distributor.id) + expect(subject.filter(variants)).to eq([product2.variants.first]) + end + + it "ignores variant overrides without filter" do + distributor = create(:distributor_enterprise) + product = create(:simple_product, supplier: supplier, price: 5) + variant = product.variants.first + order_cycle = create(:simple_order_cycle, suppliers: [supplier], + distributors: [distributor], + variants: [product.variants.first]) + create(:variant_override, hub: distributor, variant: variant, price: 2) + + result = subject.filter(variants) + + expect(result.first.price).to eq 5 + end + + it "considers variant overrides with distributor" do + distributor = create(:distributor_enterprise) + product = create(:simple_product, supplier: supplier, price: 5) + variant = product.variants.first + order_cycle = create(:simple_order_cycle, suppliers: [supplier], + distributors: [distributor], + variants: [product.variants.first]) + create(:variant_override, hub: distributor, variant: variant, price: 2) + + allow(subject).to receive(:params).and_return(distributor_id: distributor.id) + result = subject.filter(variants) + + expect(result.first.price).to eq 2 + end + + it "filters to a specific order cycle" do + distributor = create(:distributor_enterprise) + product1 = create(:simple_product, supplier: supplier) + product2 = create(:simple_product, supplier: supplier) + order_cycle = create(:simple_order_cycle, suppliers: [supplier], + distributors: [distributor], + variants: [product1.variants.first]) + + allow(subject).to receive(:params).and_return(order_cycle_id: order_cycle.id) + expect(subject.filter(variants)).to eq([product1.variants.first]) + end + + it "should do all the filters at once" do + # The following data ensures that this spec fails if any of the + # filters fail. It's testing the filters are not impacting each other. + distributor = create(:distributor_enterprise) + other_distributor = create(:distributor_enterprise) + other_supplier = create(:supplier_enterprise) + not_filtered_variant = create(:simple_product, supplier: supplier).variants.first + variant_filtered_by_order_cycle = create(:simple_product, + supplier: supplier).variants.first + variant_filtered_by_distributor = create(:simple_product, + supplier: supplier).variants.first + variant_filtered_by_supplier = create(:simple_product, + supplier: other_supplier).variants.first + variant_filtered_by_stock = create(:simple_product, supplier: supplier, + on_hand: 0).variants.first + + # This OC contains all products except the one that should be filtered + # by order cycle. We create a separate OC further down to proof that + # the product is passing all other filters. + order_cycle = create( + :simple_order_cycle, + suppliers: [supplier, other_supplier], + distributors: [distributor, other_distributor], + variants: [ + not_filtered_variant, + variant_filtered_by_distributor, + variant_filtered_by_supplier, + variant_filtered_by_stock + ] + ) + + # Remove the distribution of one product for one distributor but still + # sell it through the other distributor. + order_cycle.exchanges.outgoing.find_by(receiver_id: distributor.id). + exchange_variants. + find_by(variant_id: variant_filtered_by_distributor). + destroy + + # Make product available to be filtered later. See OC comment above. + create( + :simple_order_cycle, + suppliers: [supplier], + distributors: [distributor, other_distributor], + variants: [ + variant_filtered_by_order_cycle + ] + ) + + allow(subject).to receive(:params).and_return( + order_cycle_id: order_cycle.id, + supplier_id: supplier.id, + distributor_id: distributor.id, + report_subtype: 'inventory' + ) + + expect(subject.filter(variants)).to match_array [not_filtered_variant] + + # And it integrates with the ordering of the `variants` method. + expect(subject.variants).to match_array [not_filtered_variant] + end + end + + describe "fetching SKU for a variant" do + let(:variant) { create(:variant) } + let(:product) { variant.product } + + before { product.update_attribute(:sku, "Product SKU") } + + context "when the variant has an SKU set" do + before { variant.update_attribute(:sku, "Variant SKU") } + it "returns it" do + expect(subject.__send__(:sku_for, variant)).to eq "Variant SKU" + end + end + + context "when the variant has bo SKU set" do + before { variant.update_attribute(:sku, "") } + + it "returns the product's SKU" do + expect(subject.__send__(:sku_for, variant)).to eq "Product SKU" + end + end + end + end + end + end + end +end diff --git a/spec/lib/reports/sales_tax_report_spec.rb b/spec/lib/reports/sales_tax_report_spec.rb new file mode 100644 index 0000000000..f1695c2d4a --- /dev/null +++ b/spec/lib/reports/sales_tax_report_spec.rb @@ -0,0 +1,67 @@ +# frozen_string_literal: true + +require 'spec_helper' + +module Reporting + module Reports + module SalesTax + describe SalesTaxReport do + let(:user) { create(:user) } + let(:report) { SalesTaxReport.new(user, {}, true) } + + describe "calculating totals for line items" do + let(:li1) { double(:line_item, quantity: 1, amount: 12) } + let(:li2) { double(:line_item, quantity: 2, amount: 24) } + let(:totals) { report.__send__(:totals_of, [li1, li2]) } + + before do + allow(report).to receive(:tax_included_in).and_return(2, 4) + end + + it "calculates total quantity" do + expect(totals[:items]).to eq(3) + end + + it "calculates total price" do + expect(totals[:items_total]).to eq(36) + end + + context "when floating point math would result in fractional cents" do + let(:li1) { double(:line_item, quantity: 1, amount: 0.11) } + let(:li2) { double(:line_item, quantity: 2, amount: 0.12) } + + it "rounds to the nearest cent" do + expect(totals[:items_total]).to eq(0.23) + end + end + + it "calculates the taxable total price" do + expect(totals[:taxable_total]).to eq(36) + end + + it "calculates sales tax" do + expect(totals[:sales_tax]).to eq(6) + end + + context "when there is no tax on a line item" do + before do + allow(report).to receive(:tax_included_in) { 0 } + end + + it "does not appear in taxable total" do + expect(totals[:taxable_total]).to eq(0) + end + + it "still appears on items total" do + expect(totals[:items_total]).to eq(36) + end + + it "does not register sales tax" do + expect(totals[:sales_tax]).to eq(0) + end + end + end + end + end + end +end diff --git a/spec/lib/reports/users_and_enterprises_report_spec.rb b/spec/lib/reports/users_and_enterprises_report_spec.rb new file mode 100644 index 0000000000..b967ed3d5c --- /dev/null +++ b/spec/lib/reports/users_and_enterprises_report_spec.rb @@ -0,0 +1,128 @@ +# frozen_string_literal: true + +require 'spec_helper' + +module Reporting + module Reports + module UsersAndEnterprises + describe UsersAndEnterprisesReport do + describe "users_and_enterprises" do + let!(:owners_and_enterprises) { double(:owners_and_enterprises) } + let!(:managers_and_enterprises) { double(:managers_and_enterprises) } + let!(:subject) { UsersAndEnterprisesReport.new(nil, {}, true) } + + before do + allow(subject).to receive(:owners_and_enterprises) { owners_and_enterprises } + allow(subject).to receive(:managers_and_enterprises) { managers_and_enterprises } + end + + it "should concatenate owner and manager queries" do + expect(subject).to receive(:owners_and_enterprises).once + expect(subject).to receive(:managers_and_enterprises).once + expect(owners_and_enterprises).to receive(:concat).with(managers_and_enterprises).and_return [] + expect(subject).to receive(:sort).with [] + subject.users_and_enterprises + end + end + + describe "sorting results" do + let!(:subject) { UsersAndEnterprisesReport.new(nil, {}, true) } + + it "sorts by creation date" do + uae_mock = [ + { "created_at" => "2015-01-01", "name" => "bbb" }, + { "created_at" => "2015-01-02", "name" => "aaa" } + ] + expect(subject.sort(uae_mock)).to eq [uae_mock[1], uae_mock[0]] + end + + it "then sorts by name" do + uae_mock = [ + { "name" => "aaa", "relationship_type" => "bbb", "user_email" => "bbb" }, + { "name" => "bbb", "relationship_type" => "aaa", "user_email" => "aaa" } + ] + expect(subject.sort(uae_mock)).to eq [uae_mock[0], uae_mock[1]] + end + + it "then sorts by relationship type (reveresed)" do + uae_mock = [ + { "name" => "aaa", "relationship_type" => "bbb", "user_email" => "bbb" }, + { "name" => "aaa", "relationship_type" => "aaa", "user_email" => "aaa" }, + { "name" => "aaa", "relationship_type" => "bbb", "user_email" => "aaa" } + ] + expect(subject.sort(uae_mock)).to eq [uae_mock[2], uae_mock[0], uae_mock[1]] + end + + it "then sorts by user_email" do + uae_mock = [ + { "name" => "aaa", "relationship_type" => "bbb", "user_email" => "aaa" }, + { "name" => "aaa", "relationship_type" => "aaa", "user_email" => "aaa" }, + { "name" => "aaa", "relationship_type" => "aaa", "user_email" => "bbb" } + ] + expect(subject.sort(uae_mock)).to eq [uae_mock[0], uae_mock[1], uae_mock[2]] + end + end + + describe "filtering results" do + let!(:enterprise1) { create(:enterprise, owner: create(:user) ) } + let!(:enterprise2) { create(:enterprise, owner: create(:user) ) } + + describe "for owners and enterprises" do + describe "by enterprise id" do + let!(:params) { { enterprise_id_in: [enterprise1.id.to_s] } } + let!(:subject) { UsersAndEnterprisesReport.new nil, params, true } + + it "excludes enterprises that are not explicitly requested" do + results = subject.owners_and_enterprises.to_a.map{ |oae| oae["name"] } + expect(results).to include enterprise1.name + expect(results).to_not include enterprise2.name + end + end + + describe "by user id" do + let!(:params) { { user_id_in: [enterprise1.owner.id.to_s] } } + let!(:subject) { UsersAndEnterprisesReport.new nil, params, true } + + it "excludes enterprises that are not explicitly requested" do + results = subject.owners_and_enterprises.to_a.map{ |oae| oae["name"] } + expect(results).to include enterprise1.name + expect(results).to_not include enterprise2.name + end + end + end + + describe "for managers and enterprises" do + describe "by enterprise id" do + let!(:params) { { enterprise_id_in: [enterprise1.id.to_s] } } + let!(:subject) { UsersAndEnterprisesReport.new nil, params, true } + + it "excludes enterprises that are not explicitly requested" do + results = subject.managers_and_enterprises.to_a.map{ |mae| mae["name"] } + expect(results).to include enterprise1.name + expect(results).to_not include enterprise2.name + end + end + + describe "by user id" do + let!(:manager1) { create(:user) } + let!(:manager2) { create(:user) } + let!(:params) { { user_id_in: [manager1.id.to_s] } } + let!(:subject) { UsersAndEnterprisesReport.new nil, params, true } + + before do + enterprise1.enterprise_roles.build(user: manager1).save + enterprise2.enterprise_roles.build(user: manager2).save + end + + it "excludes enterprises whose managers are not explicitly requested" do + results = subject.managers_and_enterprises.to_a.map{ |mae| mae["name"] } + expect(results).to include enterprise1.name + expect(results).to_not include enterprise2.name + end + end + end + end + end + end + end +end diff --git a/spec/lib/reports/xero_invoices_report_spec.rb b/spec/lib/reports/xero_invoices_report_spec.rb new file mode 100644 index 0000000000..a5ad21e0cc --- /dev/null +++ b/spec/lib/reports/xero_invoices_report_spec.rb @@ -0,0 +1,99 @@ +# frozen_string_literal: true + +require 'spec_helper' + +module Reporting + module Reports + module XeroInvoices + describe XeroInvoicesReport do + subject { XeroInvoicesReport.new user, {}, true } + + let(:user) { create(:user) } + + describe "option defaults" do + let(:report) { + XeroInvoicesReport.new user, initial_invoice_number: '', invoice_date: '', due_date: '', + account_code: '' + } + + around { |example| Timecop.travel(Time.zone.local(2015, 5, 5, 14, 0, 0)) { example.run } } + + it "uses defaults when blank params are passed" do + expect(report.instance_variable_get(:@opts)).to eq( invoice_date: Date.civil(2015, 5, 5), + due_date: Date.civil(2015, 6, 5), + account_code: 'food sales', + report_subtype: 'summary' ) + end + end + + describe "summary rows" do + let(:report) { + XeroInvoicesReport.new user, initial_invoice_number: '', invoice_date: '', due_date: '', + account_code: '' + } + let(:order) { double(:order) } + let(:summary_rows) { report.__send__(:summary_rows_for_order, order, 1, {}) } + + before do + allow(report).to receive(:produce_summary_rows) { ['produce'] } + allow(report).to receive(:fee_summary_rows) { ['fee'] } + allow(report).to receive(:shipping_summary_rows) { ['shipping'] } + allow(report).to receive(:payment_summary_rows) { ['payment'] } + allow(report).to receive(:admin_adjustment_summary_rows) { ['admin'] } + end + + it "displays produce summary rows when summary report" do + allow(report).to receive(:detail?) { false } + expect(summary_rows).to include 'produce' + end + + it "does not display produce summary rows when detail report" do + allow(report).to receive(:detail?) { true } + expect(summary_rows).not_to include 'produce' + end + + it "displays fee summary rows when summary report" do + allow(report).to receive(:detail?) { false } + expect(summary_rows).to include 'fee' + end + + it "displays fee summary rows when detail report" do + allow(report).to receive(:detail?) { true } + expect(summary_rows).to include 'fee' + end + + it "always displays shipping summary rows" do + expect(summary_rows).to include 'shipping' + end + + it "displays admin adjustment summary rows when summary report" do + expect(summary_rows).to include 'admin' + end + + it "does not display admin adjustment summary rows when detail report" do + allow(report).to receive(:detail?) { true } + expect(summary_rows).not_to include 'admin' + end + end + + describe "generating invoice numbers" do + let(:order) { double(:order, number: 'R731032860') } + + describe "when no initial invoice number is given" do + it "returns the order number" do + expect(subject.send(:invoice_number_for, order, 123)).to eq('R731032860') + end + end + + describe "when an initial invoice number is given" do + subject { XeroInvoicesReport.new user, initial_invoice_number: '123' } + + it "increments the number by the index" do + expect(subject.send(:invoice_number_for, order, 456)).to eq(579) + end + end + end + end + end + end +end diff --git a/spec/validators/date_time_string_validator_spec.rb b/spec/validators/date_time_string_validator_spec.rb index 3ee3143eb8..5476b0f6a0 100644 --- a/spec/validators/date_time_string_validator_spec.rb +++ b/spec/validators/date_time_string_validator_spec.rb @@ -13,11 +13,11 @@ describe DateTimeStringValidator do describe "internationalization" do it "has translation for NOT_STRING_ERROR" do - expect(described_class::NOT_STRING_ERROR).not_to be_blank + expect(described_class.not_string_error).not_to be_blank end it "has translation for INVALID_FORMAT_ERROR" do - expect(described_class::INVALID_FORMAT_ERROR).not_to be_blank + expect(described_class.invalid_format_error).not_to be_blank end end @@ -37,13 +37,13 @@ describe DateTimeStringValidator do it "adds error NOT_STRING_ERROR when blank but neither nil nor a string" do instance.timestamp = [] expect(instance).not_to be_valid - expect(instance.errors[:timestamp]).to eq([described_class::NOT_STRING_ERROR]) + expect(instance.errors[:timestamp]).to eq([described_class.not_string_error]) end it "adds error NOT_STRING_ERROR when not a string" do instance.timestamp = 1 expect(instance).not_to be_valid - expect(instance.errors[:timestamp]).to eq([described_class::NOT_STRING_ERROR]) + expect(instance.errors[:timestamp]).to eq([described_class.not_string_error]) end it "does not add error when value can be parsed" do @@ -54,7 +54,7 @@ describe DateTimeStringValidator do it "adds error INVALID_FORMAT_ERROR when value cannot be parsed" do instance.timestamp = "Not Valid" expect(instance).not_to be_valid - expect(instance.errors[:timestamp]).to eq([described_class::INVALID_FORMAT_ERROR]) + expect(instance.errors[:timestamp]).to eq([described_class.invalid_format_error]) end end end diff --git a/spec/validators/integer_array_validator_spec.rb b/spec/validators/integer_array_validator_spec.rb index 57fb3d6b3b..0fbb08fd20 100644 --- a/spec/validators/integer_array_validator_spec.rb +++ b/spec/validators/integer_array_validator_spec.rb @@ -13,11 +13,11 @@ describe IntegerArrayValidator do describe "internationalization" do it "has translation for NOT_ARRAY_ERROR" do - expect(described_class::NOT_ARRAY_ERROR).not_to be_blank + expect(described_class.not_array_error).not_to be_blank end it "has translation for INVALID_ELEMENT_ERROR" do - expect(described_class::INVALID_ELEMENT_ERROR).not_to be_blank + expect(described_class.invalid_element_error).not_to be_blank end end @@ -37,7 +37,7 @@ describe IntegerArrayValidator do it "adds error NOT_ARRAY_ERROR when neither nil nor an array" do instance.ids = 1 expect(instance).not_to be_valid - expect(instance.errors[:ids]).to include(described_class::NOT_ARRAY_ERROR) + expect(instance.errors[:ids]).to include(described_class.not_array_error) end it "does not add error when array of integers" do @@ -53,7 +53,7 @@ describe IntegerArrayValidator do it "adds error INVALID_ELEMENT_ERROR when an element cannot be parsed as Integer" do instance.ids = [1, "2", "Not Integer", 3] expect(instance).not_to be_valid - expect(instance.errors[:ids]).to include(described_class::INVALID_ELEMENT_ERROR) + expect(instance.errors[:ids]).to include(described_class.invalid_element_error) end end end