diff --git a/.rubocop_todo.yml b/.rubocop_todo.yml index ac90405d33..3dba2bd123 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' - - 'engines/order_management/app/services/order_management/reports/bulk_coop/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,19 @@ 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/xero_invoices/base.rb' - 'lib/spree/localized_number.rb' - 'lib/tasks/data.rake' - 'lib/tasks/enterprises.rake' @@ -176,19 +179,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_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 +313,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 +343,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 +410,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 +431,21 @@ Metrics/AbcSize: - 'app/models/spree/order/checkout.rb' - 'app/models/spree/preferences/preferable_class_methods.rb' - 'app/models/spree/return_authorization.rb' - - 'engines/order_management/app/services/order_management/reports/bulk_coop/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/order_and_distributor_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 +469,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 +484,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,18 +528,17 @@ Metrics/ClassLength: - 'app/services/cart_service.rb' - 'app/services/order_syncer.rb' - 'engines/order_management/app/services/order_management/order/updater.rb' - - 'engines/order_management/app/services/order_management/reports/bulk_coop/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/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/xero_invoices/base.rb' -# Offense count: 40 +# Offense count: 39 # Configuration parameters: IgnoredMethods, Max. Metrics/CyclomaticComplexity: Exclude: @@ -555,20 +564,19 @@ Metrics/CyclomaticComplexity: - 'app/models/spree/tax_rate.rb' - 'app/models/spree/variant.rb' - 'app/models/spree/zone.rb' - - 'engines/order_management/app/services/order_management/reports/bulk_coop/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_fulfillments_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/base.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: @@ -578,23 +586,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' - - 'engines/order_management/app/services/order_management/reports/bulk_coop/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/base.rb' - 'lib/tasks/sample_data/product_factory.rb' -# Offense count: 51 +# Offense count: 54 # Configuration parameters: CountComments, Max, CountAsOne. Metrics/ModuleLength: Exclude: @@ -625,17 +633,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_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' @@ -656,11 +667,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/base.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: @@ -669,9 +680,8 @@ Metrics/PerceivedComplexity: - 'app/models/enterprise_relationship.rb' - 'app/models/spree/ability.rb' - 'app/models/spree/order/checkout.rb' - - 'engines/order_management/app/services/order_management/reports/bulk_coop/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: @@ -716,7 +726,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_fulfillments_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' @@ -895,7 +905,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' @@ -1092,9 +1102,9 @@ Style/MissingRespondToMissing: # Offense count: 1 Style/MixinUsage: Exclude: - - 'lib/open_food_network/orders_and_fulfillments_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 @@ -1118,7 +1128,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: @@ -1132,16 +1142,7 @@ Style/OptionalBooleanParameter: - 'app/models/spree/order_contents.rb' - 'app/models/spree/preferences/file_configuration.rb' - 'app/models/spree/shipment.rb' - - 'engines/order_management/app/services/order_management/reports/bulk_coop/bulk_coop_report.rb' - 'engines/order_management/app/services/order_management/stock/estimator.rb' - - 'lib/open_food_network/customers_report.rb' - - 'lib/open_food_network/order_and_distributor_report.rb' - - 'lib/open_food_network/order_cycle_management_report.rb' - - 'lib/open_food_network/orders_and_fulfillments_report.rb' - - 'lib/open_food_network/payments_report.rb' - - 'lib/open_food_network/products_and_inventory_report_base.rb' - - 'lib/open_food_network/users_and_enterprises_report.rb' - - 'lib/open_food_network/xero_invoices_report.rb' - 'lib/spree/core/controller_helpers/order.rb' - 'lib/spree/core/delegate_belongs_to.rb' - 'spec/support/request/web_helper.rb' @@ -1161,11 +1162,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' @@ -1177,13 +1177,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' @@ -1208,7 +1205,7 @@ Style/SingleArgumentDig: Exclude: - 'app/services/checkout/form_data_adapter.rb' -# Offense count: 5 +# Offense count: 4 # Cop supports --auto-correct. Style/SlicingWithRange: Exclude: @@ -1216,9 +1213,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: @@ -1235,11 +1231,8 @@ Style/StringConcatenation: - 'app/serializers/api/enterprise_shopfront_list_serializer.rb' - 'app/services/embedded_page_service.rb' - 'app/services/products_renderer.rb' - - 'engines/order_management/app/services/order_management/reports/bulk_coop/bulk_coop_report.rb' - - 'lib/open_food_network/orders_and_fulfillments_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/admin/reports_controller.rb b/app/controllers/admin/reports_controller.rb index 79e5176792..f7505208ec 100644 --- a/app/controllers/admin/reports_controller.rb +++ b/app/controllers/admin/reports_controller.rb @@ -5,14 +5,23 @@ module Admin include ReportsActions helper ReportsHelper - before_action :authorize_report + before_action :authorize_report, only: [:show] + + # Define model class for Can? permissions + def model_class + Admin::ReportsController + end + + def index + @reports = reports.select do |report_type, _description| + can? report_type, :report + end + end def show - render_report && return if ransack_params.blank? + @report = report_class.new(spree_current_user, params, request) - @report = report_class.new(spree_current_user, ransack_params, report_options) - - if export_spreadsheet? + if report_format.present? export_report else render_report @@ -22,33 +31,23 @@ module Admin private def export_report - render report_format.to_sym => @report.public_send("to_#{report_format}"), - :filename => report_filename + send_data @report.render_as(report_format, controller: self), filename: report_filename end def render_report assign_view_data - load_form_options - - render report_type + render "show" end def assign_view_data @report_type = report_type - @report_subtype = report_subtype || report_loader.default_report_subtype - @report_subtypes = report_class.report_subtypes.map do |subtype| - [t("packing.#{subtype}_report", scope: i18n_scope), subtype] - end - end + @report_subtypes = report_subtypes + @report_subtype = report_subtype - def load_form_options - return unless form_options_required? + # Initialize data + params[:display_summary_row] = true if request.get? - form_options = Reporting::FrontendData.new(spree_current_user) - - @distributors = form_options.distributors.to_a - @suppliers = form_options.suppliers.to_a - @order_cycles = form_options.order_cycles.to_a + @data = Reporting::FrontendData.new(spree_current_user) end end end diff --git a/app/controllers/api/v0/reports_controller.rb b/app/controllers/api/v0/reports_controller.rb index b6cc0facc9..48c5077f8b 100644 --- a/app/controllers/api/v0/reports_controller.rb +++ b/app/controllers/api/v0/reports_controller.rb @@ -10,7 +10,8 @@ module Api before_action :validate_report, :authorize_report, :validate_query def show - @report = report_class.new(current_api_user, ransack_params, report_options) + params[:report_format] = 'json' + @report = report_class.new(current_api_user, params) render_report end diff --git a/app/controllers/concerns/reports_actions.rb b/app/controllers/concerns/reports_actions.rb index 7ff6e0c461..35a508b154 100644 --- a/app/controllers/concerns/reports_actions.rb +++ b/app/controllers/concerns/reports_actions.rb @@ -3,10 +3,14 @@ module ReportsActions extend ActiveSupport::Concern + def reports + Reporting::Reports::List.all + end + private def authorize_report - authorize! report_type&.to_sym, :report + authorize! report_type.to_sym, :report end def report_class @@ -23,31 +27,26 @@ module ReportsActions params[:report_type] end + def report_subtypes + reports[report_type.to_sym] || [] + end + + def report_subtypes_codes + report_subtypes.map(&:second).map(&:to_s) + end + def report_subtype - params[:report_subtype] + params[:report_subtype] || report_subtypes_codes.first end def ransack_params raw_params[:q] end - def report_options - raw_params[:options] - end - def report_format params[:report_format] end - def export_spreadsheet? - ['xlsx', 'ods', 'csv'].include?(report_format) - end - - def form_options_required? - [:packing, :customers, :products_and_inventory, :order_cycle_management]. - include? report_type.to_sym - end - def report_filename "#{report_type || action_name}_#{file_timestamp}.#{report_format}" end diff --git a/app/controllers/spree/admin/reports_controller.rb b/app/controllers/spree/admin/reports_controller.rb deleted file mode 100644 index de57236dbf..0000000000 --- a/app/controllers/spree/admin/reports_controller.rb +++ /dev/null @@ -1,310 +0,0 @@ -# frozen_string_literal: true - -require 'csv' - -require 'open_food_network/reports/list' -require 'open_food_network/order_and_distributor_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_fulfillments_report' - -module Spree - module Admin - class ReportsController < Spree::Admin::BaseController - include Spree::ReportsHelper - helper ::ReportsHelper - - ORDER_MANAGEMENT_ENGINE_REPORTS = [ - :bulk_coop, - :enterprise_fee_summary - ].freeze - - helper_method :render_content? - - before_action :cache_search_state - # Fetches user's distributors, suppliers and order_cycles - before_action :load_basic_data, only: [:customers, :products_and_inventory, :order_cycle_management] - before_action :load_associated_data, only: [:orders_and_fulfillment] - - respond_to :html - - def report_types - OpenFoodNetwork::Reports::List.all - end - - def index - @reports = authorized_reports - respond_with(@reports) - end - - def customers - @report_types = report_types[:customers] - @report_type = params[:report_type] - @report = OpenFoodNetwork::CustomersReport.new spree_current_user, raw_params, - render_content? - render_report(@report.header, @report.table, params[:csv], "customers_#{timestamp}.csv") - end - - def order_cycle_management - raw_params[:q] ||= {} - - @report_types = report_types[:order_cycle_management] - @report_type = params[:report_type] - - # -- Build Report with Order Grouper - @report = OpenFoodNetwork::OrderCycleManagementReport.new spree_current_user, - raw_params, - render_content? - @table = @report.table_items - - render_report(@report.header, @table, params[:csv], - "order_cycle_management_#{timestamp}.csv") - end - - def orders_and_distributors - @report = OpenFoodNetwork::OrderAndDistributorReport.new spree_current_user, - raw_params, - render_content? - @search = @report.search - csv_file_name = "orders_and_distributors_#{timestamp}.csv" - render_report(@report.header, @report.table, params[:csv], csv_file_name) - end - - def sales_tax - @distributors = my_distributors - @report_type = params[:report_type] - @report = OpenFoodNetwork::SalesTaxReport.new spree_current_user, raw_params, - render_content? - render_report(@report.header, @report.table, params[:csv], "sales_tax.csv") - end - - def payments - # -- Prepare Form Options - @distributors = my_distributors - @report_type = params[:report_type] - - # -- Build Report with Order Grouper - @report = OpenFoodNetwork::PaymentsReport.new spree_current_user, raw_params, - render_content? - @table = order_grouper_table - csv_file_name = "payments_#{timestamp}.csv" - - render_report(@report.header, @table, params[:csv], csv_file_name) - end - - def orders_and_fulfillment - raw_params[:q] ||= orders_and_fulfillment_default_filters - - @report_types = report_types[:orders_and_fulfillment] - @report_type = params[:report_type] - - @include_blank = I18n.t(:all) - - # -- Build Report with Order Grouper - @report = OpenFoodNetwork::OrdersAndFulfillmentsReport.new spree_current_user, - raw_params, - render_content? - @table = order_grouper_table - csv_file_name = "#{params[:report_type]}_#{timestamp}.csv" - - render_report(@report.header, @table, params[:csv], csv_file_name) - end - - def products_and_inventory - @report_types = report_types[:products_and_inventory] - @report = if params[:report_type] != 'lettuce_share' - OpenFoodNetwork::ProductsAndInventoryReport.new spree_current_user, - raw_params, - render_content? - else - OpenFoodNetwork::LettuceShareReport.new spree_current_user, - raw_params, - render_content? - end - - render_report @report.header, - @report.table, - params[:csv], - "products_and_inventory_#{timestamp}.csv" - end - - def users_and_enterprises - @report = OpenFoodNetwork::UsersAndEnterprisesReport.new raw_params, render_content? - render_report(@report.header, @report.table, params[:csv], - "users_and_enterprises_#{timestamp}.csv") - end - - def xero_invoices - raw_params[:q] ||= {} - - @distributors = my_distributors - @order_cycles = my_order_cycles - - @report = OpenFoodNetwork::XeroInvoicesReport.new(spree_current_user, - raw_params, - render_content?) - render_report(@report.header, @report.table, params[:csv], "xero_invoices_#{timestamp}.csv") - end - - private - - def model_class - Spree::Admin::ReportsController - end - - # Some actions are changing the `params` object. That is unfortunate Spree - # behavior and we are building on it. So we have to look at `params` early - # to check if we are searching or just displaying a report search form. - def cache_search_state - search_keys = [ - # search parameter for ransack - :q, - # common in all reports, only set for CSV rendering - :csv, - # `button` is included in all forms. It's not important for searching, - # but the Users & Enterprises report doesn't have any other parameter - # for an empty search. So we use this one to display data. - :button, - # Some reports use filtering by enterprise or order cycle - :distributor_id, - :supplier_id, - :order_cycle_id, - # Xero Invoices can be filtered by date - :invoice_date, - :due_date - ] - @searching = search_keys.any? { |key| raw_params.key? key } - end - - # We don't want to render data unless search params are supplied. - # Compiling data can take a long time. - def render_content? - @searching - end - - def render_report(header, table, create_csv, csv_file_name) - send_data csv_report(header, table), filename: csv_file_name if create_csv - @header = header - @table = table - # Rendering HTML is the default. - end - - def load_associated_data - form_options = Reporting::FrontendData.new(spree_current_user) - - @distributors = form_options.distributors - @suppliers = form_options.suppliers - @order_cycles = form_options.order_cycles - end - - def csv_report(header, table) - CSV.generate do |csv| - csv << header - table.each { |row| csv << row } - end - end - - def load_basic_data - @distributors = my_distributors - @suppliers = my_suppliers | suppliers_of_products_distributed_by(@distributors) - @order_cycles = my_order_cycles - end - - # Load managed distributor enterprises of current user - def my_distributors - Enterprise.is_distributor.managed_by(spree_current_user) - end - - # Load managed producer enterprises of current user - def my_suppliers - Enterprise.is_primary_producer.managed_by(spree_current_user) - end - - def suppliers_of_products_distributed_by(distributors) - supplier_ids = Spree::Product.in_distributors(distributors.select('enterprises.id')). - select('spree_products.supplier_id') - - Enterprise.where(id: supplier_ids) - end - - # Load order cycles the current user has access to - def my_order_cycles - OrderCycle. - active_or_complete. - visible_by(spree_current_user). - order('orders_close_at DESC') - end - - def order_grouper_table - order_grouper = OpenFoodNetwork::OrderGrouper.new @report.rules, @report.columns, @report - order_grouper.table(@report.table_items) - end - - def authorized_reports - all_reports = [ - :orders_and_distributors, - :bulk_coop, - :payments, - :orders_and_fulfillment, - :customers, - :products_and_inventory, - :users_and_enterprises, - :enterprise_fee_summary, - :order_cycle_management, - :sales_tax, - :xero_invoices, - :packing - ] - reports = all_reports.select { |action| can? action, Spree::Admin::ReportsController } - reports.map { |report| [report, describe_report(report)] }.to_h - end - - def describe_report(report) - name = I18n.t(:name, scope: [:admin, :reports, report]) - description = begin - I18n.t!(:description, scope: [:admin, :reports, report]) - rescue I18n::MissingTranslationData - render_to_string( - partial: "#{report}_description", - layout: false, - locals: { report_types: report_types[report] } - ).html_safe - end - { name: name, url: url_for_report(report), description: description } - end - - def url_for_report(report) - if report_in_order_management_engine?(report) - main_app.public_send("new_order_management_reports_#{report}_url".to_sym) - else - spree.public_send("#{report}_admin_reports_url".to_sym) - end - rescue NoMethodError - main_app.admin_reports_url(report_type: report) - end - - # List of reports that have been moved to the Order Management engine - def report_in_order_management_engine?(report) - ORDER_MANAGEMENT_ENGINE_REPORTS.include?(report) - end - - def timestamp - Time.zone.now.strftime("%Y%m%d") - end - - def orders_and_fulfillment_default_filters - now = Time.zone.now - { completed_at_gt: (now - 1.month).beginning_of_day, - completed_at_lt: (now + 1.day).beginning_of_day } - end - end - end -end diff --git a/app/helpers/reports_helper.rb b/app/helpers/reports_helper.rb index bfcc4232db..953a4bbffa 100644 --- a/app/helpers/reports_helper.rb +++ b/app/helpers/reports_helper.rb @@ -9,7 +9,24 @@ module ReportsHelper end end - def report_subtypes(report) - Reporting::ReportLoader.new(report).report_subtypes + def report_payment_method_options(orders) + orders.map do |order| + payment_method = order.payments.last&.payment_method + + next unless payment_method + + [payment_method.name, payment_method.id] + end.compact.uniq + end + + def report_shipping_method_options(orders) + orders.map do |o| + sm = o.shipping_method + [sm&.name, sm&.id] + end.uniq + end + + def currency_symbol + Spree::Money.currency_symbol end end diff --git a/app/helpers/spree/admin/navigation_helper.rb b/app/helpers/spree/admin/navigation_helper.rb index 324e056330..56fc759660 100644 --- a/app/helpers/spree/admin/navigation_helper.rb +++ b/app/helpers/spree/admin/navigation_helper.rb @@ -77,7 +77,7 @@ module Spree klass = EnterpriseGroup if klass == :group klass = VariantOverride if klass == :Inventory klass = ProductImport::ProductImporter if klass == :import - klass = Spree::Admin::ReportsController if klass == :report + klass = ::Admin::ReportsController if klass == :report klass end diff --git a/app/helpers/spree/reports_helper.rb b/app/helpers/spree/reports_helper.rb deleted file mode 100644 index 80bafa4c5a..0000000000 --- a/app/helpers/spree/reports_helper.rb +++ /dev/null @@ -1,33 +0,0 @@ -# frozen_string_literal: true - -require 'spree/money' - -module Spree - module ReportsHelper - def report_payment_method_options(orders) - orders.map do |order| - payment_method = order.payments.last&.payment_method - - next unless payment_method - - [payment_method.name, payment_method.id] - end.compact.uniq - end - - def report_shipping_method_options(orders) - orders.map do |o| - sm = o.shipping_method - [sm&.name, sm&.id] - end.uniq - end - - def xero_report_types - [[I18n.t(:summary), 'summary'], - [I18n.t(:detailed), 'detailed']] - end - - def currency_symbol - Spree::Money.currency_symbol - end - end -end diff --git a/app/models/spree/ability.rb b/app/models/spree/ability.rb index fcb1ffc37a..2e9aec849f 100644 --- a/app/models/spree/ability.rb +++ b/app/models/spree/ability.rb @@ -236,12 +236,10 @@ module Spree :validate_data, :reset_absent_products], ProductImport::ProductImporter # Reports page - can [:admin, :index, :customers, :orders_and_distributors, :group_buys, :payments, - :orders_and_fulfillment, :products_and_inventory, :order_cycle_management, :packing], - Spree::Admin::ReportsController - can [:admin, :show, :packing], :report - add_bulk_coop_abilities - add_enterprise_fee_summary_abilities + can [:admin, :index, :show], ::Admin::ReportsController + can [:admin, :show, :customers, :orders_and_distributors, :group_buys, :payments, + :orders_and_fulfillment, :products_and_inventory, :order_cycle_management, + :packing, :enterprise_fee_summary, :bulk_coop], :report end def add_order_cycle_management_abilities(user) @@ -317,11 +315,10 @@ module Spree end # Reports page - can [:admin, :index, :customers, :group_buys, :sales_tax, :payments, + can [:admin, :index, :show], ::Admin::ReportsController + can [:admin, :customers, :group_buys, :sales_tax, :payments, :orders_and_distributors, :orders_and_fulfillment, :products_and_inventory, - :order_cycle_management, :xero_invoices], Spree::Admin::ReportsController - add_bulk_coop_abilities - add_enterprise_fee_summary_abilities + :order_cycle_management, :xero_invoices, :enterprise_fee_summary, :bulk_coop], :report can [:create], Customer can [:admin, :index, :update, @@ -346,19 +343,5 @@ module Spree user.enterprises.include?(enterprise_relationship.child) end end - - def add_bulk_coop_abilities - # Reveal the report link in spree/admin/reports#index - can [:bulk_coop], Spree::Admin::ReportsController - # Allow direct access to the report resource - can [:admin, :new, :create], :bulk_coop - end - - def add_enterprise_fee_summary_abilities - # Reveal the report link in spree/admin/reports#index - can [:enterprise_fee_summary], Spree::Admin::ReportsController - # Allow direct access to the report resource - can [:admin, :new, :create], :enterprise_fee_summary - end end end diff --git a/app/models/spree/address.rb b/app/models/spree/address.rb index 3538336d7d..e4dd2cf5ed 100644 --- a/app/models/spree/address.rb +++ b/app/models/spree/address.rb @@ -105,6 +105,10 @@ module Spree render_address([city, zipcode, state&.name]) end + def address_and_city + [address1, address2, city].select(&:present?).join(' ') + end + private def require_zipcode? 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/app/views/admin/reports/_date_range_form.html.haml b/app/views/admin/reports/_date_range_form.html.haml index fa585fe14f..34bd30ec84 100644 --- a/app/views/admin/reports/_date_range_form.html.haml +++ b/app/views/admin/reports/_date_range_form.html.haml @@ -1,9 +1,10 @@ +-# Field used for ransack search. This date range is mostly used for Spree::Order +-# so default field is 'completed_at' +- field ||= 'completed_at' .row.date-range-filter - = label_tag nil, t(:date_range) - %br - = label_tag nil, t(:start), :class => 'inline' - = text_field_tag "q[order_completed_at_gt]", params.dig(:q, :order_completed_at_gt), :class => 'datetimepicker datepicker-from' - %span.range-divider - %i.icon-arrow-right - = text_field_tag "q[order_completed_at_lt]", params.dig(:q, :order_completed_at_lt), :class => 'datetimepicker datepicker-to' - = label_tag nil, t(:end), :class => 'inline' + .alpha.two.columns= label_tag nil, t(:date_range) + .omega.fourteen.columns + = f.text_field "#{field}_gt", :class => 'datetimepicker datepicker-from', :placeholder => t(:start) + %span.range-divider + %i.icon-arrow-right + = f.text_field "#{field}_lt", :class => 'datetimepicker datepicker-to', :placeholder => t(:stop) diff --git a/app/views/admin/reports/_rendering_options.html.haml b/app/views/admin/reports/_rendering_options.html.haml index 0967df8999..e51b1d5862 100644 --- a/app/views/admin/reports/_rendering_options.html.haml +++ b/app/views/admin/reports/_rendering_options.html.haml @@ -1,8 +1,36 @@ -.row.rendering-options - = label_tag :report_format, t(".generate_report") - %br - = select_tag :report_format, options_for_select({t('.on_screen') => '', t('.csv_spreadsheet') => 'csv', t('.excel_spreadsheet') => 'xlsx', t('.openoffice_spreadsheet') => 'ods'}) +- if @report_subtypes.present? && @report_subtypes.count > 1 + .row + .alpha.two.columns= label_tag nil, t(:report_type) + .omega.fourteen.columns + = select_tag(:report_subtype, options_for_select(@report_subtypes, @report_subtype)) + +- if @report.header_option? || @report.summary_row_option? + .row + .alpha.two.columns= label_tag nil, t(".display") + .omega.fourteen.columns + - if @report.header_option? + %span.inline-checkbox{ style: "margin-right: 1rem;" } + = check_box_tag :display_header_row, true, params[:display_header_row] + = label_tag :display_header_row, t(".header_row") + - if @report.summary_row_option? + %span.inline-checkbox + = check_box_tag :display_summary_row, true, params[:display_summary_row] + = label_tag :display_summary_row, t(".summary_row") + +- if @report.available_headers.present? + .row + .alpha.two.columns= label_tag nil, t(:report_hide_columns) + .omega.fourteen.columns + = select_tag(:fields_to_hide, options_for_select(@report.available_headers, params[:fields_to_hide]), + class: "select2 fullwidth", multiple: true) + +.row.rendering-options + .alpha.two.columns + = label_tag :report_format, t(".generate_report") + .omega.fourteen.columns + = select_tag :report_format, grouped_options_for_select({ | + t('.formatted_data') => { t('.on_screen') => '', "PDF" => 'pdf', t('.spreadsheet') => 'xlsx' }, | + t('.raw_data') => { "CSV" => 'csv', "JSON" => 'json'}, | + }) + - -#.inline-checkbox - -# = check_box_tag "options[exclude_summaries]", true, params[:options].andand[:exclude_summaries] - -# = label_tag t(".hide_summary_rows") diff --git a/app/views/admin/reports/_row_group.haml b/app/views/admin/reports/_row_group.haml new file mode 100644 index 0000000000..6a5bea828b --- /dev/null +++ b/app/views/admin/reports/_row_group.haml @@ -0,0 +1,22 @@ +-# Locals : +-# - data +-# - report + +- data.each do |group_or_row| + - if group_or_row[:is_group].present? + / Header Row + - if group_or_row[:header].present? && report.display_header_row? + %tr + %td.header-row{ colspan: report.table_headers.count, class: group_or_row[:header_class] } + = group_or_row[:header].html_safe + / Rows + = render partial: 'admin/reports/row_group', locals: { report: report, data: group_or_row[:data] } + / Summary Row + - if group_or_row[:summary_row].present? && report.display_summary_row? + %tr.summary-row{ class: group_or_row[:summary_row_class] } + - group_or_row[:summary_row].to_h.each do |key, value| + %td= value + - else + %tr + - group_or_row.row.to_h.each do |key, value| + %td= value \ No newline at end of file diff --git a/app/views/admin/reports/_table.html.haml b/app/views/admin/reports/_table.html.haml index edbc2f1a8f..091d70e632 100644 --- a/app/views/admin/reports/_table.html.haml +++ b/app/views/admin/reports/_table.html.haml @@ -1,20 +1,15 @@ -- if params[:q].present? - %table.report__table{id: id} +- report ||= @report + +.report__table-container + %table.report__table %thead %tr - - @report.table_headers.each do |heading| + - report.table_headers.each do |heading| %th - = t("admin.reports.table.headings.#{heading}") + = heading %tbody - - @report.table_rows.each do |row| - - if row - %tr - - row.each do |cell| - %td - = cell - - if @report.table_rows.empty? + - if report.grouped_data.present? + = render partial: 'admin/reports/row_group', locals: { report: report, data: report.grouped_data } + - else %tr - %td{colspan: @report.table_headers.count}= t(:none) -- else - %p.report__message - = t(".select_and_search", option: msg_option.upcase) + %td{colspan: report.table_headers.count}= t(:none) diff --git a/app/views/admin/reports/filters/_bulk_coop.html.haml b/app/views/admin/reports/filters/_bulk_coop.html.haml new file mode 100644 index 0000000000..264facbf9f --- /dev/null +++ b/app/views/admin/reports/filters/_bulk_coop.html.haml @@ -0,0 +1,6 @@ += render 'admin/reports/date_range_form', f: f + +.row + .alpha.two.columns= label_tag nil, t(:report_hubs) + .omega.fourteen.columns + = f.collection_select(:distributor_id_in, @data.distributors, :id, :name, {}, {class: "select2 fullwidth", multiple: true}) diff --git a/app/views/admin/reports/filters/_customers.html.haml b/app/views/admin/reports/filters/_customers.html.haml new file mode 100644 index 0000000000..caa88f4e5d --- /dev/null +++ b/app/views/admin/reports/filters/_customers.html.haml @@ -0,0 +1,18 @@ +.row + .alpha.two.columns= label_tag nil, t(:report_customers_distributor) + .omega.fourteen.columns + = select_tag(:distributor_id, + options_from_collection_for_select(@data.distributors, :id, :name, params[:distributor_id]), + {:include_blank => true, :class => "select2 fullwidth light"}) +.row + .alpha.two.columns= label_tag nil, t(:report_customers_supplier) + .omega.fourteen.columns + = select_tag(:supplier_id, + options_from_collection_for_select(@data.suppliers, :id, :name, params[:supplier_id]), + {:include_blank => true, :class => "select2 fullwidth light"}) +.row + .alpha.two.columns= label_tag nil, t(:report_customers_cycle) + .omega.fourteen.columns + = select_tag(:order_cycle_id, + options_for_select(report_order_cycle_options(@data.order_cycles), params[:order_cycle_id]), + {:include_blank => true, :class => "select2 fullwidth light"}) \ No newline at end of file diff --git a/app/views/admin/reports/filters/_enterprise_fee_summary.html.haml b/app/views/admin/reports/filters/_enterprise_fee_summary.html.haml new file mode 100644 index 0000000000..79998980cd --- /dev/null +++ b/app/views/admin/reports/filters/_enterprise_fee_summary.html.haml @@ -0,0 +1,31 @@ += render 'admin/reports/date_range_form', f: f + +.row + .alpha.two.columns= label_tag nil, t(:report_hubs) + .omega.fourteen.columns + = collection_select(:q, :distributor_ids, @report.permissions.allowed_distributors, :id, :name, {selected: params.dig(:q, :distributor_ids)}, {class: "select2 fullwidth", multiple: true}) + +.row + .alpha.two.columns= label_tag nil, t(:report_producers) + .omega.fourteen.columns + = collection_select(:q, :producer_ids, @report.permissions.allowed_producers, :id, :name, {selected: params.dig(:q, :producer_ids)}, {class: "select2 fullwidth", multiple: true}) + +.row + .alpha.two.columns= label_tag nil, t(:order_cycles) + .omega.fourteen.columns + = collection_select(:q, :order_cycle_ids, @report.permissions.allowed_order_cycles, :id, :name, {selected: params.dig(:q, :order_cycle_ids)}, {class: "select2 fullwidth", multiple: true}) + +.row + .alpha.two.columns= label_tag nil, t(:report_enterprise_fee) + .omega.fourteen.columns + = collection_select(:q, :enterprise_fee_ids, @report.permissions.allowed_enterprise_fees, :id, :name, {selected: params.dig(:q, :enterprise_fee_ids)}, {class: "select2 fullwidth", multiple: true}) + +.row + .alpha.two.columns= label_tag nil, t('spree.shipping_methods') + .omega.fourteen.columns + = collection_select(:q, :shipping_method_ids, @report.permissions.allowed_shipping_methods, :id, :name, {selected: params.dig(:q, :shipping_method_ids)}, {class: "select2 fullwidth", multiple: true}) + +.row + .alpha.two.columns= label_tag nil, t(:report_payment) + .omega.fourteen.columns + = collection_select(:q, :payment_method_ids, @report.permissions.allowed_payment_methods, :id, :name, {selected: params.dig(:q, :payment_method_ids)}, {class: "select2 fullwidth", multiple: true}) diff --git a/app/views/admin/reports/filters/_order_cycle_management.html.haml b/app/views/admin/reports/filters/_order_cycle_management.html.haml new file mode 100644 index 0000000000..3699b5cc63 --- /dev/null +++ b/app/views/admin/reports/filters/_order_cycle_management.html.haml @@ -0,0 +1,18 @@ += render 'admin/reports/date_range_form', f: f + +.row + .alpha.two.columns= label_tag nil, t(:report_hubs) + .omega.fourteen.columns= f.collection_select(:distributor_id_in, @data.distributors, :id, :name, {}, {class: "select2 fullwidth", multiple: true}) + +.row + .alpha.two.columns= label_tag nil, t(:report_customers_cycle) + .omega.fourteen.columns + = f.select(:order_cycle_id_in, report_order_cycle_options(@data.order_cycles), {selected: params.dig(:q, :order_cycle_id_in)}, {class: "select2 fullwidth", multiple: true}) + +.row + .alpha.two.columns= label_tag nil, t(:report_payment) + .omega.fourteen.columns= select_tag(:payment_method_in, options_for_select(report_payment_method_options(@report.query_result), params[:payment_method_in]), {class: "select2 fullwidth", multiple: true}) + +.row + .alpha.two.columns= label_tag nil, t(:shipping_methods) + .omega.fourteen.columns= select_tag(:shipping_method_in, options_for_select(report_shipping_method_options(@report.query_result), params[:shipping_method_in]), {class: "select2 fullwidth", multiple: true}) diff --git a/app/views/admin/reports/filters/_orders_and_distributors.html.haml b/app/views/admin/reports/filters/_orders_and_distributors.html.haml new file mode 100644 index 0000000000..35c5a74266 --- /dev/null +++ b/app/views/admin/reports/filters/_orders_and_distributors.html.haml @@ -0,0 +1 @@ += render 'admin/reports/date_range_form', f: f diff --git a/app/views/admin/reports/filters/_orders_and_fulfillment.html.haml b/app/views/admin/reports/filters/_orders_and_fulfillment.html.haml new file mode 100755 index 0000000000..0a9963544f --- /dev/null +++ b/app/views/admin/reports/filters/_orders_and_fulfillment.html.haml @@ -0,0 +1,14 @@ += render 'admin/reports/date_range_form', f: f + +.row + .alpha.two.columns= label_tag nil, t(:report_hubs) + .omega.fourteen.columns= f.collection_select(:distributor_id_in, @data.orders_distributors, :id, :name, {}, {class: "select2 fullwidth", multiple: true}) + +.row + .alpha.two.columns= label_tag nil, t(:report_producers) + .omega.fourteen.columns= select_tag(:supplier_id_in, options_from_collection_for_select(@data.orders_suppliers, :id, :name, params[:supplier_id_in]), {class: "select2 fullwidth", multiple: true}) + +.row + .alpha.two.columns= label_tag nil, t(:report_customers_cycle) + .omega.fourteen.columns + = f.select(:order_cycle_id_in, report_order_cycle_options(@data.order_cycles), {selected: params.dig(:q, :order_cycle_id_in)}, {class: "select2 fullwidth", multiple: true}) diff --git a/app/views/admin/reports/filters/_packing.html.haml b/app/views/admin/reports/filters/_packing.html.haml new file mode 100755 index 0000000000..b15d3a5085 --- /dev/null +++ b/app/views/admin/reports/filters/_packing.html.haml @@ -0,0 +1,16 @@ += render partial: 'admin/reports/date_range_form', locals: { f: f, field: 'order_completed_at' } + +.row + .alpha.two.columns= label_tag nil, t(:report_hubs) + .omega.fourteen.columns + = collection_select("q", "order_distributor_id_in", @data.orders_distributors, :id, :name, {selected: params.dig(:q, :order_distributor_id_in)}, {class: "select2 fullwidth", multiple: true}) + +.row + .alpha.two.columns= label_tag nil, t(:report_producers) + .omega.fourteen.columns + = select_tag("q[supplier_id_in]", options_from_collection_for_select(@data.orders_suppliers, :id, :name, params.dig(:q, :supplier_id_in)), {class: "select2 fullwidth", multiple: true}) + +.row + .alpha.two.columns= label_tag nil, t(:report_customers_cycle) + .omega.fourteen.columns + = select_tag("q[order_cycle_id_in]", options_for_select(report_order_cycle_options(@data.order_cycles), params.dig(:q, :order_cycle_id_in)), {class: "select2 fullwidth", multiple: true}) \ No newline at end of file diff --git a/app/views/admin/reports/filters/_payments.html.haml b/app/views/admin/reports/filters/_payments.html.haml new file mode 100644 index 0000000000..91ec0c9e71 --- /dev/null +++ b/app/views/admin/reports/filters/_payments.html.haml @@ -0,0 +1,6 @@ += render 'admin/reports/date_range_form', f: f + +.row + .alpha.two.columns= label_tag nil, t(:report_distributor) + .omega.fourteen.columns + = f.collection_select(:distributor_id_eq, @data.distributors, :id, :name, {:include_blank => t(:report_all)}, {:class => "select2 fullwidth light"}) diff --git a/app/views/admin/reports/filters/_products_and_inventory.html.haml b/app/views/admin/reports/filters/_products_and_inventory.html.haml new file mode 100644 index 0000000000..921e55be0f --- /dev/null +++ b/app/views/admin/reports/filters/_products_and_inventory.html.haml @@ -0,0 +1,20 @@ +.row + .alpha.two.columns= label_tag nil, t(:report_distributor) + .omega.fourteen.columns + = select_tag(:distributor_id, + options_from_collection_for_select(@data.distributors, :id, :name, params[:distributor_id]), + {:include_blank => true, :class => "select2 fullwidth light"}) + +.row + .alpha.two.columns= label_tag nil, t(:report_customers_supplier) + .omega.fourteen.columns + = select_tag(:supplier_id, + options_from_collection_for_select(@data.suppliers, :id, :name, params[:supplier_id]), + {:include_blank => true, :class => "select2 fullwidth light"}) + +.row + .alpha.two.columns= label_tag nil, t(:report_order_cycle) + .omega.fourteen.columns + = select_tag(:order_cycle_id, + options_for_select(report_order_cycle_options(@data.order_cycles), params[:order_cycle_id]), + {:include_blank => true, :class => "select2 fullwidth light"}) diff --git a/app/views/admin/reports/filters/_sales_tax.html.haml b/app/views/admin/reports/filters/_sales_tax.html.haml new file mode 100644 index 0000000000..bdedb904c2 --- /dev/null +++ b/app/views/admin/reports/filters/_sales_tax.html.haml @@ -0,0 +1,6 @@ += render 'admin/reports/date_range_form', f: f + +.row + .alpha.two.columns= label_tag nil, t(:report_distributor) + .omega.fourteen.columns + = f.collection_select(:distributor_id_eq, @data.distributors, :id, :name, {:include_blank => t(:all)}, {:class => "select2 fullwidth light"}) diff --git a/app/views/admin/reports/filters/_users_and_enterprises.html.haml b/app/views/admin/reports/filters/_users_and_enterprises.html.haml new file mode 100644 index 0000000000..f8e14cf75d --- /dev/null +++ b/app/views/admin/reports/filters/_users_and_enterprises.html.haml @@ -0,0 +1,7 @@ +.row + .alpha.two.columns= label_tag nil, t(:report_enterprises) + .omega.fourteen.columns= select_tag(:enterprise_id_in, options_from_collection_for_select(Enterprise.all, :id, :name, params[:enterprise_id_in]), {class: "select2 fullwidth", multiple: true}) + +.row + .alpha.two.columns= label_tag nil, t(:report_users) + .omega.fourteen.columns= select_tag(:user_id_in, options_from_collection_for_select(Spree::User.all, :id, :email, params[:user_id_in]), {class: "select2 fullwidth", multiple: true}) \ No newline at end of file diff --git a/app/views/admin/reports/filters/_xero_invoices.html.haml b/app/views/admin/reports/filters/_xero_invoices.html.haml new file mode 100644 index 0000000000..d7cc558445 --- /dev/null +++ b/app/views/admin/reports/filters/_xero_invoices.html.haml @@ -0,0 +1,25 @@ += render 'admin/reports/date_range_form', f: f + +.row + .two.columns.alpha= label_tag nil, t(:report_hubs) + .fourteen.columns.omega= f.collection_select(:distributor_id_eq, @data.distributors, :id, :name, {:include_blank => 'All'}, {:class => "select2 fullwidth light"}) +.row + .two.columns.alpha= label_tag nil, t(:report_order_cycle) + .fourteen.columns.omega= f.select(:order_cycle_id_eq, + options_for_select(report_order_cycle_options(@data.order_cycles), params.dig(:q, :order_cycle_id_eq)), + {:include_blank => true}, {:class => "select2 fullwidth light"}) + +%fieldset.no-border-bottom.print-hidden{ style: "padding-bottom: 0" } + %legend{ align: 'center'}= t(:report_xero_configuration) + .row + .two.columns.alpha= label_tag :initial_invoice_number, t(:initial_invoice_number) + .fourteen.columns.omega= number_field_tag :initial_invoice_number, params[:initial_invoice_number] + .row + .two.columns.alpha= label_tag :invoice_date, t(:invoice_date) + .fourteen.columns.omega= text_field_tag :invoice_date, params[:invoice_date], class: 'datetimepicker' + .row + .two.columns.alpha= label_tag :due_date, t(:due_date) + .fourteen.columns.omega= text_field_tag :due_date, params[:due_date], class: 'datetimepicker' + .row + .two.columns.alpha= label_tag :account_code, t(:account_code) + .fourteen.columns.omega= text_field_tag :account_code, params[:account_code] diff --git a/app/views/admin/reports/index.html.haml b/app/views/admin/reports/index.html.haml new file mode 100644 index 0000000000..4465695cd6 --- /dev/null +++ b/app/views/admin/reports/index.html.haml @@ -0,0 +1,25 @@ +- content_for :page_title do + = t(:listing_reports) + +.columns.twelve + %table.index + %thead + %tr + %th= t(:name) + %th= t(:description) + %tbody + - @reports.each do |report_type, report_subtypes| + %tr + %td + - name = I18n.t(:name, scope: [:admin, :reports, report_type]) + - url = main_app.admin_report_url(report_type: report_type) + = link_to name, url + %td + - begin + = I18n.t!(:description, scope: [:admin, :reports, report_type]) + - rescue I18n::MissingTranslationData + %ul{style: "margin-left: 12pt"} + - report_subtypes.each do |report_subtype| + %li + - url = main_app.admin_report_url(report_type: report_type, report_subtype: report_subtype[1]) + = link_to report_subtype[0], url \ No newline at end of file diff --git a/app/views/admin/reports/packing.html.haml b/app/views/admin/reports/packing.html.haml deleted file mode 100644 index 48fe52194a..0000000000 --- a/app/views/admin/reports/packing.html.haml +++ /dev/null @@ -1,31 +0,0 @@ -= form_tag main_app.admin_reports_path, report_type: 'packing' do - = render partial: 'date_range_form' - - .row - .alpha.two.columns= label_tag nil, t(:report_hubs) - .omega.fourteen.columns - = collection_select("q", "order_distributor_id_in", @distributors, :id, :name, {selected: params.dig(:q, :order_distributor_id_in)}, {class: "select2 fullwidth", multiple: true}) - - .row - .alpha.two.columns= label_tag nil, t(:report_producers) - .omega.fourteen.columns - = select_tag("q[supplier_id_in]", options_from_collection_for_select(@suppliers, :id, :name, params.dig(:q, :supplier_id_in)), {class: "select2 fullwidth", multiple: true}) - - .row - .alpha.two.columns= label_tag nil, t(:report_customers_cycle) - .omega.fourteen.columns - = select_tag("q[order_cycle_id_in]", options_for_select(report_order_cycle_options(@order_cycles), params.dig(:q, :order_cycle_id_in)), {class: "select2 fullwidth", multiple: true}) - - .row - .alpha.two.columns= label_tag nil, t(:report_type) - .omega.fourteen.columns - = select_tag(:report_subtype, options_for_select(@report_subtypes, @report_subtype)) - - = render partial: "rendering_options" - - .row - = button t(:search) - -= render partial: "spree/admin/reports/customer_names_message" - -= render "table", id: "listing_orders", msg_option: t(:search) diff --git a/app/views/admin/reports/show.html.haml b/app/views/admin/reports/show.html.haml new file mode 100644 index 0000000000..3fc60d9d8b --- /dev/null +++ b/app/views/admin/reports/show.html.haml @@ -0,0 +1,22 @@ += form_for @report.search, :url => url_for(only_path: false) do |f| + %fieldset.no-border-bottom.print-hidden + %legend{ align: 'center'}= t(:report_filters) + = render partial: "admin/reports/filters/#{@report_type}", locals: { f: f } + + %fieldset.print-hidden + %legend{ align: 'center'}= t(:report_render_options) + = render partial: "rendering_options" + + .actions.filter-actions + = button t(:go), "report__submit-btn" + +.report__header.print-hidden + - if @report.message.present? + %p.report__message= @report.message + - if request.post? + %button.btn-print.icon-print{ onclick: "window.print()"}= t(:report_print) + +/ We don't want to render data unless search params are supplied. +/ Compiling data can take a long time. +- if request.post? + = render "table" diff --git a/app/views/layouts/pdf.html.haml b/app/views/layouts/pdf.html.haml new file mode 100644 index 0000000000..81b43b6588 --- /dev/null +++ b/app/views/layouts/pdf.html.haml @@ -0,0 +1,56 @@ +!!! +%html + %head + %meta{charset: 'utf-8'} + -# Using wicked_pdf_stylesheet_pack_tag with a new pdf pack was not working when using + -# WickedPdf.new.pdf_from_string cause the css file reference was not absolute + -# So I ended up putting inline css here, so it's included for sure in the PDF + :css + body { + width: 100%; + height: 100%; + margin: 0; + padding: 0; + font-family: system-ui,-apple-system,"Helvetica Neue",Arial,sans-serif; + color: #212529; + } + table { + width: 100%; + border-collapse: collapse; + } + th { + text-align: left; + } + th, td { + padding: 7px 5px; + vertical-align: middle; + text-overflow: ellipsis; + padding-top: 12px; + } + tr { + border-bottom: 1px solid #e2e2e2; + } + thead { + background-color: #f6f6f6; + border-bottom: 1px solid grey; + } + .h1, .h2, .h3 { + font-weight: bold; + padding-top: 15px; + } + .h1 { + font-size: 1.6rem; + padding-top: 20px; + } + .h2 { + font-size: 1.3rem; + } + .h3 { + font-size: 1.15rem; + } + .text-bold { + font-weight: bold; + } + + %body + = yield diff --git a/app/views/spree/admin/reports/_customer_names_message.html.haml b/app/views/spree/admin/reports/_customer_names_message.html.haml deleted file mode 100644 index 191a3e1aa1..0000000000 --- a/app/views/spree/admin/reports/_customer_names_message.html.haml +++ /dev/null @@ -1,2 +0,0 @@ -%p.customer-names-tip - = t(".customer_names_tip") diff --git a/app/views/spree/admin/reports/_customers_description.html.haml b/app/views/spree/admin/reports/_customers_description.html.haml deleted file mode 100644 index 85f93a82c0..0000000000 --- a/app/views/spree/admin/reports/_customers_description.html.haml +++ /dev/null @@ -1,4 +0,0 @@ -%ul{style: "margin-left: 12pt"} - - report_types.each do |report_type| - %li - = link_to report_type[0], "#{customers_admin_reports_url}?report_type=#{report_type[1]}" diff --git a/app/views/spree/admin/reports/_date_range_form.html.haml b/app/views/spree/admin/reports/_date_range_form.html.haml deleted file mode 100644 index 4d76b95d9f..0000000000 --- a/app/views/spree/admin/reports/_date_range_form.html.haml +++ /dev/null @@ -1,9 +0,0 @@ -.row.date-range-filter - = label_tag nil, t(:date_range) - %br - = label_tag nil, t(:start), :class => 'inline' - = f.text_field :completed_at_gt, :class => 'datetimepicker datepicker-from' - %span.range-divider - %i.icon-arrow-right - = f.text_field :completed_at_lt, :class => 'datetimepicker datepicker-to' - = label_tag nil, t(:end), :class => 'inline' diff --git a/app/views/spree/admin/reports/_link_order.html.haml b/app/views/spree/admin/reports/_link_order.html.haml deleted file mode 100644 index 1efecf6d25..0000000000 --- a/app/views/spree/admin/reports/_link_order.html.haml +++ /dev/null @@ -1,2 +0,0 @@ -- order_number = value -= link_to order_number, edit_admin_order_url(order_number), class: 'edit-order' diff --git a/app/views/spree/admin/reports/_order_cycle_management_description.html.haml b/app/views/spree/admin/reports/_order_cycle_management_description.html.haml deleted file mode 100644 index 0a1ee7f69c..0000000000 --- a/app/views/spree/admin/reports/_order_cycle_management_description.html.haml +++ /dev/null @@ -1,4 +0,0 @@ -%ul{style: "margin-left: 12pt"} - - report_types.each do |report_type| - %li - = link_to report_type[0], "#{order_cycle_management_admin_reports_url}?report_type=#{report_type[1]}" diff --git a/app/views/spree/admin/reports/_orders_and_fulfillment_description.html.haml b/app/views/spree/admin/reports/_orders_and_fulfillment_description.html.haml deleted file mode 100644 index a813492cac..0000000000 --- a/app/views/spree/admin/reports/_orders_and_fulfillment_description.html.haml +++ /dev/null @@ -1,5 +0,0 @@ -%ul{style: "margin-left: 12pt"} - - report_types.each do |report_type| - %li - = link_to report_type[0], "#{orders_and_fulfillment_admin_reports_url}?report_type=#{report_type[1]}" - \ No newline at end of file diff --git a/app/views/spree/admin/reports/_packing_description.html.haml b/app/views/spree/admin/reports/_packing_description.html.haml deleted file mode 100644 index d91e291909..0000000000 --- a/app/views/spree/admin/reports/_packing_description.html.haml +++ /dev/null @@ -1,5 +0,0 @@ -%ul{style: "margin-left: 12pt"} - - report_subtypes("packing").each do |report_subtype| - %li - = link_to t("admin.reports.packing.#{report_subtype}_report"), - main_app.admin_reports_url(report_type: 'packing', report_subtype: report_subtype) diff --git a/app/views/spree/admin/reports/_products_and_inventory_description.html.haml b/app/views/spree/admin/reports/_products_and_inventory_description.html.haml deleted file mode 100644 index f662298e3e..0000000000 --- a/app/views/spree/admin/reports/_products_and_inventory_description.html.haml +++ /dev/null @@ -1,4 +0,0 @@ -%ul{style: "margin-left: 12pt"} - - report_types.each do |report_type| - %li - = link_to report_type[0], "#{products_and_inventory_admin_reports_url}?report_type=#{report_type[1]}" diff --git a/app/views/spree/admin/reports/_sales_tax_description.html.haml b/app/views/spree/admin/reports/_sales_tax_description.html.haml deleted file mode 100644 index 5f4f7f6dfd..0000000000 --- a/app/views/spree/admin/reports/_sales_tax_description.html.haml +++ /dev/null @@ -1,4 +0,0 @@ -%ul{style: "margin-left: 12pt"} - - report_types.each do |report_type| - %li - = link_to report_type[0], "#{sales_tax_admin_reports_url}?report_type=#{report_type[1]}" diff --git a/app/views/spree/admin/reports/_table.html.haml b/app/views/spree/admin/reports/_table.html.haml deleted file mode 100644 index d1927aaf55..0000000000 --- a/app/views/spree/admin/reports/_table.html.haml +++ /dev/null @@ -1,23 +0,0 @@ -- column_partials ||= {} -- if render_content? - %table.report__table{id: id} - %thead - %tr - - @header.each do |heading| - %th= heading - %tbody - - @table.each do |row| - %tr - - row.each_with_index do |cell_value, column_index| - %td - - partial = column_partials[column_index] - - if partial - = render partial, value: cell_value - - else - = cell_value - - if @table.empty? - %tr - %td{colspan: @header.count}= t(:none) -- else - %p.report__message - = t(".select_and_search", option: msg_option.upcase) diff --git a/app/views/spree/admin/reports/customers.html.haml b/app/views/spree/admin/reports/customers.html.haml deleted file mode 100644 index 8f58149ae7..0000000000 --- a/app/views/spree/admin/reports/customers.html.haml +++ /dev/null @@ -1,32 +0,0 @@ -= form_tag spree.customers_admin_reports_url do |f| - %br - .row - .four.columns.alpha - = label_tag nil, t(:report_customers_distributor) - = select_tag(:distributor_id, - options_from_collection_for_select(@distributors, :id, :name, params[:distributor_id]), - {:include_blank => true, :class => "select2 fullwidth"}) - - .four.columns - = label_tag nil, t(:report_customers_supplier) - = select_tag(:supplier_id, - options_from_collection_for_select(@suppliers, :id, :name, params[:supplier_id]), - {:include_blank => true, :class => "select2 fullwidth"}) - - .six.columns - = label_tag nil, t(:report_customers_cycle) - = select_tag(:order_cycle_id, - options_for_select(report_order_cycle_options(@order_cycles), params[:order_cycle_id]), - {:include_blank => true, :class => "select2 fullwidth"}) - - = label_tag nil, t(:report_customers_type) - = select_tag(:report_type, options_for_select(@report_types, @report_type)) - - %br - %br - = check_box_tag :csv - = label_tag :csv, t(:report_customers_csv) - %br - = button t(:go) - -= render "table", id: "listing_customers", msg_option: t(:go) diff --git a/app/views/spree/admin/reports/index.html.haml b/app/views/spree/admin/reports/index.html.haml deleted file mode 100644 index 533eab2de1..0000000000 --- a/app/views/spree/admin/reports/index.html.haml +++ /dev/null @@ -1,14 +0,0 @@ -- content_for :page_title do - = t(:listing_reports) - -.columns.twelve - %table.index - %thead - %tr - %th= t(:name) - %th= t(:description) - %tbody - - @reports.each do |key, value| - %tr - %td= link_to value[:name], value[:url] - %td= value[:description] diff --git a/app/views/spree/admin/reports/order_cycle_management.html.haml b/app/views/spree/admin/reports/order_cycle_management.html.haml deleted file mode 100644 index c91f2e6ce7..0000000000 --- a/app/views/spree/admin/reports/order_cycle_management.html.haml +++ /dev/null @@ -1,32 +0,0 @@ -= form_for @report.search, :url => spree.order_cycle_management_admin_reports_path do |f| - = render 'date_range_form', f: f - - .row - .alpha.two.columns= label_tag nil, t(:report_hubs) - .omega.fourteen.columns= f.collection_select(:distributor_id_in, @distributors, :id, :name, {}, {class: "select2 fullwidth", multiple: true}) - - .row - .alpha.two.columns= label_tag nil, t(:report_customers_cycle) - .omega.fourteen.columns - = f.select(:order_cycle_id_in, report_order_cycle_options(@order_cycles), {selected: params.dig(:q, :order_cycle_id_in)}, {class: "select2 fullwidth", multiple: true}) - - .row - .alpha.two.columns= label_tag nil, t(:report_payment) - .omega.fourteen.columns= select_tag(:payment_method_in, options_for_select(report_payment_method_options(@report.orders), params[:payment_method_in]), {class: "select2 fullwidth", multiple: true}) - - .row - .alpha.two.columns= label_tag nil, "#{t(:shipping_methods)}: " - .omega.fourteen.columns= select_tag(:shipping_method_in, options_for_select(report_shipping_method_options(@report.orders), params[:shipping_method_in]), {class: "select2 fullwidth", multiple: true}) - - .row - .alpha.two.columns= label_tag nil, "#{t(:report_type)}: " - .omega.fourteen.columns= select_tag(:report_type, options_for_select(@report_types, @report_type)) - - .row - = check_box_tag :csv - = label_tag :csv, t(:report_customers_csv) - - .row - = button t(:search) - -= render "table", id: "listing_ocm_orders", msg_option: t(:search) diff --git a/app/views/spree/admin/reports/orders_and_distributors.html.haml b/app/views/spree/admin/reports/orders_and_distributors.html.haml deleted file mode 100644 index 4d99b181af..0000000000 --- a/app/views/spree/admin/reports/orders_and_distributors.html.haml +++ /dev/null @@ -1,11 +0,0 @@ -= form_for @search, :url => spree.orders_and_distributors_admin_reports_path do |f| - = render 'date_range_form', f: f - - = check_box_tag :csv - = label_tag :csv, t(:report_customers_csv) - %br - = button t(:search) - -= render partial: "customer_names_message" - -= render "table", id: "listing_orders", msg_option: t(:search) diff --git a/app/views/spree/admin/reports/orders_and_fulfillment.html.haml b/app/views/spree/admin/reports/orders_and_fulfillment.html.haml deleted file mode 100644 index 038c4d8a0d..0000000000 --- a/app/views/spree/admin/reports/orders_and_fulfillment.html.haml +++ /dev/null @@ -1,30 +0,0 @@ -= form_for @report.search, :url => spree.orders_and_fulfillment_admin_reports_path do |f| - = render 'date_range_form', f: f - - .row - .alpha.two.columns= label_tag nil, t(:report_hubs) - .omega.fourteen.columns= f.collection_select(:distributor_id_in, @distributors, :id, :name, {}, {class: "select2 fullwidth", multiple: true}) - - .row - .alpha.two.columns= label_tag nil, t(:report_producers) - .omega.fourteen.columns= select_tag(:supplier_id_in, options_from_collection_for_select(@suppliers, :id, :name, params[:supplier_id_in]), {class: "select2 fullwidth", multiple: true}) - - .row - .alpha.two.columns= label_tag nil, t(:report_customers_cycle) - .omega.fourteen.columns - = f.select(:order_cycle_id_in, report_order_cycle_options(@order_cycles), {selected: params.dig(:q, :order_cycle_id_in)}, {class: "select2 fullwidth", multiple: true}) - - .row - .alpha.two.columns= label_tag nil, t(:report_type) - .omega.fourteen.columns= select_tag(:report_type, options_for_select(@report_types, @report_type)) - - .row - = check_box_tag :csv - = label_tag :csv, t(:report_customers_csv) - - .row - = button t(:search) - -= render partial: "customer_names_message" - -= render "table", id: "listing_orders", msg_option: t(:search) diff --git a/app/views/spree/admin/reports/payments.html.haml b/app/views/spree/admin/reports/payments.html.haml deleted file mode 100644 index 5da46d8d5e..0000000000 --- a/app/views/spree/admin/reports/payments.html.haml +++ /dev/null @@ -1,19 +0,0 @@ -= form_for @report.search, :url => spree.payments_admin_reports_path do |f| - = render 'date_range_form', f: f - - .row - .four.columns.alpha - = label_tag nil, t(:report_distributor) - = f.collection_select(:distributor_id_eq, @distributors, :id, :name, {:include_blank => t(:report_all)}, {:class => "select2 fullwidth"}) - = label_tag nil, t(:report_customers_type) - %br - = select_tag(:report_type, options_for_select([[t(:report_payment_by),:payments_by_payment_type],[t(:report_itemised_payment),:itemised_payment_totals],[t(:report_payment_totals),:payment_totals]], @report_type)) - %br - %br - = check_box_tag :csv - = label_tag :csv, t(:report_customers_csv) - %br - %br - = button t(:search) - -= render "table", id: "listing_orders", msg_option: t(:search) diff --git a/app/views/spree/admin/reports/products_and_inventory.html.haml b/app/views/spree/admin/reports/products_and_inventory.html.haml deleted file mode 100644 index 73d31a6990..0000000000 --- a/app/views/spree/admin/reports/products_and_inventory.html.haml +++ /dev/null @@ -1,34 +0,0 @@ -= form_tag spree.products_and_inventory_admin_reports_url do |f| - %br - .row - .four.columns.alpha - = label_tag nil, t(:report_distributor) - = select_tag(:distributor_id, - options_from_collection_for_select(@distributors, :id, :name, params[:distributor_id]), - {:include_blank => true, :class => "select2 fullwidth"}) - - - .four.columns - = label_tag nil, t(:report_customers_supplier) - = select_tag(:supplier_id, - options_from_collection_for_select(@suppliers, :id, :name, params[:supplier_id]), - {:include_blank => true, :class => "select2 fullwidth"}) - - - .six.columns - = label_tag nil, t(:report_order_cycle) - = select_tag(:order_cycle_id, - options_for_select(report_order_cycle_options(@order_cycles), params[:order_cycle_id]), - {:include_blank => true, :class => "select2 fullwidth"}) - - = label_tag nil, t(:report_type) - %br - = select_tag(:report_type, options_for_select(@report_types, params[:report_type])) - - %br - %br - = check_box_tag :csv - = label_tag :csv, t(:report_customers_csv) - %br - = button t(:go) -= render "table", id: "listing_products", msg_option: t(:go) diff --git a/app/views/spree/admin/reports/sales_tax.html.haml b/app/views/spree/admin/reports/sales_tax.html.haml deleted file mode 100644 index 1482d27266..0000000000 --- a/app/views/spree/admin/reports/sales_tax.html.haml +++ /dev/null @@ -1,19 +0,0 @@ -= form_for @report.search, :url => spree.sales_tax_admin_reports_path do |f| - = render 'date_range_form', f: f - - .row - .four.columns.alpha - = label_tag nil, t(:report_distributor) - = f.collection_select(:distributor_id_eq, @distributors, :id, :name, {:include_blank => t(:all)}, {:class => "select2 fullwidth"}) - = label_tag nil, t(:report_customers_type) - %br - = select_tag(:report_type, options_for_select([[t(:report_tax_types),:tax_types],[t(:report_tax_rates),:tax_rates]], @report_type)) - %br - %br - = check_box_tag :csv - = label_tag :csv, t(:report_customers_csv) - %br - %br - = button t(:search) - -= render "table", id: "listing_orders", column_partials: {0 => "link_order"}, msg_option: t(:search) diff --git a/app/views/spree/admin/reports/users_and_enterprises.html.haml b/app/views/spree/admin/reports/users_and_enterprises.html.haml deleted file mode 100644 index 54adef9bb7..0000000000 --- a/app/views/spree/admin/reports/users_and_enterprises.html.haml +++ /dev/null @@ -1,21 +0,0 @@ -= form_tag spree.users_and_enterprises_admin_reports_url do |f| - .row - .alpha.two.columns= label_tag nil, t(:report_enterprises) - .omega.fourteen.columns= select_tag(:enterprise_id_in, options_from_collection_for_select(Enterprise.all, :id, :name, params[:enterprise_id_in]&.split(",")), {class: "select2 fullwidth", multiple: true}) - - .row - .alpha.two.columns= label_tag nil, t(:report_users) - .omega.fourteen.columns= select_tag(:user_id_in, options_from_collection_for_select(Spree::User.all, :id, :email, params[:user_id_in]&.split(",")), {class: "select2 fullwidth", multiple: true}) - - -# Might need this later if we add different kinds of reports - -# .row - -# .alpha.two.columns= label_tag nil, "Report Type: " - -# .omega.fourteen.columns= select_tag(:report_type, options_for_select(@report_types, params[:report_type])) - - .row - = check_box_tag :csv - = label_tag :csv, t(:report_customers_csv) - .row - = button t(:search) - -= render "table", id: "users_and_enterprises", msg_option: t(:search) diff --git a/app/views/spree/admin/reports/xero_invoices.html.haml b/app/views/spree/admin/reports/xero_invoices.html.haml deleted file mode 100644 index b445e1b230..0000000000 --- a/app/views/spree/admin/reports/xero_invoices.html.haml +++ /dev/null @@ -1,35 +0,0 @@ -= form_for @report.search, url: spree.xero_invoices_admin_reports_path do |f| - = render 'date_range_form', f: f - - .row - .four.columns.alpha= label_tag :report_type, t(:report_type) - .four.columns.omega= select_tag :report_type, options_for_select(xero_report_types, params[:report_type]), {include_blank: false, class: "select2 fullwidth"} - .row - .four.columns.alpha= label_tag nil, t(:report_hubs) - .four.columns.omega= f.collection_select(:distributor_id_eq, @distributors, :id, :name, {:include_blank => 'All'}, {:class => "select2 fullwidth"}) - .row - .four.columns.alpha= label_tag nil, t(:report_order_cycle) - .four.columns.omega= f.select(:order_cycle_id_eq, - options_for_select(report_order_cycle_options(@order_cycles), params.dig(:q, :order_cycle_id_eq)), - {:include_blank => true}, {:class => "select2 fullwidth"}) - - .row - .four.columns.alpha= label_tag :initial_invoice_number, t(:initial_invoice_number) - .twelve.columns.omega= text_field_tag :initial_invoice_number, params[:initial_invoice_number] - .row - .four.columns.alpha= label_tag :invoice_date, t(:invoice_date) - .twelve.columns.omega= text_field_tag :invoice_date, params[:invoice_date], class: 'datetimepicker' - .row - .four.columns.alpha= label_tag :due_date, t(:due_date) - .twelve.columns.omega= text_field_tag :due_date, params[:due_date], class: 'datetimepicker' - .row - .four.columns.alpha= label_tag :account_code, t(:account_code) - .twelve.columns.omega= text_field_tag :account_code, params[:account_code] - .row - .four.columns.alpha= label_tag :csv, t(:report_customers_csv) - .twelve.columns.omega= check_box_tag :csv - .row - .four.columns.alpha= button t(:search) - - -= render "table", id: "listing_invoices", msg_option: t(:search) diff --git a/app/views/spree/admin/shared/_head.html.haml b/app/views/spree/admin/shared/_head.html.haml index c90e5f0263..d4c5856fc3 100644 --- a/app/views/spree/admin/shared/_head.html.haml +++ b/app/views/spree/admin/shared/_head.html.haml @@ -11,7 +11,7 @@ %link{:href => "https://fonts.googleapis.com/css?family=Open+Sans:400italic,600italic,400,600&subset=latin,cyrillic,greek,vietnamese", :rel => "stylesheet", :type => "text/css"} -= stylesheet_pack_tag 'admin-styles' += stylesheet_pack_tag 'admin-styles', media: "screen, print" = render "layouts/bugsnag_js" = javascript_include_tag 'admin/all' diff --git a/app/views/spree/admin/shared/_tabs.html.haml b/app/views/spree/admin/shared/_tabs.html.haml index eb653c2500..696a3d29a6 100644 --- a/app/views/spree/admin/shared/_tabs.html.haml +++ b/app/views/spree/admin/shared/_tabs.html.haml @@ -2,7 +2,7 @@ = tab :products, :properties, :inventory, :product_import, :images, :variants, :product_properties, :group_buy_options, :seo, url: admin_products_path, icon: 'icon-th-large' = tab :order_cycles, url: main_app.admin_order_cycles_path, icon: 'icon-refresh' = tab :orders, :subscriptions, :customer_details, :adjustments, :payments, :return_authorizations, url: admin_orders_path('q[s]' => 'completed_at desc'), icon: 'icon-shopping-cart' -= tab :reports, icon: 'icon-file' += tab :reports, url: main_app.admin_reports_path, icon: 'icon-file' = tab :general_settings, :mail_methods, :tax_categories, :tax_rates, :tax_settings, :zones, :countries, :states, :payment_methods, :taxonomies, :shipping_methods, :shipping_categories, :enterprise_fees, :contents, :invoice_settings, :matomo_settings, :stripe_connect_settings, label: 'configuration', icon: 'icon-wrench', url: edit_admin_general_settings_path = tab :enterprises, :enterprise_relationships, url: main_app.admin_enterprises_path = tab :customers, url: main_app.admin_customers_path diff --git a/app/webpacker/css/admin/components/date-picker.scss b/app/webpacker/css/admin/components/date-picker.scss index 8511a73617..5296bd1f6a 100644 --- a/app/webpacker/css/admin/components/date-picker.scss +++ b/app/webpacker/css/admin/components/date-picker.scss @@ -3,6 +3,7 @@ .date-range-filter { .range-divider { padding: 0; + margin-left: 5px; } input.datepicker { width: 96px !important; diff --git a/app/webpacker/css/admin/openfoodnetwork.scss b/app/webpacker/css/admin/openfoodnetwork.scss index a573de4126..636b938d6d 100644 --- a/app/webpacker/css/admin/openfoodnetwork.scss +++ b/app/webpacker/css/admin/openfoodnetwork.scss @@ -6,6 +6,10 @@ text-align: right; } +.text-bold{ + font-weight: bold; +} + .underline { text-decoration: underline; } diff --git a/app/webpacker/css/admin/reports.scss b/app/webpacker/css/admin/reports.scss index e8c36b6af6..d51d54204b 100644 --- a/app/webpacker/css/admin/reports.scss +++ b/app/webpacker/css/admin/reports.scss @@ -1,29 +1,62 @@ -.report__table { - margin-top: 2em; +table.report__table { + margin-top: 1em; + @media print { + margin: 0; + td, th { + border-left: none; + border-right: none; + } + } + thead th { + text-align: left; + padding: 10px 6px; + } + + .header-row { + &.h1, &.h2, &.h3 { + font-weight: bold; + margin-top: 8px; + } + &.h1 { + font-size: 2em; + } + &.h2 { + font-size: 1.5em; + } + &.h3 { + font-size: 1.25em; + } + &.h4 { + font-size: 1; + } + &.with-background { + background-color: #eef5fc; + } + } } -.report__message { +.report__header { + display: flex; margin-top: 2em; - border: 1px solid $pale-blue; - border-radius: .5em; - padding: .5em; - text-align: center; + justify-content: flex-end; + + .btn-print { + margin-left: 1em; + } + .report__message { + border: 1px solid $pale-blue; + border-radius: .5em; + padding: .5em; + text-align: center; + flex-grow: 1; + } } .customer-names-tip { margin-top: 1em; } -.rendering-options { - select { - display: block; - float: left; - } - - .inline-checkbox { - line-height: 2.5em; - margin-left: 1em; - display: block; - float: left; - } +.report__submit-btn { + margin: 0 !important; + width: 120px; } diff --git a/app/webpacker/css/admin/shared/forms.scss b/app/webpacker/css/admin/shared/forms.scss index 7e4dc7c3bb..e1c92e38ae 100644 --- a/app/webpacker/css/admin/shared/forms.scss +++ b/app/webpacker/css/admin/shared/forms.scss @@ -242,3 +242,17 @@ select { @extend input, [type="text"]; background-color: white; } + +.inline-checkbox { + display: inline-flex; + align-items: center; + margin-top: 3px; + + input, label { + cursor: pointer; + } + label { + margin: 0; + padding-left: .4rem; + } +} diff --git a/app/webpacker/css/admin/shared/layout.scss b/app/webpacker/css/admin/shared/layout.scss index 7705d09caa..0df03e79c0 100644 --- a/app/webpacker/css/admin/shared/layout.scss +++ b/app/webpacker/css/admin/shared/layout.scss @@ -68,6 +68,11 @@ .hidden { display: none; } +@media print { + .print-hidden { + display: none !important; + } +} // Header //--------------------------------------------------- @@ -103,3 +108,9 @@ border-top: 1px solid $color-border; padding: 10px 0; } + +@media print { + header, nav { + display:none; + } +} \ No newline at end of file diff --git a/config/locales/en.yml b/config/locales/en.yml index ebfff986f6..82c667fb07 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -160,16 +160,6 @@ en: withdrawal_count_limit_exceeded: "The customer has exceeded the balance or credit limit available on their card." activemodel: - attributes: - order_management/reports/enterprise_fee_summary/parameters: - start_at: "Start" - end_at: "End" - distributor_ids: "Hubs" - producer_ids: "Producers" - order_cycle_ids: "Order Cycles" - enterprise_fee_ids: "Fees Names" - shipping_method_ids: "Shipping Methods" - payment_method_ids: "Payment Methods" errors: messages: inclusion: "is not included in the list" @@ -1265,6 +1255,8 @@ en: unitsize: UNITSIZE total: TOTAL total_items: TOTAL ITEMS + total_by_customer: Total By Customer + total_by_supplier: Total By Supplier supplier_totals: Order Cycle Supplier Totals supplier_totals_by_distributor: Order Cycle Supplier Totals by Distributor totals_by_supplier: Order Cycle Distributor Totals by Supplier @@ -1280,6 +1272,7 @@ en: tax_rates: Tax Rates pack_by_customer: Pack By Customer pack_by_supplier: Pack By Supplier + pack_by_product: Pack By Product orders_and_distributors: name: Orders And Distributors description: Orders with distributor details @@ -1295,7 +1288,6 @@ en: name: Customers products_and_inventory: name: Products & Inventory - description: users_and_enterprises: name: Users & Enterprises description: Enterprise Ownership & Status @@ -1329,17 +1321,18 @@ en: quantity: "Quantity" is_temperature_controlled: "TempControlled?" temp_controlled: "TempControlled?" + price: "Price" rendering_options: - generate_report: "Generate report:" + generate_report: "Generate report" on_screen: "On screen" - csv_spreadsheet: "CSV Spreadsheet" - excel_spreadsheet: "Excel Spreadsheet" - openoffice_spreadsheet: "OpenOffice Spreadsheet" - hide_summary_rows: "Hide summary Rows" + spreadsheet: "Spreadsheet (Excel, OpenOffice..)" + display: Display + summary_row: Summary Row + header_row: Header Row + raw_data: Raw Data + formatted_data: Formatted Data packing: name: "Packing Reports" - customer_report: "Pack By Customer" - supplier_report: "Pack By Supplier" subscriptions: index: title: "Subscriptions" @@ -2622,20 +2615,25 @@ See the %{link} to find out more about %{sitename}'s features and to start using report_customers_cycle: "Order Cycle" report_customers_type: "Report Type" report_customers_csv: "Download as csv" - report_producers: "Producers: " - report_type: "Report Type: " - report_hubs: "Hubs: " - report_payment: "Payment Methods: " - report_distributor: "Distributor: " + report_producers: "Producers" + report_type: "Report Type" + report_hubs: "Hubs" + report_payment: "Payment Methods" + report_distributor: "Distributor" report_payment_by: 'Payments By Type' report_itemised_payment: 'Itemised Payment Totals' report_payment_totals: 'Payment Totals' report_all: 'all' - report_order_cycle: "Order Cycle: " - report_enterprises: "Enterprises: " - report_users: "Users: " + report_order_cycle: "Order Cycle" + report_hide_columns: Columns to Hide + report_enterprises: "Enterprises" + report_enterprise_fee: "Fees Names" + report_users: "Users" report_tax_rates: Tax rates report_tax_types: Tax types + report_filters: Report Filters + report_print: Print Report + report_render_options: Rendering Options report_header_order_cycle: Order Cycle report_header_user: User report_header_email: Email @@ -2771,10 +2769,11 @@ See the %{link} to find out more about %{sitename}'s features and to start using report_header_transaction_fee: Transaction Fee (no tax) report_header_total_untaxable_admin: Total untaxable admin adjustments (no tax) report_header_total_taxable_admin: Total taxable admin adjustments (tax inclusive) - initial_invoice_number: "Initial invoice number:" - invoice_date: "Invoice date:" - due_date: "Due date:" - account_code: "Account code:" + report_xero_configuration: Xero Configuration + initial_invoice_number: "Initial invoice number" + invoice_date: "Invoice date" + due_date: "Due date" + account_code: "Account code" equals: "Equals" contains: "contains" discount: "Discount" @@ -3310,9 +3309,6 @@ See the %{link} to find out more about %{sitename}'s features and to start using bulk_coop_customer_payments: "Bulk Co-op Customer Payments" bulk_coop_packing_sheets: "Bulk Co-op Packing Sheets" bulk_coop_supplier_report: "Bulk Co-op Supplier Report" - date_range: "Date range" - generate_report: "Generate Report" - report_format_csv: "Report format CSV" enterprise_fee_summaries: filters: date_range: "Date Range" @@ -3954,11 +3950,6 @@ See the %{link} to find out more about %{sitename}'s features and to start using reports: table: select_and_search: "Select filters and click on %{option} to access your data." - bulk_coop: - bulk_coop_supplier_report: 'Bulk Co-op - Totals by Supplier' - bulk_coop_allocation: 'Bulk Co-op - Allocation' - bulk_coop_packing_sheets: 'Bulk Co-op - Packing Sheets' - bulk_coop_customer_payments: 'Bulk Co-op - Customer Payments' customer_names_message: customer_names_tip: "If customer names are hidden for orders you have supplied, you can contact the distributor and ask if they can update their shop preferences to allow their suppliers to view customer names." users: diff --git a/config/routes/admin.rb b/config/routes/admin.rb index 954b2861d4..7fc4afcd8f 100644 --- a/config/routes/admin.rb +++ b/config/routes/admin.rb @@ -115,6 +115,7 @@ Openfoodnetwork::Application.routes.draw do put :resume, on: :member, format: :json end - match '/reports/:report_type(/:report_subtype)', to: 'reports#show', via: [:get, :post], as: :reports + get '/reports', to: 'reports#index', as: :reports + match '/reports/:report_type(/:report_subtype)', to: 'reports#show', via: [:get, :post], as: :report end end diff --git a/config/routes/spree.rb b/config/routes/spree.rb index 1e495f3122..eed705f5d6 100644 --- a/config/routes/spree.rb +++ b/config/routes/spree.rb @@ -31,18 +31,6 @@ Spree::Core::Engine.routes.draw do resource :account, :controller => 'users' - match '/admin/reports/orders_and_distributors' => 'admin/reports#orders_and_distributors', :as => "orders_and_distributors_admin_reports", :via => [:get, :post] - match '/admin/reports/order_cycle_management' => 'admin/reports#order_cycle_management', :as => "order_cycle_management_admin_reports", :via => [:get, :post] - match '/admin/reports/group_buys' => 'admin/reports#group_buys', :as => "group_buys_admin_reports", :via => [:get, :post] - match '/admin/reports/bulk_coop' => 'admin/reports#bulk_coop', :as => "bulk_coop_admin_reports", :via => [:get, :post] - match '/admin/reports/payments' => 'admin/reports#payments', :as => "payments_admin_reports", :via => [:get, :post] - match '/admin/reports/orders_and_fulfillment' => 'admin/reports#orders_and_fulfillment', :as => "orders_and_fulfillment_admin_reports", :via => [:get, :post] - match '/admin/reports/users_and_enterprises' => 'admin/reports#users_and_enterprises', :as => "users_and_enterprises_admin_reports", :via => [:get, :post] - match '/admin/reports/sales_tax' => 'admin/reports#sales_tax', :as => "sales_tax_admin_reports", :via => [:get, :post] - match '/admin/reports/products_and_inventory' => 'admin/reports#products_and_inventory', :as => "products_and_inventory_admin_reports", :via => [:get, :post] - match '/admin/reports/customers' => 'admin/reports#customers', :as => "customers_admin_reports", :via => [:get, :post] - match '/admin/reports/xero_invoices' => 'admin/reports#xero_invoices', :as => "xero_invoices_admin_reports", :via => [:get, :post] - match '/admin/orders/bulk_management' => 'admin/orders#bulk_management', :as => "admin_bulk_order_management", via: :get match '/admin/payment_methods/show_provider_preferences' => 'admin/payment_methods#show_provider_preferences', :via => :get put 'credit_cards/new_from_token', to: 'credit_cards#new_from_token' @@ -126,8 +114,6 @@ Spree::Core::Engine.routes.draw do end end - resources :reports, only: :index - resources :users do member do put :generate_api_key diff --git a/engines/order_management/app/controllers/order_management/reports/bulk_coop_controller.rb b/engines/order_management/app/controllers/order_management/reports/bulk_coop_controller.rb deleted file mode 100644 index 3de16eaf61..0000000000 --- a/engines/order_management/app/controllers/order_management/reports/bulk_coop_controller.rb +++ /dev/null @@ -1,78 +0,0 @@ -# frozen_string_literal: true - -module OrderManagement - module Reports - class BulkCoopController < Spree::Admin::BaseController - before_action :load_report_parameters - before_action :load_permissions - - def new; end - - def create - return respond_to_invalid_parameters unless @report_parameters.valid? - - @report_parameters.authorize!(@permissions) - - @report = report_klass::ReportService.new(@permissions, legacy_format_report_params, - spree_current_user) - renderer.render(self) - rescue ::Reports::Authorizer::ParameterNotAllowedError => e - flash[:error] = e.message - render_report_form - end - - private - - def respond_to_invalid_parameters - flash[:error] = I18n.t("invalid_filter_parameters", scope: i18n_scope) - render_report_form - end - - def i18n_scope - "order_management.reports.enterprise_fee_summary" - end - - def render_report_form - render action: :new - end - - def report_klass - OrderManagement::Reports::BulkCoop - end - - def legacy_format_report_params - { - q: { - completed_at_gt: params[:report][:start_at], - completed_at_lt: params[:report][:end_at], - distributor_id_in: params[:report][:distributor_ids], - }, - report_type: params[:report][:report_type] - } - end - - def load_report_parameters - @report_parameters = report_klass::Parameters.new(params[:report] || {}) - end - - def load_permissions - @permissions = report_klass::Permissions.new(spree_current_user) - end - - def report_renderer_klass - case params[:report_format] - when "csv" - report_klass::Renderers::CsvRenderer - when nil, "", "html" - report_klass::Renderers::HtmlRenderer - else - raise Reports::UnsupportedReportFormatException - end - end - - def renderer - @renderer ||= report_renderer_klass.new(@report) - end - end - end -end diff --git a/engines/order_management/app/controllers/order_management/reports/enterprise_fee_summaries_controller.rb b/engines/order_management/app/controllers/order_management/reports/enterprise_fee_summaries_controller.rb deleted file mode 100644 index 9f52ab3e9b..0000000000 --- a/engines/order_management/app/controllers/order_management/reports/enterprise_fee_summaries_controller.rb +++ /dev/null @@ -1,66 +0,0 @@ -# frozen_string_literal: true - -module OrderManagement - module Reports - class EnterpriseFeeSummariesController < Spree::Admin::BaseController - before_action :load_report_parameters - before_action :load_permissions - - def new; end - - def create - return respond_to_invalid_parameters unless @report_parameters.valid? - - @report_parameters.authorize!(@permissions) - - @report = report_klass::ReportService.new(@permissions, @report_parameters) - renderer.render(self) - rescue ::Reports::Authorizer::ParameterNotAllowedError => e - flash[:error] = e.message - render_report_form - end - - private - - def respond_to_invalid_parameters - flash[:error] = I18n.t("invalid_filter_parameters", scope: i18n_scope) - render_report_form - end - - def i18n_scope - "order_management.reports.enterprise_fee_summary" - end - - def render_report_form - render action: :new - end - - def report_klass - OrderManagement::Reports::EnterpriseFeeSummary - end - - def load_report_parameters - @report_parameters = report_klass::Parameters.new(params[:report] || {}) - end - - def load_permissions - @permissions = report_klass::Permissions.new(spree_current_user) - end - - def report_renderer_klass - case params[:report_format] - when "csv" - report_klass::Renderers::CsvRenderer - when nil, "", "html" - report_klass::Renderers::HtmlRenderer - else - raise Reports::UnsupportedReportFormatException - end - end - - def renderer - @renderer ||= report_renderer_klass.new(@report) - end - end - end -end diff --git a/engines/order_management/app/services/order_management/reports/bulk_coop/authorizer.rb b/engines/order_management/app/services/order_management/reports/bulk_coop/authorizer.rb deleted file mode 100644 index b0e398e0a0..0000000000 --- a/engines/order_management/app/services/order_management/reports/bulk_coop/authorizer.rb +++ /dev/null @@ -1,13 +0,0 @@ -# frozen_string_literal: true - -module OrderManagement - module Reports - module BulkCoop - class Authorizer < ::Reports::Authorizer - def authorize! - require_ids_allowed(parameters.distributor_ids, permissions.allowed_distributors) - end - end - end - end -end diff --git a/engines/order_management/app/services/order_management/reports/bulk_coop/bulk_coop_allocation_report.rb b/engines/order_management/app/services/order_management/reports/bulk_coop/bulk_coop_allocation_report.rb deleted file mode 100644 index 9513f51e9b..0000000000 --- a/engines/order_management/app/services/order_management/reports/bulk_coop/bulk_coop_allocation_report.rb +++ /dev/null @@ -1,67 +0,0 @@ -# frozen_string_literal: true - -module OrderManagement - module Reports - module BulkCoop - class BulkCoopAllocationReport - def header - [ - 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/engines/order_management/app/services/order_management/reports/bulk_coop/bulk_coop_report.rb b/engines/order_management/app/services/order_management/reports/bulk_coop/bulk_coop_report.rb deleted file mode 100644 index 5a5bbc7b33..0000000000 --- a/engines/order_management/app/services/order_management/reports/bulk_coop/bulk_coop_report.rb +++ /dev/null @@ -1,323 +0,0 @@ -# frozen_string_literal: true - -require "open_food_network/reports/line_items" - -module OrderManagement - module Reports - module BulkCoop - class BulkCoopReport - REPORT_TYPES = [ - :bulk_coop_supplier_report, - :bulk_coop_allocation, - :bulk_coop_packing_sheets, - :bulk_coop_customer_payments - ].freeze - - 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 header - case params[:report_type] - when "bulk_coop_supplier_report" - @supplier_report.header - when "bulk_coop_allocation" - @allocation_report.header - 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 rules - case params[:report_type] - 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_type] - 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) - (line_items.first.variant.product.group_buy_unit_size || 0.0) / - (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/engines/order_management/app/services/order_management/reports/bulk_coop/bulk_coop_supplier_report.rb b/engines/order_management/app/services/order_management/reports/bulk_coop/bulk_coop_supplier_report.rb deleted file mode 100644 index 0f57c58f6a..0000000000 --- a/engines/order_management/app/services/order_management/reports/bulk_coop/bulk_coop_supplier_report.rb +++ /dev/null @@ -1,65 +0,0 @@ -# frozen_string_literal: true - -module OrderManagement - module Reports - module BulkCoop - class BulkCoopSupplierReport - def header - [ - 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/engines/order_management/app/services/order_management/reports/bulk_coop/parameters.rb b/engines/order_management/app/services/order_management/reports/bulk_coop/parameters.rb deleted file mode 100644 index 2f5a48d296..0000000000 --- a/engines/order_management/app/services/order_management/reports/bulk_coop/parameters.rb +++ /dev/null @@ -1,45 +0,0 @@ -# frozen_string_literal: true - -module OrderManagement - module Reports - module BulkCoop - class Parameters < ::Reports::Parameters::Base - extend ActiveModel::Naming - extend ActiveModel::Translation - include ActiveModel::Validations - - attr_accessor :start_at, :end_at, :distributor_ids, :report_type - - before_validation :cleanup_arrays - - validates :start_at, :end_at, date_time_string: true - validates :distributor_ids, integer_array: true - validates_inclusion_of :report_type, in: BulkCoopReport::REPORT_TYPES.map(&:to_s) - - validate :require_valid_datetime_range - - def initialize(attributes = {}) - self.distributor_ids = [] - - super(attributes) - end - - def authorize!(permissions) - authorizer = Authorizer.new(self, permissions) - authorizer.authorize! - end - - protected - - # Remove the blank strings that Rails multiple selects add by default to - # make sure that blank lists are still submitted to the server as arrays - # instead of nil. - # - # https://api.rubyonrails.org/classes/ActionView/Helpers/FormOptionsHelper.html#method-i-select - def cleanup_arrays - distributor_ids.reject!(&:blank?) - end - end - end - end -end diff --git a/engines/order_management/app/services/order_management/reports/bulk_coop/permissions.rb b/engines/order_management/app/services/order_management/reports/bulk_coop/permissions.rb deleted file mode 100644 index ca2a38467c..0000000000 --- a/engines/order_management/app/services/order_management/reports/bulk_coop/permissions.rb +++ /dev/null @@ -1,13 +0,0 @@ -# frozen_string_literal: true - -module OrderManagement - module Reports - module BulkCoop - class Permissions < ::Reports::Permissions - def allowed_distributors - @allowed_distributors ||= Enterprise.is_distributor.managed_by(user) - end - end - end - end -end diff --git a/engines/order_management/app/services/order_management/reports/bulk_coop/renderers/csv_renderer.rb b/engines/order_management/app/services/order_management/reports/bulk_coop/renderers/csv_renderer.rb deleted file mode 100644 index f848edbe71..0000000000 --- a/engines/order_management/app/services/order_management/reports/bulk_coop/renderers/csv_renderer.rb +++ /dev/null @@ -1,32 +0,0 @@ -# frozen_string_literal: true - -module OrderManagement - module Reports - module BulkCoop - module Renderers - class CsvRenderer < ::Reports::Renderers::Base - def render(context) - context.send_data(generate, filename: filename) - end - - def generate - CSV.generate do |csv| - csv << report_data.header - - report_data.list.each do |data| - csv << data - end - end - end - - private - - def filename - timestamp = Time.zone.now.strftime("%Y%m%d") - "#{report_data.parameters[:report_type]}_#{timestamp}.csv" - end - end - end - end - end -end diff --git a/engines/order_management/app/services/order_management/reports/bulk_coop/renderers/html_renderer.rb b/engines/order_management/app/services/order_management/reports/bulk_coop/renderers/html_renderer.rb deleted file mode 100644 index 92594feaae..0000000000 --- a/engines/order_management/app/services/order_management/reports/bulk_coop/renderers/html_renderer.rb +++ /dev/null @@ -1,22 +0,0 @@ -# frozen_string_literal: true - -module OrderManagement - module Reports - module BulkCoop - module Renderers - class HtmlRenderer < ::Reports::Renderers::Base - def render(context) - context.instance_variable_set :@renderer, self - context.render(action: :create, renderer: self) - end - - delegate :header, to: :report_data - - def data_rows - report_data.list - end - end - end - end - end -end diff --git a/engines/order_management/app/services/order_management/reports/bulk_coop/report_service.rb b/engines/order_management/app/services/order_management/reports/bulk_coop/report_service.rb deleted file mode 100644 index ac7f965218..0000000000 --- a/engines/order_management/app/services/order_management/reports/bulk_coop/report_service.rb +++ /dev/null @@ -1,29 +0,0 @@ -# frozen_string_literal: true - -require 'open_food_network/order_grouper' - -module OrderManagement - module Reports - module BulkCoop - class ReportService - attr_accessor :permissions, :parameters, :user - - def initialize(permissions, parameters, user) - @permissions = permissions - @parameters = parameters - @user = user - @report = BulkCoopReport.new(user, parameters, true) - end - - def header - @report.header - end - - def list - order_grouper = OpenFoodNetwork::OrderGrouper.new @report.rules, @report.columns, @report - order_grouper.table(@report.table_items) - end - end - end - end -end diff --git a/engines/order_management/app/services/order_management/reports/enterprise_fee_summary/renderers/csv_renderer.rb b/engines/order_management/app/services/order_management/reports/enterprise_fee_summary/renderers/csv_renderer.rb deleted file mode 100644 index 253f904430..0000000000 --- a/engines/order_management/app/services/order_management/reports/enterprise_fee_summary/renderers/csv_renderer.rb +++ /dev/null @@ -1,66 +0,0 @@ -# frozen_string_literal: true - -module OrderManagement - module Reports - module EnterpriseFeeSummary - module Renderers - class CsvRenderer < ::Reports::Renderers::Base - def render(context) - context.send_data(generate, filename: filename) - end - - def generate - CSV.generate do |csv| - render_header(csv) - - report_data.list.each do |data| - render_data_row(csv, data) - end - end - end - - private - - def filename - timestamp = Time.zone.now.strftime("%Y%m%d") - "enterprise_fee_summary_#{timestamp}.csv" - end - - def render_header(csv) - csv << [ - header_label(:fee_type), - header_label(:enterprise_name), - header_label(:fee_name), - header_label(:customer_name), - header_label(:fee_placement), - header_label(:fee_calculated_on_transfer_through_name), - header_label(:tax_category_name), - header_label(:total_amount) - ] - end - - def render_data_row(csv, data) - csv << [ - data.fee_type, - data.enterprise_name, - data.fee_name, - data.customer_name, - data.fee_placement, - data.fee_calculated_on_transfer_through_name, - data.tax_category_name, - data.total_amount - ] - end - - def header_label(attribute) - I18n.t("header.#{attribute}", scope: i18n_scope) - end - - def i18n_scope - "order_management.reports.enterprise_fee_summary.formats.csv" - end - end - end - end - end -end diff --git a/engines/order_management/app/services/order_management/reports/enterprise_fee_summary/renderers/html_renderer.rb b/engines/order_management/app/services/order_management/reports/enterprise_fee_summary/renderers/html_renderer.rb deleted file mode 100644 index fa372fe893..0000000000 --- a/engines/order_management/app/services/order_management/reports/enterprise_fee_summary/renderers/html_renderer.rb +++ /dev/null @@ -1,53 +0,0 @@ -# frozen_string_literal: true - -module OrderManagement - module Reports - module EnterpriseFeeSummary - module Renderers - class HtmlRenderer < ::Reports::Renderers::Base - def render(context) - context.instance_variable_set :@renderer, self - context.render(action: :create, renderer: self) - end - - def header - data_row_attributes.map do |attribute| - header_label(attribute) - end - end - - def data_rows - report_data.list.map do |data| - data_row_attributes.map do |attribute| - data.public_send(attribute) - end - end - end - - private - - def data_row_attributes - [ - :fee_type, - :enterprise_name, - :fee_name, - :customer_name, - :fee_placement, - :fee_calculated_on_transfer_through_name, - :tax_category_name, - :total_amount - ] - end - - def header_label(attribute) - I18n.t("header.#{attribute}", scope: i18n_scope) - end - - def i18n_scope - "order_management.reports.enterprise_fee_summary.formats.csv" - end - end - end - end - end -end diff --git a/engines/order_management/app/services/order_management/reports/enterprise_fee_summary/report_service.rb b/engines/order_management/app/services/order_management/reports/enterprise_fee_summary/report_service.rb deleted file mode 100644 index 43525fee87..0000000000 --- a/engines/order_management/app/services/order_management/reports/enterprise_fee_summary/report_service.rb +++ /dev/null @@ -1,49 +0,0 @@ -# frozen_string_literal: true - -module OrderManagement - module Reports - module EnterpriseFeeSummary - class ReportService - attr_accessor :permissions, :parameters - - def initialize(permissions, parameters) - @permissions = permissions - @parameters = parameters - end - - def enterprise_fees_by_customer - Scope.new.apply_filters(permission_filters).apply_filters(parameters).result - end - - def list - enterprise_fee_type_total_list.sort - end - - private - - def permission_filters - Parameters.new(order_cycle_ids: permissions.allowed_order_cycles.map(&:id)) - end - - def enterprise_fee_type_total_list - enterprise_fees_by_customer.map do |total_data| - summarizer = Summarizer.new(total_data) - - ReportData::EnterpriseFeeTypeTotal.new.tap do |total| - enterprise_fee_type_summarizer_to_total_attributes.each do |attribute| - total.public_send("#{attribute}=", summarizer.public_send(attribute)) - end - end - end - end - - def enterprise_fee_type_summarizer_to_total_attributes - [ - :fee_type, :enterprise_name, :fee_name, :customer_name, :fee_placement, - :fee_calculated_on_transfer_through_name, :tax_category_name, :total_amount - ] - end - end - end - end -end diff --git a/engines/order_management/app/services/reports.rb b/engines/order_management/app/services/reports.rb deleted file mode 100644 index 5a25947ad7..0000000000 --- a/engines/order_management/app/services/reports.rb +++ /dev/null @@ -1,5 +0,0 @@ -# frozen_string_literal: true - -module Reports - class UnsupportedReportFormatException < StandardError; 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/permissions.rb b/engines/order_management/app/services/reports/permissions.rb deleted file mode 100644 index ebdfeacf24..0000000000 --- a/engines/order_management/app/services/reports/permissions.rb +++ /dev/null @@ -1,11 +0,0 @@ -# frozen_string_literal: true - -module Reports - class Permissions - attr_accessor :user - - def initialize(user) - @user = user - end - end -end diff --git a/engines/order_management/app/services/reports/renderers/base.rb b/engines/order_management/app/services/reports/renderers/base.rb deleted file mode 100644 index 3fb3a55f12..0000000000 --- a/engines/order_management/app/services/reports/renderers/base.rb +++ /dev/null @@ -1,13 +0,0 @@ -# frozen_string_literal: true - -module Reports - module Renderers - class Base - attr_reader :report_data - - def initialize(report_data) - @report_data = report_data - 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/app/views/order_management/reports/_report.html.haml b/engines/order_management/app/views/order_management/reports/_report.html.haml deleted file mode 100644 index 332253b143..0000000000 --- a/engines/order_management/app/views/order_management/reports/_report.html.haml +++ /dev/null @@ -1,19 +0,0 @@ -- if @report.present? - %table#enterprise_fee_summary_report.report__table - %thead - %tr - - @renderer.header.each do |heading| - %th= heading - - %tbody - - @renderer.data_rows.each do |row| - %tr - - row.each do |cell_value| - %td= cell_value - - - if @renderer.data_rows.empty? - %tr - %td{colspan: @renderer.header.length}= t('.none') -- else - %p.report__message - = t(".select_and_search") diff --git a/engines/order_management/app/views/order_management/reports/bulk_coop/_filters.html.haml b/engines/order_management/app/views/order_management/reports/bulk_coop/_filters.html.haml deleted file mode 100644 index 4d10fafb3b..0000000000 --- a/engines/order_management/app/views/order_management/reports/bulk_coop/_filters.html.haml +++ /dev/null @@ -1,34 +0,0 @@ -= form_for @report_parameters, as: :report, url: main_app.order_management_reports_bulk_coop_path, method: :post do |f| - .row.date-range-filter - .sixteen.columns.alpha - = label_tag nil, t(".date_range") - %br - - = f.label :start_at, class: "inline" - = f.text_field :start_at, class: "datetimepicker datepicker-from" - - %span.range-divider - %i.icon-arrow-right - - = f.text_field :end_at, class: "datetimepicker datepicker-to" - = f.label :end_at, class: "inline" - - .row - .sixteen.columns.alpha - = f.label :distributor_ids - = f.collection_select(:distributor_ids, @permissions.allowed_distributors, :id, :name, {}, {class: "select2 fullwidth", multiple: true}) - - .row - .sixteen.columns.alpha - = f.label :report_type - = f.collection_select(:report_type, OrderManagement::Reports::BulkCoop::BulkCoopReport::REPORT_TYPES.map { |report_type| [t(".#{report_type}"), report_type] }, :last, :first, {}, {class: "select2 fullwidth", multiple: false}) - - .row - .sixteen.columns.alpha - = check_box_tag :report_format, "csv", false, id: "report_format_csv" - = label_tag :report_format_csv, t(".report_format_csv") - - = button t(".generate_report") - - = render partial: "spree/admin/reports/customer_names_message" - diff --git a/engines/order_management/app/views/order_management/reports/bulk_coop/_report.html.haml b/engines/order_management/app/views/order_management/reports/bulk_coop/_report.html.haml deleted file mode 100644 index c4e2e5fc76..0000000000 --- a/engines/order_management/app/views/order_management/reports/bulk_coop/_report.html.haml +++ /dev/null @@ -1,20 +0,0 @@ -- if @report.present? - %table#bulk_coop_report.report__table - %thead - %tr - - @renderer.header.each do |heading| - %th= heading - - %tbody - - @renderer.data_rows.each do |row| - %tr - - row.each do |cell_value| - %td= cell_value - - - if @renderer.data_rows.empty? - %tr - %td{colspan: @renderer.header.length}= t('.none') -- else - %p.report__message - = t(".select_and_search") - diff --git a/engines/order_management/app/views/order_management/reports/bulk_coop/create.html.haml b/engines/order_management/app/views/order_management/reports/bulk_coop/create.html.haml deleted file mode 100644 index 06938dd67d..0000000000 --- a/engines/order_management/app/views/order_management/reports/bulk_coop/create.html.haml +++ /dev/null @@ -1,2 +0,0 @@ -= render "filters" -= render "order_management/reports/report" diff --git a/engines/order_management/app/views/order_management/reports/bulk_coop/new.html.haml b/engines/order_management/app/views/order_management/reports/bulk_coop/new.html.haml deleted file mode 100644 index 790853ca1f..0000000000 --- a/engines/order_management/app/views/order_management/reports/bulk_coop/new.html.haml +++ /dev/null @@ -1 +0,0 @@ -= render "filters" diff --git a/engines/order_management/app/views/order_management/reports/enterprise_fee_summaries/_filters.html.haml b/engines/order_management/app/views/order_management/reports/enterprise_fee_summaries/_filters.html.haml deleted file mode 100644 index f63882d7de..0000000000 --- a/engines/order_management/app/views/order_management/reports/enterprise_fee_summaries/_filters.html.haml +++ /dev/null @@ -1,52 +0,0 @@ -= form_for @report_parameters, as: :report, url: main_app.order_management_reports_enterprise_fee_summary_path, method: :post do |f| - .row.date-range-filter - .sixteen.columns.alpha - = label_tag nil, t(".date_range") - %br - - = f.label :start_at, class: "inline" - = f.text_field :start_at, class: "datetimepicker datepicker-from" - - %span.range-divider - %i.icon-arrow-right - - = f.text_field :end_at, class: "datetimepicker datepicker-to" - = f.label :end_at, class: "inline" - - .row - .sixteen.columns.alpha - = f.label :distributor_ids - = f.collection_select(:distributor_ids, @permissions.allowed_distributors, :id, :name, {}, {class: "select2 fullwidth", multiple: true}) - - .row - .sixteen.columns.alpha - = f.label :producer_ids - = f.collection_select(:producer_ids, @permissions.allowed_producers, :id, :name, {}, {class: "select2 fullwidth", multiple: true}) - - .row - .sixteen.columns.alpha - = f.label :order_cycle_ids - = f.collection_select(:order_cycle_ids, @permissions.allowed_order_cycles, :id, :name, {}, {class: "select2 fullwidth", multiple: true}) - - .row - .eight.columns.alpha - = f.label :enterprise_fee_ids - = f.collection_select(:enterprise_fee_ids, @permissions.allowed_enterprise_fees, :id, :name, {}, {class: "select2 fullwidth", multiple: true}) - .eight.columns.omega - = f.label :shipping_method_ids - = f.collection_select(:shipping_method_ids, @permissions.allowed_shipping_methods, :id, :name, {}, {class: "select2 fullwidth", multiple: true}) - - .row - .eight.columns.alpha   - .eight.columns.omega - = f.label :payment_method_ids - = f.collection_select(:payment_method_ids, @permissions.allowed_payment_methods, :id, :name, {}, {class: "select2 fullwidth", multiple: true}) - - .row - .sixteen.columns.alpha - = check_box_tag :report_format, "csv", false, id: "report_format_csv" - = label_tag :report_format_csv, t(".report_format_csv") - - = button t(".generate_report") - -= render partial: "spree/admin/reports/customer_names_message" diff --git a/engines/order_management/app/views/order_management/reports/enterprise_fee_summaries/create.html.haml b/engines/order_management/app/views/order_management/reports/enterprise_fee_summaries/create.html.haml deleted file mode 100644 index 06938dd67d..0000000000 --- a/engines/order_management/app/views/order_management/reports/enterprise_fee_summaries/create.html.haml +++ /dev/null @@ -1,2 +0,0 @@ -= render "filters" -= render "order_management/reports/report" diff --git a/engines/order_management/app/views/order_management/reports/enterprise_fee_summaries/new.html.haml b/engines/order_management/app/views/order_management/reports/enterprise_fee_summaries/new.html.haml deleted file mode 100644 index 790853ca1f..0000000000 --- a/engines/order_management/app/views/order_management/reports/enterprise_fee_summaries/new.html.haml +++ /dev/null @@ -1 +0,0 @@ -= render "filters" diff --git a/engines/order_management/spec/controllers/order_management/reports/bulk_coop_controller_spec.rb b/engines/order_management/spec/controllers/order_management/reports/bulk_coop_controller_spec.rb deleted file mode 100644 index aa046a52f3..0000000000 --- a/engines/order_management/spec/controllers/order_management/reports/bulk_coop_controller_spec.rb +++ /dev/null @@ -1,103 +0,0 @@ -# frozen_string_literal: true - -require "spec_helper" - -describe OrderManagement::Reports::BulkCoopController, type: :controller do - let(:report_klass) { OrderManagement::Reports::BulkCoop } - - let!(:distributor) { create(:distributor_enterprise) } - - let(:current_user) { distributor.owner } - - before do - allow(controller).to receive(:spree_current_user) { current_user } - end - - describe "#new" do - it "renders the report form" do - get :new - - expect(response.status).to eq 200 - expect(response).to render_template(new_template_path) - end - end - - describe "#create" do - context "when the parameters are valid" do - it "sends the generated report in the correct format" do - post :create, params: { - report: { - start_at: "2018-10-09 07:30:00", - report_type: "bulk_coop_supplier_report" - }, report_format: "csv" - } - - expect(response.status).to eq 200 - expect(response.body).not_to be_blank - expect(response.header["Content-Type"]).to eq("text/csv") - end - end - - context "when the parameters are invalid" do - it "renders the report form with an error" do - post :create, params: { - report: { - start_at: "invalid_date", - report_type: "bulk_coop_supplier_report" - }, report_format: "csv" - } - - expect(flash[:error]).to eq(I18n.t("invalid_filter_parameters", scope: i18n_scope)) - expect(response).to render_template(new_template_path) - end - end - - context "when some parameters are now allowed" do - let!(:distributor) { create(:distributor_enterprise) } - let!(:other_distributor) { create(:distributor_enterprise) } - - let(:current_user) { distributor.owner } - - it "renders the report form with an error" do - post :create, params: { - report: { - distributor_ids: [other_distributor.id], - report_type: "bulk_coop_supplier_report" - }, report_format: "csv" - } - - expect(flash[:error]).to eq(report_klass::Authorizer.parameter_not_allowed_error_message) - expect(response).to render_template(new_template_path) - end - end - - describe "filtering results based on permissions" do - let!(:distributor) { create(:distributor_enterprise) } - let!(:other_distributor) { create(:distributor_enterprise) } - - let(:current_user) { distributor.owner } - - it "applies permissions to report" do - post :create, params: { report: {}, report_format: "csv" } - - expect(assigns(:permissions).allowed_distributors.to_a).to eq([distributor]) - end - end - end - - private - - def default_report_params - { - report_type: "bulk_coop_supplier_report" - } - end - - def i18n_scope - "order_management.reports.enterprise_fee_summary" - end - - def new_template_path - "order_management/reports/bulk_coop/new" - end -end diff --git a/engines/order_management/spec/controllers/order_management/reports/enterprise_fee_summaries_controller_spec.rb b/engines/order_management/spec/controllers/order_management/reports/enterprise_fee_summaries_controller_spec.rb deleted file mode 100644 index 62897da6cc..0000000000 --- a/engines/order_management/spec/controllers/order_management/reports/enterprise_fee_summaries_controller_spec.rb +++ /dev/null @@ -1,89 +0,0 @@ -# frozen_string_literal: true - -require "spec_helper" - -describe OrderManagement::Reports::EnterpriseFeeSummariesController, type: :controller do - let(:report_klass) { OrderManagement::Reports::EnterpriseFeeSummary } - - let!(:distributor) { create(:distributor_enterprise) } - - let(:current_user) { distributor.owner } - - before do - allow(controller).to receive(:spree_current_user) { current_user } - end - - describe "#new" do - it "renders the report form" do - get :new - - expect(response.status).to eq 200 - expect(response).to render_template(new_template_path) - end - end - - describe "#create" do - context "when the parameters are valid" do - it "sends the generated report in the correct format" do - post :create, params: { - report: { start_at: "2018-10-09 07:30:00" }, report_format: "csv" - } - - expect(response.status).to eq 200 - expect(response.body).not_to be_blank - expect(response.header["Content-Type"]).to eq("text/csv") - end - end - - context "when the parameters are invalid" do - it "renders the report form with an error" do - post :create, params: { - report: { start_at: "invalid date" }, report_format: "csv" - } - - expect(flash[:error]).to eq(I18n.t("invalid_filter_parameters", scope: i18n_scope)) - expect(response).to render_template(new_template_path) - end - end - - context "when some parameters are now allowed" do - let!(:distributor) { create(:distributor_enterprise) } - let!(:other_distributor) { create(:distributor_enterprise) } - - let(:current_user) { distributor.owner } - - it "renders the report form with an error" do - post :create, params: { - report: { distributor_ids: [other_distributor.id] }, report_format: "csv" - } - - expect(flash[:error]).to eq(report_klass::Authorizer.parameter_not_allowed_error_message) - expect(response).to render_template(new_template_path) - end - end - - describe "filtering results based on permissions" do - let!(:distributor) { create(:distributor_enterprise) } - let!(:other_distributor) { create(:distributor_enterprise) } - - let!(:order_cycle) { create(:simple_order_cycle, coordinator: distributor) } - let!(:other_order_cycle) { create(:simple_order_cycle, coordinator: other_distributor) } - - let(:current_user) { distributor.owner } - - it "applies permissions to report" do - post :create, params: { report: {}, report_format: "csv" } - - expect(assigns(:permissions).allowed_order_cycles.to_a).to eq([order_cycle]) - end - end - end - - def i18n_scope - "order_management.reports.enterprise_fee_summary" - end - - def new_template_path - "order_management/reports/enterprise_fee_summaries/new" - end -end diff --git a/engines/order_management/spec/features/order_management/reports/bulk_coop_spec.rb b/engines/order_management/spec/features/order_management/reports/bulk_coop_spec.rb deleted file mode 100644 index 781107795c..0000000000 --- a/engines/order_management/spec/features/order_management/reports/bulk_coop_spec.rb +++ /dev/null @@ -1,75 +0,0 @@ -# frozen_string_literal: true - -require "spec_helper" - -feature "bulk coop" do - include AuthenticationHelper - include WebHelper - - scenario "generating Bulk Co-op Supplier Report" do - login_as_admin_and_visit new_order_management_reports_bulk_coop_path - select "Bulk Co-op Supplier Report", from: "report_report_type" - click_button 'Generate Report' - - expect(page).to have_table_row [ - "Supplier", - "Product", - "Bulk Unit Size", - "Variant", - "Variant Value", - "Variant Unit", - "Weight", - "Sum Total", - "Units Required", - "Unallocated", - "Max Quantity Excess" - ] - end - - scenario "generating Bulk Co-op Allocation report" do - login_as_admin_and_visit new_order_management_reports_bulk_coop_path - select "Bulk Co-op Allocation", from: "report_report_type" - click_button 'Generate Report' - - expect(page).to have_table_row [ - "Customer", - "Product", - "Bulk Unit Size", - "Variant", - "Variant Value", - "Variant Unit", - "Weight", - "Sum Total", - "Total available", - "Unallocated", - "Max Quantity Excess" - ] - end - - scenario "generating Bulk Co-op Packing Sheets report" do - login_as_admin_and_visit new_order_management_reports_bulk_coop_path - select "Bulk Co-op Packing Sheets", from: "report_report_type" - click_button 'Generate Report' - - expect(page).to have_table_row [ - "Customer", - "Product", - "Variant", - "Sum Total" - ] - end - - scenario "generating Bulk Co-op Customer Payments report" do - login_as_admin_and_visit new_order_management_reports_bulk_coop_path - select "Bulk Co-op Customer Payments", from: "report_report_type" - click_button 'Generate Report' - - expect(page).to have_table_row [ - "Customer", - "Date of Order", - "Total Cost", - "Amount Owing", - "Amount Paid" - ] - end -end diff --git a/engines/order_management/spec/services/order_management/reports/bulk_coop/bulk_coop_report_spec.rb b/engines/order_management/spec/services/order_management/reports/bulk_coop/bulk_coop_report_spec.rb deleted file mode 100644 index 5084671813..0000000000 --- a/engines/order_management/spec/services/order_management/reports/bulk_coop/bulk_coop_report_spec.rb +++ /dev/null @@ -1,186 +0,0 @@ -# frozen_string_literal: true - -require 'spec_helper' - -describe OrderManagement::Reports::BulkCoop::BulkCoopReport do - subject { OrderManagement::Reports::BulkCoop::BulkCoopReport.new user, params, true } - let(:user) { create(:admin_user) } - - describe '#table_items' do - let(:params) { {} } - - let(:d1) { create(:distributor_enterprise) } - let(:oc1) { create(:simple_order_cycle) } - let(:o1) { create(:order, completed_at: 1.day.ago, order_cycle: oc1, distributor: d1) } - let(:li1) { build(:line_item_with_shipment) } - - before { o1.line_items << li1 } - - context "as a site admin" do - context 'when searching' do - let(:params) { { q: { completed_at_gt: '', completed_at_lt: '', distributor_id_in: [] } } } - - it "fetches completed orders" do - o2 = create(:order, state: 'cart') - o2.line_items << build(:line_item) - expect(subject.table_items).to eq([li1]) - end - - it 'shows canceled orders' do - o2 = create(:order, state: 'canceled', completed_at: 1.day.ago, order_cycle: oc1, - distributor: d1) - line_item = build(:line_item_with_shipment) - o2.line_items << line_item - expect(subject.table_items).to include(line_item) - end - end - - context 'when not searching' do - let(:params) { {} } - - it "fetches completed orders" do - o2 = create(:order, state: 'cart') - o2.line_items << build(:line_item) - expect(subject.table_items).to eq([li1]) - end - - it 'shows canceled orders' do - o2 = create(:order, state: 'canceled', completed_at: 1.day.ago, order_cycle: oc1, - distributor: d1) - line_item = build(:line_item_with_shipment) - o2.line_items << line_item - expect(subject.table_items).to include(line_item) - end - end - end - - context "filtering by date" do - it do - user = create(:admin_user) - o2 = create(:order, completed_at: 3.days.ago, order_cycle: oc1, distributor: d1) - li2 = build(:line_item_with_shipment) - o2.line_items << li2 - - report = OrderManagement::Reports::BulkCoop::BulkCoopReport.new user, {}, true - expect(report.table_items).to match_array [li1, li2] - - report = OrderManagement::Reports::BulkCoop::BulkCoopReport.new( - user, { q: { completed_at_gt: 2.days.ago } }, true - ) - expect(report.table_items).to eq([li1]) - - report = OrderManagement::Reports::BulkCoop::BulkCoopReport.new( - user, { q: { completed_at_lt: 2.days.ago } }, true - ) - expect(report.table_items).to eq([li2]) - end - end - - context "filtering by distributor" do - it do - user = create(:admin_user) - d2 = create(:distributor_enterprise) - o2 = create(:order, distributor: d2, order_cycle: oc1, - completed_at: Time.zone.now) - li2 = build(:line_item_with_shipment) - o2.line_items << li2 - - report = OrderManagement::Reports::BulkCoop::BulkCoopReport.new user, {}, true - expect(report.table_items).to match_array [li1, li2] - - report = OrderManagement::Reports::BulkCoop::BulkCoopReport.new( - user, { q: { distributor_id_in: [d1.id] } }, true - ) - expect(report.table_items).to eq([li1]) - - report = OrderManagement::Reports::BulkCoop::BulkCoopReport.new( - user, { q: { distributor_id_in: [d2.id] } }, true - ) - expect(report.table_items).to eq([li2]) - end - end - - context "as a manager of a supplier" do - let!(:user) { create(:user) } - subject { OrderManagement::Reports::BulkCoop::BulkCoopReport.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) do - create(:order, distributor: d1, completed_at: 1.day.ago, bill_address: create(:address), - ship_address: create(:address)) - end - let(:li2) do - build(:line_item_with_shipment, product: create(:simple_product, supplier: s1)) - end - - before do - o2.line_items << li2 - create(:enterprise_relationship, parent: s1, child: d1, - 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 - end - - context "that has not granted P-OC to the distributor" do - let(:o2) do - create(:order, distributor: d1, completed_at: 1.day.ago, bill_address: create(:address), - ship_address: create(:address)) - end - let(:li2) do - build(:line_item_with_shipment, product: create(:simple_product, supplier: s1)) - end - - 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 - end - end - end - - describe '#columns' do - context 'when report type is bulk_coop_customer_payments' do - let(:params) { { report_type: 'bulk_coop_customer_payments' } } - - it 'returns' do - expect(subject.columns).to eq( - [ - :order_billing_address_name, - :order_completed_at, - :customer_payments_total_cost, - :customer_payments_amount_owed, - :customer_payments_amount_paid, - ] - ) - end - end - end - - # Yes, I know testing a private method is bad practice but report's design, tighly coupling - # OpenFoodNetwork::OrderGrouper and OrderManagement::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) { {} } - let(:user) { build(:user) } - let!(:line_item) { create(:line_item) } - let(:order) { line_item.order } - - it 'calls #new_outstanding_balance' do - expect_any_instance_of(Spree::Order).to receive(:new_outstanding_balance) - subject.send(:customer_payments_amount_owed, [line_item]) - 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/engines/order_management/spec/services/order_management/reports/enterprise_fee_summary/renderers/csv_renderer_spec.rb b/engines/order_management/spec/services/order_management/reports/enterprise_fee_summary/renderers/csv_renderer_spec.rb deleted file mode 100644 index bc255ab8b9..0000000000 --- a/engines/order_management/spec/services/order_management/reports/enterprise_fee_summary/renderers/csv_renderer_spec.rb +++ /dev/null @@ -1,95 +0,0 @@ -# frozen_string_literal: true - -require "spec_helper" - -describe OrderManagement::Reports::EnterpriseFeeSummary::Renderers::CsvRenderer do - let(:report_klass) { OrderManagement::Reports::EnterpriseFeeSummary } - - let!(:permissions) { report_klass::Permissions.new(current_user) } - let!(:parameters) { report_klass::Parameters.new } - let!(:service) { report_klass::ReportService.new(permissions, parameters) } - let!(:renderer) { described_class.new(service) } - - # Context which will be passed to the renderer. The response object is not automatically prepared, - # so this has to be assigned explicitly. - let!(:response) { ActionDispatch::TestResponse.new } - let!(:request) { double(Rack::Request) } - let!(:controller) do - ActionController::Base.new.tap do |controller_mock| - controller_mock.instance_variable_set(:@_response, response) - controller_mock.instance_variable_set(:@_request, request) - end - end - - let!(:enterprise_fee_type_totals) do - [ - report_klass::ReportData::EnterpriseFeeTypeTotal.new( - fee_type: "Fee Type A", - enterprise_name: "Enterprise A", - fee_name: "Fee A", - customer_name: "Custoemr A", - fee_placement: "Fee Placement A", - fee_calculated_on_transfer_through_name: "Transfer Enterprise A", - tax_category_name: "Tax Category A", - total_amount: "1.00" - ), - report_klass::ReportData::EnterpriseFeeTypeTotal.new( - fee_type: "Fee Type B", - enterprise_name: "Enterprise B", - fee_name: "Fee C", - customer_name: "Custoemr D", - fee_placement: "Fee Placement E", - fee_calculated_on_transfer_through_name: "Transfer Enterprise F", - tax_category_name: "Tax Category G", - total_amount: "2.00" - ) - ] - end - - let(:current_user) { nil } - - before do - allow(service).to receive(:list) { enterprise_fee_type_totals } - allow(request).to receive_messages(variant: double(Spree::Variant), - should_apply_vary_header?: true) - end - - it "generates CSV header" do - renderer.render(controller) - result = response.body - csv = CSV.parse(result) - header_row = csv[0] - - # Test all header cells have values - expect(header_row.length).to eq(8) - expect(header_row.all?(&:present?)).to be_truthy - end - - it "generates CSV data rows" do - renderer.render(controller) - result = response.body - csv = CSV.parse(result, headers: true) - - expect(csv.length).to eq(2) - - # Test random cells - expect(csv[0][i18n_translate("header.fee_type")]).to eq("Fee Type A") - expect(csv[0][i18n_translate("header.total_amount")]).to eq("1.00") - expect(csv[1][i18n_translate("header.total_amount")]).to eq("2.00") - end - - it "generates filename correctly" do - Timecop.freeze(Time.zone.local(2018, 10, 9, 7, 30, 0)) do - filename = renderer.__send__(:filename) - expect(filename).to eq("enterprise_fee_summary_20181009.csv") - end - end - - def i18n_translate(key) - I18n.t(key, scope: i18n_scope) - end - - def i18n_scope - "order_management.reports.enterprise_fee_summary.formats.csv" - end -end diff --git a/engines/order_management/spec/services/order_management/reports/enterprise_fee_summary/renderers/html_renderer_spec.rb b/engines/order_management/spec/services/order_management/reports/enterprise_fee_summary/renderers/html_renderer_spec.rb deleted file mode 100644 index 957adf9226..0000000000 --- a/engines/order_management/spec/services/order_management/reports/enterprise_fee_summary/renderers/html_renderer_spec.rb +++ /dev/null @@ -1,72 +0,0 @@ -# frozen_string_literal: true - -require "spec_helper" - -describe OrderManagement::Reports::EnterpriseFeeSummary::Renderers::HtmlRenderer do - let(:report_klass) { OrderManagement::Reports::EnterpriseFeeSummary } - - let!(:permissions) { report_klass::Permissions.new(current_user) } - let!(:parameters) { report_klass::Parameters.new } - let!(:controller) { OrderManagement::Reports::EnterpriseFeeSummariesController.new } - let!(:service) { report_klass::ReportService.new(permissions, parameters) } - let!(:renderer) { described_class.new(service) } - - let!(:enterprise_fee_type_totals) do - [ - report_klass::ReportData::EnterpriseFeeTypeTotal.new( - fee_type: "Fee Type A", - enterprise_name: "Enterprise A", - fee_name: "Fee A", - customer_name: "Custoemr A", - fee_placement: "Fee Placement A", - fee_calculated_on_transfer_through_name: "Transfer Enterprise A", - tax_category_name: "Tax Category A", - total_amount: "1.00" - ), - report_klass::ReportData::EnterpriseFeeTypeTotal.new( - fee_type: "Fee Type B", - enterprise_name: "Enterprise B", - fee_name: "Fee C", - customer_name: "Custoemr D", - fee_placement: "Fee Placement E", - fee_calculated_on_transfer_through_name: "Transfer Enterprise F", - tax_category_name: "Tax Category G", - total_amount: "2.00" - ) - ] - end - - let(:current_user) { nil } - - before do - allow(service).to receive(:list) { enterprise_fee_type_totals } - end - - it "generates header values" do - header_row = renderer.header - - # Test all header cells have values - expect(header_row.length).to eq(8) - expect(header_row.all?(&:present?)).to be_truthy - end - - it "generates data rows" do - header_row = renderer.header - result = renderer.data_rows - - expect(result.length).to eq(2) - - # Test random cells - expect(result[0][header_row.index(i18n_translate("header.fee_type"))]).to eq("Fee Type A") - expect(result[0][header_row.index(i18n_translate("header.total_amount"))]).to eq("1.00") - expect(result[1][header_row.index(i18n_translate("header.total_amount"))]).to eq("2.00") - end - - def i18n_translate(key) - I18n.t(key, scope: i18n_scope) - end - - def i18n_scope - "order_management.reports.enterprise_fee_summary.formats.csv" - 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 60a7863dea..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 header - 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 - 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_type] == "mailing_list" - end - end -end diff --git a/lib/open_food_network/group_buy_report.rb b/lib/open_food_network/group_buy_report.rb deleted file mode 100644 index c6ab52a6df..0000000000 --- a/lib/open_food_network/group_buy_report.rb +++ /dev/null @@ -1,71 +0,0 @@ -# frozen_string_literal: true - -module OpenFoodNetwork - GroupBuyVariantRow = Struct.new(:variant, :sum_quantities, :sum_max_quantities) do - def to_row - [variant.product.supplier.name, variant.product.name, I18n.t('admin.reports.unitsize'), - variant.options_text, variant.weight, sum_quantities, sum_max_quantities] - end - end - - GroupBuyProductRow = Struct.new(:product, :sum_quantities, :sum_max_quantities) do - def to_row - [product.supplier.name, product.name, I18n.t('admin.reports.unitsize'), - I18n.t('admin.reports.total'), "", sum_quantities, sum_max_quantities] - end - end - - class GroupBuyReport - def initialize(orders) - @orders = orders - end - - def header - [ - I18n.t(:report_header_supplier), - I18n.t(:report_header_product), - I18n.t(:report_header_unit_size), - I18n.t(:report_header_variant), - I18n.t(:report_header_weight), - I18n.t(:report_header_total_ordered), - I18n.t(:report_header_total_max), - ] - end - - def variants_and_quantities - variants_and_quantities = [] - line_items = @orders.map(&:line_items).flatten - supplier_groups = line_items.group_by { |li| li.variant.product.supplier } - supplier_groups.each do |_supplier, line_items_by_supplier| - product_groups = line_items_by_supplier.group_by { |li| li.variant.product } - product_groups.each do |product, line_items_by_product| - # Cycle thorugh variant of a product - variant_groups = line_items_by_product.group_by(&:variant) - variant_groups.each do |variant, line_items_by_variant| - sum_quantities = line_items_by_variant.to_a.sum(&:quantity) - sum_max_quantities = line_items_by_variant.sum { |li| li.max_quantity || 0 } - variants_and_quantities << GroupBuyVariantRow.new(variant, sum_quantities, - sum_max_quantities) - end - - # Sum quantities for each product (Total line) - sum_quantities = line_items_by_product.sum { |li| (li.variant.weight || 0) * li.quantity } - sum_max_quantities = line_items_by_product.sum { |li| - (li.variant.weight || 0) * (li.max_quantity || 0) - } - variants_and_quantities << GroupBuyProductRow.new(product, sum_quantities, - sum_max_quantities) - end - end - variants_and_quantities - end - - def table - table = [] - variants_and_quantities.each do |vr| - table << vr.to_row - end - table - 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 25868c9006..0000000000 --- a/lib/open_food_network/lettuce_share_report.rb +++ /dev/null @@ -1,82 +0,0 @@ -# frozen_string_literal: true - -require 'open_food_network/products_and_inventory_report_base' -require 'variant_units/option_value_namer' - -module OpenFoodNetwork - class LettuceShareReport < ProductsAndInventoryReportBase - def header - # 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 - 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_and_distributor_report.rb b/lib/open_food_network/order_and_distributor_report.rb deleted file mode 100644 index 9764184b02..0000000000 --- a/lib/open_food_network/order_and_distributor_report.rb +++ /dev/null @@ -1,112 +0,0 @@ -# frozen_string_literal: true - -module OpenFoodNetwork - class OrderAndDistributorReport - def initialize(user, params = {}, render_table = false) - @params = params - @user = user - @render_table = render_table - - @permissions = ::Permissions::Order.new(user, @params[:q]) - end - - def header - [ - 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 - 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/order_cycle_management_report.rb b/lib/open_food_network/order_cycle_management_report.rb deleted file mode 100644 index d011b66467..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 header - 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_items - 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_type] == "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..4635322482 100644 --- a/lib/open_food_network/order_cycle_permissions.rb +++ b/lib/open_food_network/order_cycle_permissions.rb @@ -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 @@ -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) @@ -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/order_grouper.rb b/lib/open_food_network/order_grouper.rb deleted file mode 100644 index cc4720bab9..0000000000 --- a/lib/open_food_network/order_grouper.rb +++ /dev/null @@ -1,88 +0,0 @@ -# frozen_string_literal: true - -module OpenFoodNetwork - class OrderGrouper - def initialize(rules, column_constructors, report = nil) - @rules = rules - @column_constructors = column_constructors - @report = report - end - - def build_tree(items, remaining_rules) - rules = remaining_rules.clone - if rules.any? - rule = rules.delete_at(0) # Remove current rule for subsequent groupings - group_and_sort(rule, rules, items) - else - items - end - end - - def group_and_sort(rule, remaining_rules, items) - branch = {} - groups = items.group_by { |item| rule[:group_by].call(item) } - - sorted_groups = groups.sort_by { |key, _value| rule[:sort_by].call(key) } - - sorted_groups.each do |property, items_by_property| - branch[property] = build_tree(items_by_property, remaining_rules) - - next if rule[:summary_columns].nil? || is_leaf_node(branch[property]) - - branch[property][:summary_row] = { - items: items_by_property, - columns: rule[:summary_columns] - } - end - - branch - end - - def build_table(groups) - rows = [] - if is_leaf_node(groups) - rows << build_row(groups) - else - groups.each do |key, group| - if key == :summary_row - rows << build_summary_row(group[:columns], group[:items]) - else - build_table(group).each { |g| rows << g } - end - end - end - rows - end - - def table(items) - tree = build_tree(items, @rules) - build_table(tree) - end - - private - - def build_cell(column_constructor, items) - if column_constructor.is_a?(Symbol) - @report.__send__(column_constructor, items) - else - column_constructor.call(items) - end - end - - def build_row(groups) - @column_constructors.map do |column_constructor| - build_cell(column_constructor, groups) - end - end - - def build_summary_row(summary_row_column_constructors, items) - summary_row_column_constructors.map do |summary_row_column_constructor| - build_cell(summary_row_column_constructor, items) - end - end - - def is_leaf_node(node) - node.is_a? Array - end - end -end diff --git a/lib/open_food_network/orders_and_fulfillments_report.rb b/lib/open_food_network/orders_and_fulfillments_report.rb deleted file mode 100644 index d6bd7487d6..0000000000 --- a/lib/open_food_network/orders_and_fulfillments_report.rb +++ /dev/null @@ -1,114 +0,0 @@ -# frozen_string_literal: true - -require "open_food_network/reports/line_items" -require "open_food_network/orders_and_fulfillments_report/supplier_totals_report" -require "open_food_network/orders_and_fulfillments_report/supplier_totals_by_distributor_report" -require "open_food_network/orders_and_fulfillments_report/distributor_totals_by_supplier_report" -require "open_food_network/orders_and_fulfillments_report/customer_totals_report" -require 'open_food_network/orders_and_fulfillments_report/default_report' - -include Spree::ReportsHelper - -module OpenFoodNetwork - class OrdersAndFulfillmentsReport - attr_reader :options, :report_type - - delegate :header, :rules, :columns, to: :report - - def initialize(user, options = {}, render_table = false) - @user = user - @options = options - @report_type = options[:report_type] - @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 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_fulfillments_report/customer_totals_report.rb b/lib/open_food_network/orders_and_fulfillments_report/customer_totals_report.rb deleted file mode 100644 index f65ad3a16f..0000000000 --- a/lib/open_food_network/orders_and_fulfillments_report/customer_totals_report.rb +++ /dev/null @@ -1,221 +0,0 @@ -# frozen_string_literal: true - -# rubocop:disable Metrics/ClassLength -module OpenFoodNetwork - class OrdersAndFulfillmentsReport - 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 header - [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_fulfillments_report/default_report.rb b/lib/open_food_network/orders_and_fulfillments_report/default_report.rb deleted file mode 100644 index 3d10c9d7c4..0000000000 --- a/lib/open_food_network/orders_and_fulfillments_report/default_report.rb +++ /dev/null @@ -1,64 +0,0 @@ -# frozen_string_literal: true - -module OpenFoodNetwork - class OrdersAndFulfillmentsReport - class DefaultReport - delegate :line_item_name, :supplier_name, :product_name, :line_items_name, to: :context - - def initialize(context) - @context = context - end - - def header - [ - 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_fulfillments_report/distributor_totals_by_supplier_report.rb b/lib/open_food_network/orders_and_fulfillments_report/distributor_totals_by_supplier_report.rb deleted file mode 100644 index 5ce88bdab6..0000000000 --- a/lib/open_food_network/orders_and_fulfillments_report/distributor_totals_by_supplier_report.rb +++ /dev/null @@ -1,78 +0,0 @@ -# frozen_string_literal: true - -module OpenFoodNetwork - class OrdersAndFulfillmentsReport - class DistributorTotalsBySupplierReport - REPORT_TYPE = "order_cycle_distributor_totals_by_supplier" - - attr_reader :context - - def initialize(context) - @context = context - end - - def header - [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_fulfillments_report/supplier_totals_by_distributor_report.rb b/lib/open_food_network/orders_and_fulfillments_report/supplier_totals_by_distributor_report.rb deleted file mode 100644 index 404fd2eeb3..0000000000 --- a/lib/open_food_network/orders_and_fulfillments_report/supplier_totals_by_distributor_report.rb +++ /dev/null @@ -1,77 +0,0 @@ -# frozen_string_literal: true - -module OpenFoodNetwork - class OrdersAndFulfillmentsReport - 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 header - [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_fulfillments_report/supplier_totals_report.rb b/lib/open_food_network/orders_and_fulfillments_report/supplier_totals_report.rb deleted file mode 100644 index 9fd5f7fcf9..0000000000 --- a/lib/open_food_network/orders_and_fulfillments_report/supplier_totals_report.rb +++ /dev/null @@ -1,60 +0,0 @@ -# frozen_string_literal: true - -module OpenFoodNetwork - class OrdersAndFulfillmentsReport - 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 header - [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 efbecec205..0000000000 --- a/lib/open_food_network/payments_report.rb +++ /dev/null @@ -1,137 +0,0 @@ -# frozen_string_literal: true - -module OpenFoodNetwork - class PaymentsReport - attr_reader :params - - def initialize(user, params = {}, render_table = false) - @params = params - @user = user - @render_table = render_table - end - - def header - case params[:report_type] - 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_type] - when "payments_by_payment_type" - payments - when "itemised_payment_totals" - orders - when "payment_totals" - orders - else - payments - end - end - - def rules - case params[:report_type] - 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_type] - 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_report.rb b/lib/open_food_network/products_and_inventory_report.rb deleted file mode 100644 index df40f3aa4c..0000000000 --- a/lib/open_food_network/products_and_inventory_report.rb +++ /dev/null @@ -1,47 +0,0 @@ -# frozen_string_literal: true - -require 'open_food_network/products_and_inventory_report_base' - -module OpenFoodNetwork - class ProductsAndInventoryReport < ProductsAndInventoryReportBase - def header - [ - 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 - 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_base.rb b/lib/open_food_network/products_and_inventory_report_base.rb deleted file mode 100644 index 22cd561f36..0000000000 --- a/lib/open_food_network/products_and_inventory_report_base.rb +++ /dev/null @@ -1,80 +0,0 @@ -# frozen_string_literal: true - -require 'open_food_network/scope_variant_to_hub' - -module OpenFoodNetwork - class ProductsAndInventoryReportBase - attr_reader :params - - def initialize(user, params = {}, render_table = false) - @user = user - @params = params - @render_table = render_table - 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 params[: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 3284d5b164..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 header - case params[:report_type] - 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 - return [] unless @render_table - - case params[:report_type] - 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 fc13095534..0000000000 --- a/lib/open_food_network/users_and_enterprises_report.rb +++ /dev/null @@ -1,137 +0,0 @@ -# frozen_string_literal: true - -module OpenFoodNetwork - class UsersAndEnterprisesReport - attr_reader :params - - def initialize(params = {}, compile_table = false) - @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 header - [ - 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 - 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 a874bd6047..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_type: 'summary', - invoice_date: Time.zone.today, - due_date: Time.zone.today + 1.month, - account_code: 'food sales' ) - @compile_table = compile_table - end - - def header - # 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 - 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_type] == '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/frontend_data.rb b/lib/reporting/frontend_data.rb index aad748b1ff..a702a91de0 100644 --- a/lib/reporting/frontend_data.rb +++ b/lib/reporting/frontend_data.rb @@ -6,18 +6,39 @@ module Reporting @current_user = current_user end + # Load managed distributor enterprises of current user def distributors - permissions.visible_enterprises_for_order_reports.is_distributor. - select("enterprises.id, enterprises.name") + @distributors ||= Enterprise.is_distributor.managed_by(current_user) end def suppliers - permissions.visible_enterprises_for_order_reports.is_primary_producer. - select("enterprises.id, enterprises.name") + @suppliers ||= my_suppliers | suppliers_of_products_distributed_by(distributors) + end + + # Load managed producer enterprises of current user + def my_suppliers + Enterprise.is_primary_producer.managed_by(current_user) + end + + def suppliers_of_products_distributed_by(distributors) + supplier_ids = Spree::Product.in_distributors(distributors.select('enterprises.id')). + select('spree_products.supplier_id') + + Enterprise.where(id: supplier_ids) + end + + def orders_distributors + @orders_distributors ||= permissions.visible_enterprises_for_order_reports + .is_distributor.select("enterprises.id, enterprises.name") + end + + def orders_suppliers + @orders_suppliers ||= permissions.visible_enterprises_for_order_reports + .is_primary_producer.select("enterprises.id, enterprises.name") end def order_cycles - OrderCycle. + @order_cycles ||= OrderCycle. active_or_complete. visible_by(current_user). order('order_cycles.orders_close_at DESC') diff --git a/lib/reporting/line_items.rb b/lib/reporting/line_items.rb new file mode 100644 index 0000000000..2ffd375bc7 --- /dev/null +++ b/lib/reporting/line_items.rb @@ -0,0 +1,58 @@ +# 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 = [variant: [product: :supplier]]) + line_items = order_permissions.visible_line_items.in_orders(orders.result) + .order("supplier.name", "product.name", "variant.display_name") + + 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/reporting/queries/query_builder.rb b/lib/reporting/queries/query_builder.rb index ef3080aa31..4def4b6c5d 100644 --- a/lib/reporting/queries/query_builder.rb +++ b/lib/reporting/queries/query_builder.rb @@ -8,7 +8,7 @@ module Reporting attr_reader :grouping_fields - def initialize(model, grouping_fields = []) + def initialize(model, grouping_fields = proc { [] }) @grouping_fields = instance_exec(&grouping_fields) super model.arel_table @@ -84,7 +84,7 @@ module Reporting end def summary_row_title - I18n.t("total_items", scope: i18n_scope) + I18n.t("total", scope: i18n_scope) end def i18n_scope diff --git a/lib/reporting/report_headers_builder.rb b/lib/reporting/report_headers_builder.rb new file mode 100644 index 0000000000..7d3a6b8e29 --- /dev/null +++ b/lib/reporting/report_headers_builder.rb @@ -0,0 +1,45 @@ +# frozen_string_literal: true + +module Reporting + class ReportHeadersBuilder + attr_reader :report + + def initialize(report) + @report = report + end + + def table_headers + report.columns.keys.filter{ |key| !key.in?(fields_to_hide) }.map do |key| + translate_header(key) + end + end + + def available_headers + report.columns.keys.map { |key| [translate_header(key), key] } + end + + def fields_to_hide + if report.display_header_row? + report.formatted_rules.map { |rule| rule[:fields_used_in_header] }.flatten.reject(&:blank?) + else + [] + end.concat(params_fields_to_hide) + end + + private + + def translate_header(key) + # Quite some headers use currency interpolation, so providing it by default + default_params = { currency: currency_symbol, currency_symbol: currency_symbol } + report.custom_headers[key] || I18n.t("report_header_#{key}", **default_params) + end + + def currency_symbol + Spree::Money.currency_symbol + end + + def params_fields_to_hide + report.params[:fields_to_hide]&.map(&:to_sym) || [] + end + end +end diff --git a/lib/reporting/report_loader.rb b/lib/reporting/report_loader.rb index b40f03b4ca..f38245c209 100644 --- a/lib/reporting/report_loader.rb +++ b/lib/reporting/report_loader.rb @@ -2,21 +2,19 @@ module Reporting class ReportLoader - delegate :report_subtypes, to: :base_class - def initialize(report_type, report_subtype = nil) @report_type = report_type @report_subtype = report_subtype end def report_class - "#{report_module}::#{report_subtype_class}".constantize + "#{report_module}::#{report_subtype.camelize}".constantize rescue NameError - raise Reporting::Errors::ReportNotFound - end - - def default_report_subtype - report_subtypes.first || "base" + begin + "#{report_module}::Base".constantize + rescue NameError + raise Reporting::Errors::ReportNotFound + end end private @@ -26,15 +24,5 @@ module Reporting def report_module "Reporting::Reports::#{report_type.camelize}" end - - def report_subtype_class - (report_subtype || default_report_subtype).camelize - end - - def base_class - "#{report_module}::Base".constantize - rescue NameError - raise Reporting::Errors::ReportNotFound - end end end diff --git a/lib/reporting/report_query_template.rb b/lib/reporting/report_query_template.rb new file mode 100644 index 0000000000..4ab64b130f --- /dev/null +++ b/lib/reporting/report_query_template.rb @@ -0,0 +1,55 @@ +# frozen_string_literal: true + +# This is the new report template that use QueryBuilder to directly get the data from the DB +module Reporting + class ReportQueryTemplate < ReportTemplate + def report_query + raise NotImplementedError + end + + def report_data + @report_data ||= report_query.raw_result + end + + # ReportQueryTemplate work differently than standard reports + # Here the query_result is already the expected result, so we just create + # a fake columns method to copy the sql result into the row result + def columns + report_data.columns.map { |field| [field.to_sym, proc { |data| data[field] }] }.to_h + end + + def search + visible_line_items_relation.ransack(ransack_params) + end + + def query_result + report_data.to_a + end + + private + + def ransacked_line_items_relation + search.result + end + + def visible_orders_relation + ::Permissions::Order.new(user). + visible_orders.complete.not_state(:canceled). + select(:id).distinct + end + + def visible_line_items_relation + ::Permissions::Order.new(user). + visible_line_items. + select(:id).distinct + end + + def managed_orders_relation + ::Enterprise.managed_by(user).select(:id).distinct + end + + def i18n_scope + "admin.reports" + end + end +end diff --git a/lib/reporting/report_renderer.rb b/lib/reporting/report_renderer.rb index b9027ae60d..0c56d6daab 100644 --- a/lib/reporting/report_renderer.rb +++ b/lib/reporting/report_renderer.rb @@ -4,36 +4,92 @@ require 'spreadsheet_architect' module Reporting class ReportRenderer + REPORT_FORMATS = [:csv, :json, :xlsx, :pdf].freeze + def initialize(report) @report = report end + def raw_render? + @report.params[:report_format].in?(['json', 'csv']) + end + + def html_render? + @report.params[:report_format].in?(['', 'pdf']) + end + + def display_header_row? + @report.params[:display_header_row].present? && !raw_render? + end + + def display_summary_row? + @report.params[:display_summary_row].present? && !raw_render? + end + def table_headers - @report.report_data.columns + @report.table_headers || [] end def table_rows - @report.report_data.rows + @report.table_rows || [] end - def as_json - @report.report_data.as_json + def as_json(_context_controller = nil) + @report.rows.map(&:to_h).as_json end - def as_arrays - @as_arrays ||= [table_headers] + table_rows + def render_as(target_format, controller: nil) + unless target_format.to_sym.in?(REPORT_FORMATS) + raise ActionController::BadRequest, "report_format should be in #{REPORT_FORMATS}" + end + + public_send("to_#{target_format}", controller) end - def to_csv + def to_csv(_context_controller = nil) SpreadsheetArchitect.to_csv(headers: table_headers, data: table_rows) end - def to_ods - SpreadsheetArchitect.to_ods(headers: table_headers, data: table_rows) + def to_xlsx(_context_controller = nil) + SpreadsheetArchitect.to_xlsx(spreadsheets_options) end - def to_xlsx - SpreadsheetArchitect.to_xlsx(headers: table_headers, data: table_rows) + def to_pdf(context_controller) + WickedPdf.new.pdf_from_string( + context_controller.render_to_string( + template: 'admin/reports/_table', + layout: 'pdf', + locals: { report: @report } + ) + ) + end + + private + + def spreadsheets_options + { + headers: table_headers, + data: table_rows, + freeze_headers: true, + row_style: spreadsheets_style, + header_style: spreadsheets_style.merge({ bg_color: "f7f6f6", bold: true }), + conditional_row_styles: [ + { + # Detect header_row: the row is nil except for first cell + if: proc { |row_data, _row_index| + row_data.compact.count == 1 && row_data[0].present? + }, + styles: { font_size: 12, bold: true } + }, + ], + } + end + + def spreadsheets_style + { + font_name: 'system-ui', + alignment: { horizontal: :left, vertical: :bottom } + } end end end diff --git a/lib/reporting/report_row_builder.rb b/lib/reporting/report_row_builder.rb new file mode 100644 index 0000000000..96683138e1 --- /dev/null +++ b/lib/reporting/report_row_builder.rb @@ -0,0 +1,126 @@ +# frozen_string_literal: true + +module Reporting + class ReportRowBuilder + include ActionView::Helpers::NumberHelper + include ActionView::Helpers::TagHelper + + attr_reader :report + + def initialize(report) + @report = report + end + + # Compute the query result item into a result row + # We use OpenStruct to it's easier to access the properties + # i.e. row.my_field, rows.sum(&:quantity) + def build_row(item) + OpenStruct.new( + report.columns.transform_values do |column_constructor| + if column_constructor.is_a?(Symbol) + report.__send__(column_constructor, item) + else + column_constructor.call(item) + end + end + ) + end + + def slice_and_format_row(row) + result = row.to_h.reject { |k, _v| k.in?(report.fields_to_hide) } + unless report.raw_render? + result = result.map { |k, v| [k, format_cell(v, k)] }.to_h + end + OpenStruct.new(result) + end + + def build_header(rule, group_value, group_datas) + return if rule[:header].blank? + + rule[:header].call(group_value, group_datas.map(&:item), group_datas.map(&:full_row)) + end + + def build_summary_row(rule, group_value, datas) + return if rule[:summary_row].blank? + + proc_args = [group_value, datas.map(&:item), datas.map(&:full_row)] + row = rule[:summary_row].call(*proc_args) + row = slice_and_format_row(OpenStruct.new(row.reverse_merge!(blank_row))) + add_summary_row_label(row, rule, proc_args) + end + + private + + def add_summary_row_label(row, rule, proc_args) + previous_key = nil + label = rule[:summary_row_label] + label = label.call(*proc_args) if label.respond_to?(:call) + # Adds Total before first non empty column + row.each_pair do |key, value| + if value.present? && previous_key.present? && row[previous_key].blank? + row[previous_key] = label and break + end + + previous_key = key + end + row + end + + def blank_row + report.columns.transform_values { |_v| "" } + end + + # rubocop:disable Metrics/CyclomaticComplexity + def format_cell(value, column = nil) + return "" if value.nil? + + # Currency + if report.columns_format[column] == :currency || column.to_s.include?("price") + format_currency(value) + # Quantity + elsif report.columns_format[column] == :quantity && report.html_render? + format_quantity(value) + # Numeric + elsif report.columns_format[column] == :numeric + format_numeric(value) + # Boolean + elsif value.in? [true, false] + format_boolean(value) + # Time + elsif value.is_a?(Time) + format_time(value) + # Date + elsif value.is_a?(Date) + format_date(value) + # Default + else + value + end + end + # rubocop:enable Metrics/CyclomaticComplexity + + def format_currency(value) + value.present? ? number_to_currency(value, unit: Spree::Money.currency_symbol) : "" + end + + def format_quantity(value) + content_tag(value > 1 ? :strong : :span, value) + end + + def format_boolean(value) + value ? I18n.t(:yes) : I18n.t(:no) + end + + def format_time(value) + value.to_datetime.in_time_zone.strftime "%Y-%m-%d %H:%M" + end + + def format_date(value) + value.to_datetime.in_time_zone.strftime "%Y-%m-%d" + end + + def format_numeric(value) + number_with_delimiter(value) + end + end +end diff --git a/lib/reporting/report_rows_builder.rb b/lib/reporting/report_rows_builder.rb new file mode 100644 index 0000000000..1395aef98d --- /dev/null +++ b/lib/reporting/report_rows_builder.rb @@ -0,0 +1,114 @@ +# frozen_string_literal: true + +module Reporting + class ReportRowsBuilder + attr_reader :report + + def initialize(report) + @report = report + @builder = ReportRowBuilder.new(report) + end + + # Structured data by groups. This tree is used to render + # the grouped rows including group header and group summary_row if needed + def grouped_data + @grouped_data ||= build_tree(computed_data, report.formatted_rules) + rescue NotImplementedError + nil + end + + # Array of rows, each row being an OpenStruct with the computed data + # Exple [ + # { producer: "Freddy Shop", shop: true }, + # { producer: "Mary Farm", shop: false }, + # ] + def rows + @rows ||= extract_rows(grouped_data, []) + end + + # Array of rows, each row being a simple array with the data + # Exple [ + # ["Freddy Shop", true], + # ["Mary Farm", false], + # ] + def table_rows + @table_rows ||= rows.map(&:to_h).map(&:values) + end + + private + + def computed_data + @computed_data ||= report.query_result.map { |item| + row = @builder.build_row(item) + OpenStruct.new(item: item, full_row: row, row: @builder.slice_and_format_row(row)) + } + end + + def extract_rows(data, result) + data.each do |group_or_row| + if group_or_row[:is_group].present? + # Header Row + if group_or_row[:header].present? && report.display_header_row? + result << OpenStruct.new(header: group_or_row[:header]) + end + # Normal Row + extract_rows(group_or_row[:data], result) + # Summary Row + if group_or_row[:summary_row].present? && report.display_summary_row? + result << group_or_row[:summary_row] + end + else + result << group_or_row.row + end + end + result + end + + def build_tree(datas, remaining_rules) + return datas if remaining_rules.empty? + + rules = remaining_rules.clone + group_and_sort(rules.delete_at(0), rules, datas) + end + + def group_and_sort(rule, remaining_rules, datas) + result = [] + groups = group_data_with_rule(datas, rule) + sorted_groups = sort_groups_with_rule(groups, rule) + + sorted_groups.each do |group_value, group_datas| + result << { + is_group: true, + header: @builder.build_header(rule, group_value, group_datas), + header_class: rule[:header_class], + summary_row: @builder.build_summary_row(rule, group_value, group_datas), + summary_row_class: rule[:summary_row_class], + data: build_tree(group_datas, remaining_rules) + } + end + result + end + + def group_data_with_rule(datas, rule) + datas.group_by { |data| + if rule[:group_by].is_a?(Symbol) + data.full_row[rule[:group_by]] + else + rule[:group_by].call(data.item, data.full_row) + end + } + end + + def sort_groups_with_rule(groups, rule) + groups.sort_by do |group_key, _items| + # By default sort with the group_key if no sort_by rule is present + if rule[:sort_by].present? + rule[:sort_by].call(group_key) + else + # downcase for better comparaison + group_key.is_a?(String) ? group_key.downcase : group_key.to_s + end + end.to_h + end + end +end diff --git a/lib/reporting/report_ruler.rb b/lib/reporting/report_ruler.rb new file mode 100644 index 0000000000..448d600698 --- /dev/null +++ b/lib/reporting/report_ruler.rb @@ -0,0 +1,62 @@ +# frozen_string_literal: true + +module Reporting + class ReportRuler + def initialize(report) + @report = report + end + + def formatted_rules + @formatted_rules ||= @report.rules.map { |rule| format_rule(rule) } + end + + def header_option? + formatted_rules.find { |rule| rule[:header].present? } + end + + def summary_row_option? + formatted_rules.find { |rule| rule[:summary_row].present? } + end + + private + + def format_rule(rule) + handle_header_shortcuts(rule) + default_values = { + header_class: "h2", + summary_row_class: "text-bold", + summary_row_label: I18n.t('admin.reports.total') + } + rule.reverse_merge(default_values) + end + + def handle_header_shortcuts(rule) + # Handles shortcut header: :supplier + rule[:header] = Array(rule[:header]) if rule[:header].is_a?(Symbol) + # Handles shortcut header: [:last_name, :first_name] + case rule[:header] + when true + handle_shortcut_header_true(rule) + when proc { |r| r.is_a?(Array) } + handle_shortcut_header_array(rule) + end + rule[:fields_used_in_header] ||= [rule[:group_by]] if rule[:group_by].is_a?(Symbol) + end + + # header: true + # Use the grouping key for header + def handle_shortcut_header_true(rule) + rule[:header] = proc { |key, _items, _rows| key } + end + + # header: [:first_name, :last_name] + # Use the list of properties ot build the header + def handle_shortcut_header_array(rule) + rule[:fields_used_in_header] ||= rule[:header] + fields = rule[:header] + rule[:header] = proc do |_key, _items, rows| + fields.map { |field| rows.first[field] }.reject(&:blank?).join(' ') + end + end + end +end diff --git a/lib/reporting/report_template.rb b/lib/reporting/report_template.rb index e8de2cd322..de8f291d0f 100644 --- a/lib/reporting/report_template.rb +++ b/lib/reporting/report_template.rb @@ -2,61 +2,117 @@ module Reporting class ReportTemplate - delegate :as_json, :as_arrays, :table_headers, :table_rows, - :to_csv, :to_xlsx, :to_ods, :to_json, to: :renderer + include ReportsHelper + attr_accessor :user, :params, :ransack_params - attr_reader :options + delegate :render_as, :as_json, :to_csv, :to_xlsx, :to_pdf, :to_json, to: :renderer + delegate :raw_render?, :html_render?, :display_header_row?, :display_summary_row?, to: :renderer - SUBTYPES = [] + delegate :rows, :table_rows, :grouped_data, to: :rows_builder + delegate :available_headers, :table_headers, :fields_to_hide, to: :headers_builder - def self.report_subtypes - self::SUBTYPES + delegate :formatted_rules, :header_option?, :summary_row_option?, to: :ruler + + def initialize(user, params = {}, request = nil) + if request.nil? || request.get? + params.reverse_merge!(default_params) + params[:q] ||= {} + params[:q].reverse_merge!(default_params[:q]) if default_params[:q].present? + end + @user = user + @params = params + @params = @params.permit!.to_h unless @params.is_a? Hash + @ransack_params = @params[:q] || {} end - def initialize(current_user, ransack_params, options = {}) - @current_user = current_user - @ransack_params = ransack_params.with_indifferent_access - @options = ( options || {} ).with_indifferent_access + # Message to be displayed at the top of rendered table + def message + "" end - def report_data - @report_data ||= report_query.raw_result + # Ransack search to get base ActiveRelation + # If the report object do not use ransack search, create a fake one just for the form_for + # in reports/show.haml + def search + Ransack::Search.new(Spree::Order) + end + + # The search result, usually an ActiveRecord Array + def query_result + raise NotImplementedError + end + + # Convert the query_result into expected row result (which will be displayed) + # Example + # { + # name: proc { |model| model.display_name }, + # best_friend: proc { |model| model.friends.first.first_name } + # } + def columns + raise NotImplementedError + end + + # Exple { total_price: :currency } + def columns_format + {} + end + + # Headers are automatically translated with table_headers method + # You can customize some header name if needed + def custom_headers + {} + end + + # Rules for grouping, ordering, and summary rows + # Rule Full Example. In the following item reference the query_result item and + # row the transformation of this item into the expected result + # { + # group_by: proc { |item, row| row.last_name }, + # group_by: :last_name, # same that previous line, + # group_by: proc { |line_item, row| line_item.product }, + # sort_by: proc { |product| product.name }, + # header: proc { |group_key, items, rows| items.first.display_name }, + # header: true, # shortcut to use group_key as header + # header: :supplier, # shortcut to use supplier column as header + # header: [:last_name, :first_name], # shortcut to use last_name & first_name as header + # header_class: "h1 h2 h3 h4 text-center background", # class applies to the header row + # # Those fields will be hidden when the header_row is activated + # fields_used_in_header: [:first_name, :last_name], + # summary_row: proc do |group_key, items, rows| + # { + # quantity: rows.sum(&:quantity), + # price: "#{rows.sum(&:price)} #{currency_symbol}" + # } + # end, + # summary_row_class: "", # by default 'text-bold' + # summary_row_label: "Total by Customer" # by default 'TOTAL' + # summary_row_label: proc { |group_key, items, rows| "Total for #{group_key}" } + # } + def rules + [] + end + + # Default filters/search params to be used + def default_params + {} end private - attr_reader :current_user, :ransack_params - def renderer @renderer ||= ReportRenderer.new(self) end - def ransacked_orders_relation - visible_orders_relation.ransack(ransack_params).result + def rows_builder + @rows_builder ||= ReportRowsBuilder.new(self) end - def ransacked_line_items_relation - visible_line_items_relation.ransack(ransack_params).result + def headers_builder + @headers_builder ||= ReportHeadersBuilder.new(self) end - def visible_orders_relation - ::Permissions::Order.new(current_user). - visible_orders.complete.not_state(:canceled). - select(:id).distinct - end - - def visible_line_items_relation - ::Permissions::Order.new(current_user). - visible_line_items. - select(:id).distinct - end - - def managed_orders_relation - ::Enterprise.managed_by(current_user).select(:id).distinct - end - - def i18n_scope - "admin.reports" + def ruler + @ruler ||= ReportRuler.new(self) end end end diff --git a/lib/reporting/reports/bulk_coop/allocation.rb b/lib/reporting/reports/bulk_coop/allocation.rb new file mode 100644 index 0000000000..4805a90d73 --- /dev/null +++ b/lib/reporting/reports/bulk_coop/allocation.rb @@ -0,0 +1,47 @@ +# frozen_string_literal: true + +module Reporting + module Reports + module BulkCoop + class Allocation < Base + def query_result + table_items.group_by(&:order).values + end + + def columns + { + customer: :order_billing_address_name, + product: :product_name, + bulk_unit_size: :product_group_buy_unit_size, + variant: :full_name, + variant_value: :option_value_value, + variant_unit: :option_value_unit, + weight: :weight_from_unit_value, + sum_total: :total_amount, + total_available: :empty_cell, + unallocated: :empty_cell, + max_quantity_excess: :empty_cell + } + end + + def rules + [ + { + group_by: :product, + header: true, + summary_row: proc do |_key, items, rows| + line_items = items.flatten + { + sum_total: rows.sum(&:sum_total), + total_available: total_available(line_items), + unallocated: remainder(line_items), + max_quantity_excess: max_quantity_excess(line_items) + } + end + } + ] + end + end + end + end +end diff --git a/lib/reporting/reports/bulk_coop/base.rb b/lib/reporting/reports/bulk_coop/base.rb new file mode 100644 index 0000000000..e6d0c0229a --- /dev/null +++ b/lib/reporting/reports/bulk_coop/base.rb @@ -0,0 +1,131 @@ +# frozen_string_literal: true + +module Reporting + module Reports + module BulkCoop + class Base < ReportTemplate + def message + I18n.t("spree.admin.reports.customer_names_message.customer_names_tip") + end + + def search + report_line_items.orders + end + + def table_items + report_line_items.list(line_item_includes) + end + + private + + 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) + end + + def report_line_items + @report_line_items ||= Reporting::LineItems.new( + order_permissions, + @params, + CompleteVisibleOrders.new(order_permissions).query + ) + 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 scaled_unit_value(variant) + (variant.unit_value || 0) / (variant.product.variant_unit_scale || 1) + 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 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 remainder(line_items) + remainder = total_available(line_items) - total_amount(line_items) + remainder >= 0 ? remainder : '' + end + + def total_amount(line_items) + line_items.sum { |li| scaled_final_weight_volume(li) } + end + + def scaled_final_weight_volume(line_item) + (line_item.final_weight_volume || 0) / (line_item.product.variant_unit_scale || 1) + end + + def total_available(line_items) + units_required(line_items) * group_buy_unit_size(line_items) + 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).to_i + end + + def variant_product_name(line_items) + line_items.first.variant.product.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/customer_payments.rb b/lib/reporting/reports/bulk_coop/customer_payments.rb new file mode 100644 index 0000000000..97752b3feb --- /dev/null +++ b/lib/reporting/reports/bulk_coop/customer_payments.rb @@ -0,0 +1,45 @@ +# frozen_string_literal: true + +module Reporting + module Reports + module BulkCoop + class CustomerPayments < Base + def query_result + table_items.reorder("spree_orders.completed_at").group_by(&:order).values + end + + def columns + { + customer: :order_billing_address_name, + date_of_order: :order_completed_at, + total_cost: :customer_payments_total_cost, + amount_owing: :customer_payments_amount_owed, + amount_paid: :customer_payments_amount_paid + } + end + + private + + 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 order_completed_at(line_items) + line_items.first.order.completed_at + end + end + end + end +end diff --git a/lib/reporting/reports/bulk_coop/packing_sheets.rb b/lib/reporting/reports/bulk_coop/packing_sheets.rb new file mode 100644 index 0000000000..3086cb3c42 --- /dev/null +++ b/lib/reporting/reports/bulk_coop/packing_sheets.rb @@ -0,0 +1,28 @@ +# frozen_string_literal: true + +module Reporting + module Reports + module BulkCoop + class PackingSheets < Base + def query_result + table_items.group_by(&:order).values + end + + def columns + { + customer: :order_billing_address_name, + product: :product_name, + variant: :full_name, + sum_total: :total_quantity + } + end + + private + + def total_quantity(line_items) + line_items.sum(&:quantity) + end + end + end + end +end diff --git a/lib/reporting/reports/bulk_coop/supplier_report.rb b/lib/reporting/reports/bulk_coop/supplier_report.rb new file mode 100644 index 0000000000..16257b26c7 --- /dev/null +++ b/lib/reporting/reports/bulk_coop/supplier_report.rb @@ -0,0 +1,57 @@ +# frozen_string_literal: true + +module Reporting + module Reports + module BulkCoop + class SupplierReport < Base + def query_result + table_items.group_by(&:variant).values + end + + def columns + { + supplier: :variant_product_supplier_name, + product: :variant_product_name, + bulk_unit_size: :variant_product_group_buy_unit_size_f, + variant: :full_name, + variant_value: :option_value_value, + variant_unit: :option_value_unit, + weight: :weight_from_unit_value, + sum_total: :total_amount, + units_required: :empty_cell, + unallocated: :empty_cell, + max_quantity_excess: :empty_cell + } + end + + def columns_format + { sum_total: :currency } + end + + def rules + [ + { + group_by: :supplier, + header: true, + summary_row: proc do |_key, items, rows| + line_items = items.flatten + { + sum_total: rows.sum(&:sum_total), + units_required: units_required(line_items), + unallocated: remainder(line_items), + max_quantity_excess: max_quantity_excess(line_items) + } + end + } + ] + end + + private + + def variant_product_supplier_name(line_items) + line_items.first.variant.product.supplier.name + end + end + end + end +end diff --git a/lib/reporting/reports/customers/addresses.rb b/lib/reporting/reports/customers/addresses.rb new file mode 100644 index 0000000000..fa024c446d --- /dev/null +++ b/lib/reporting/reports/customers/addresses.rb @@ -0,0 +1,22 @@ +# frozen_string_literal: true + +module Reporting + module Reports + module Customers + class Addresses < Base + def columns + { + first_name: proc { |order| order.billing_address.firstname }, + last_name: proc { |order| order.billing_address.lastname }, + billing_address: proc { |order| order.billing_address.address_and_city }, + email: proc { |order| order.email }, + phone: proc { |order| order.billing_address.phone }, + hub: proc { |order| order.distributor&.name }, + hub_address: proc { |order| order.distributor&.address&.address_and_city }, + shipping_method: proc { |order| order.shipping_method&.name }, + } + end + end + end + end +end diff --git a/lib/reporting/reports/customers/base.rb b/lib/reporting/reports/customers/base.rb new file mode 100644 index 0000000000..f926e54b8a --- /dev/null +++ b/lib/reporting/reports/customers/base.rb @@ -0,0 +1,49 @@ +# frozen_string_literal: true + +module Reporting + module Reports + module Customers + class Base < ReportTemplate + def query_result + 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 + end + end + end +end diff --git a/lib/reporting/reports/customers/mailing_list.rb b/lib/reporting/reports/customers/mailing_list.rb new file mode 100644 index 0000000000..492a0abb4d --- /dev/null +++ b/lib/reporting/reports/customers/mailing_list.rb @@ -0,0 +1,18 @@ +# frozen_string_literal: true + +module Reporting + module Reports + module Customers + class MailingList < Base + def columns + { + email: proc { |order| order.email }, + first_name: proc { |order| order.billing_address.firstname }, + last_name: proc { |order| order.billing_address.lastname }, + suburb: proc { |order| order.billing_address.city }, + } + 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/lib/reporting/reports/enterprise_fee_summary/base.rb b/lib/reporting/reports/enterprise_fee_summary/base.rb new file mode 100644 index 0000000000..99d17747a3 --- /dev/null +++ b/lib/reporting/reports/enterprise_fee_summary/base.rb @@ -0,0 +1,83 @@ +# frozen_string_literal: true + +module Reporting + module Reports + module EnterpriseFeeSummary + class Base < ReportTemplate + attr_accessor :permissions, :parameters + + def initialize(user, params = {}, request = nil) + super(user, params, request) + p = params[:q] + if p.present? + p['start_at'] = p.delete('completed_at_gt') + p['end_at'] = p.delete('completed_at_lt') + end + @parameters = Parameters.new(p || {}) + @parameters.validate! + @permissions = Permissions.new(user) + @parameters.authorize!(@permissions) + end + + def custom_headers + data_attributes.map { |attr| [attr, I18n.t("header.#{attr}", scope: i18n_scope)] }.to_h + end + + def i18n_scope + "order_management.reports.enterprise_fee_summary.formats.csv" + end + + def message + I18n.t("spree.admin.reports.customer_names_message.customer_names_tip") + end + + def query_result + enterprise_fee_type_total_list.sort + end + + def data_attributes + [ + :fee_type, + :enterprise_name, + :fee_name, + :customer_name, + :fee_placement, + :fee_calculated_on_transfer_through_name, + :tax_category_name, + :total_amount + ] + end + + # This report calculate data in a different way, so we just encapsulate the result + # in the columns method + def columns + data_attributes.map { |field| + [field.to_sym, proc { |data| data.public_send(field) }] + }.to_h + end + + private + + def enterprise_fees_by_customer + if parameters.order_cycle_ids.empty? + # Always restrict to permitted order cycles + parameters.order_cycle_ids = permissions.allowed_order_cycles.map(&:id) + end + Scope.new.apply_filters(parameters).result + end + + def enterprise_fee_type_total_list + enterprise_fees_by_customer.map do |total_data| + summarizer = Summarizer.new(total_data) + + ReportData::EnterpriseFeeTypeTotal.new.tap do |total| + data_attributes.each do |attribute| + total.public_send("#{attribute}=", summarizer.public_send(attribute)) + end + end + end + end + end + end + end +end 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/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 92% 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 d32f2492e0..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,11 +1,9 @@ # frozen_string_literal: true -module OrderManagement +module Reporting module Reports module EnterpriseFeeSummary - class Parameters < ::Reports::Parameters::Base - extend ActiveModel::Naming - extend ActiveModel::Translation + class Parameters < Reporting::Reports::EnterpriseFeeSummary::Reports::Parameters::Base include ActiveModel::Validations attr_accessor :start_at, :end_at, :distributor_ids, :producer_ids, :order_cycle_ids, @@ -30,6 +28,8 @@ module OrderManagement self.payment_method_ids = [] super(attributes) + + cleanup_arrays end def authorize!(permissions) 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 92% 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 e518d0b89b..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,9 +1,15 @@ # frozen_string_literal: true -module OrderManagement +module Reporting module Reports module EnterpriseFeeSummary - class Permissions < ::Reports::Permissions + class Permissions + attr_accessor :user + + def initialize(user) + @user = user + end + def allowed_order_cycles @allowed_order_cycles ||= OrderCycle.visible_by(user) end 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 0be3d77921..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,11 +1,9 @@ # frozen_string_literal: true -module OrderManagement +module Reporting module Reports module EnterpriseFeeSummary class Scope - attr_accessor :parameters - def initialize setup_default_scope end 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 62% rename from lib/open_food_network/reports/list.rb rename to lib/reporting/reports/list.rb index bfbc625488..f43e61debd 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 @@ -9,12 +9,17 @@ module OpenFoodNetwork def all { + orders_and_distributors: [], + bulk_coop: bulk_coop_report_types, + payments: payments_report_types, orders_and_fulfillment: orders_and_fulfillment_report_types, - products_and_inventory: products_and_inventory_report_types, customers: customers_report_types, - enterprise_fee_summary: enterprise_fee_summary_report_types, + products_and_inventory: products_and_inventory_report_types, + users_and_enterprises: [], + enterprise_fee_summary: [], order_cycle_management: order_cycle_management_report_types, sales_tax: sales_tax_report_types, + xero_invoices: xero_report_types, packing: packing_report_types } end @@ -39,6 +44,14 @@ module OpenFoodNetwork ] end + def payments_report_types + [ + [I18n.t(:report_payment_by), :payments_by_payment_type], + [I18n.t(:report_itemised_payment), :itemised_payment_totals], + [I18n.t(:report_payment_totals), :payment_totals] + ] + end + def customers_report_types [ [i18n_translate("mailing_list"), :mailing_list], @@ -46,12 +59,6 @@ module OpenFoodNetwork ] end - def enterprise_fee_summary_report_types - [ - [i18n_translate("enterprise_fee_summary"), :enterprise_fee_summary] - ] - end - def order_cycle_management_report_types [ [i18n_translate("payment_methods"), :payment_methods], @@ -68,11 +75,32 @@ module OpenFoodNetwork def packing_report_types [ - [i18n_translate("pack_by_customer"), :pack_by_customer], - [i18n_translate("pack_by_supplier"), :pack_by_supplier] + [i18n_translate("pack_by_customer"), :customer], + [i18n_translate("pack_by_supplier"), :supplier], + [i18n_translate("pack_by_product"), :product] ] end + def xero_report_types + [ + [I18n.t(:summary), 'summary'], + [I18n.t(:detailed), 'detailed'] + ] + end + + def bulk_coop_report_types + [ + bulk_coop_item(:supplier_report), + bulk_coop_item(:allocation), + bulk_coop_item(:packing_sheets), + bulk_coop_item(:customer_payments) + ] + end + + def bulk_coop_item(key) + [I18n.t("order_management.reports.bulk_coop.filters.bulk_coop_#{key}"), key] + end + def i18n_translate(key) I18n.t(key, scope: "admin.reports") end diff --git a/lib/reporting/reports/order_cycle_management/base.rb b/lib/reporting/reports/order_cycle_management/base.rb new file mode 100644 index 0000000000..68c08fe81f --- /dev/null +++ b/lib/reporting/reports/order_cycle_management/base.rb @@ -0,0 +1,81 @@ +# frozen_string_literal: true + +module Reporting + module Reports + module OrderCycleManagement + class Base < ReportTemplate + def default_params + { + q: { + completed_at_gt: 1.month.ago.beginning_of_day, + completed_at_lt: 1.day.from_now.beginning_of_day + } + } + end + + def search + Spree::Order. + finalized. + not_state(:canceled). + distributed_by_user(@user). + managed_by(@user). + ransack(ransack_params) + end + + # This result is used in _order_cucle_management.html so caching it + def query_result + @query_result ||= orders + end + + def orders + search_result = search.result.order(:completed_at) + orders = OutstandingBalance.new(search_result).query.select('spree_orders.*') + + filter(orders) + end + + def filter(orders) + filter_to_payment_method filter_to_shipping_method filter_to_order_cycle orders + end + + private + + 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 customer_code(email) + customer = Customer.where(email: email).first + customer.nil? ? "" : customer.code + end + end + end + end +end diff --git a/lib/reporting/reports/order_cycle_management/delivery.rb b/lib/reporting/reports/order_cycle_management/delivery.rb new file mode 100644 index 0000000000..3208ce7624 --- /dev/null +++ b/lib/reporting/reports/order_cycle_management/delivery.rb @@ -0,0 +1,35 @@ +# frozen_string_literal: true + +module Reporting + module Reports + module OrderCycleManagement + class Delivery < Base + # rubocop:disable Metrics/AbcSize + def columns + { + first_name: proc { |order| order.shipping_address.firstname }, + last_name: proc { |order| order.shipping_address.lastname }, + hub: proc { |order| order.distributor&.name }, + customer_code: proc { |order| customer_code(order.email) }, + delivery_address: proc { |order| order.shipping_address.address_and_city }, + delivery_postcode: proc { |order| order.shipping_address.zipcode }, + phone: proc { |order| order.shipping_address.phone }, + shipping_method: proc { |order| order.shipping_method&.name }, + payment_method: proc { |order| order.payments.first&.payment_method&.name }, + amount: proc { |order| order.total }, + balance: proc { |order| order.balance_value }, + temp_controlled_items: proc { |order| has_temperature_controlled_items?(order) }, + special_instructions: proc { |order| order.special_instructions }, + } + end + # rubocop:enable Metrics/AbcSize + + def has_temperature_controlled_items?(order) + order.line_items.any? { |line_item| + line_item.product.shipping_category&.temperature_controlled + } + end + end + end + end +end diff --git a/lib/reporting/reports/order_cycle_management/payment_methods.rb b/lib/reporting/reports/order_cycle_management/payment_methods.rb new file mode 100644 index 0000000000..6556d762e5 --- /dev/null +++ b/lib/reporting/reports/order_cycle_management/payment_methods.rb @@ -0,0 +1,28 @@ +# frozen_string_literal: true + +module Reporting + module Reports + module OrderCycleManagement + class PaymentMethods < Base + # rubocop:disable Metrics/AbcSize + # rubocop:disable Metrics/CyclomaticComplexity + def columns + { + first_name: proc { |order| order.billing_address&.firstname }, + last_name: proc { |order| order.billing_address&.lastname }, + hub: proc { |order| order.distributor&.name }, + customer_code: proc { |order| customer_code(order.email) }, + email: proc { |order| order.email }, + phone: proc { |order| order.billing_address&.phone }, + shipping_method: proc { |order| order.shipping_method&.name }, + payment_method: proc { |order| order.payments.last&.payment_method&.name }, + amount: proc { |order| order.total }, + balance: proc { |order| order.balance_value }, + } + end + # rubocop:enable Metrics/AbcSize + # rubocop:enable Metrics/CyclomaticComplexity + end + end + end +end diff --git a/lib/reporting/reports/orders_and_distributors/base.rb b/lib/reporting/reports/orders_and_distributors/base.rb new file mode 100644 index 0000000000..9225096680 --- /dev/null +++ b/lib/reporting/reports/orders_and_distributors/base.rb @@ -0,0 +1,59 @@ +# frozen_string_literal: true + +module Reporting + module Reports + module OrdersAndDistributors + class Base < ReportTemplate + # rubocop:disable Metrics/AbcSize + def columns + { + order_date: proc { |line_item| line_item.order.completed_at.strftime("%F %T") }, + order_id: proc { |line_item| line_item.order.id }, + customer_name: proc { |line_item| line_item.order.bill_address.full_name }, + customer_email: proc { |line_item| line_item.order.email }, + customer_phone: proc { |line_item| line_item.order.bill_address.phone }, + customer_city: proc { |line_item| line_item.order.bill_address.city }, + sku: proc { |line_item| line_item.product.sku }, + item_name: proc { |line_item| line_item.product.name }, + variant: proc { |line_item| line_item.options_text }, + quantity: proc { |line_item| line_item.quantity }, + max_quantity: proc { |line_item| line_item.max_quantity }, + cost: proc { |line_item| line_item.price * line_item.quantity }, + shipping_cost: proc { |line_item| line_item.distribution_fee }, + payment_method: proc { |li| li.order.payments.first&.payment_method&.name }, + distributor: proc { |line_item| line_item.order.distributor&.name }, + distributor_address: proc { |line_item| line_item.order.distributor.address.address1 }, + distributor_city: proc { |line_item| line_item.order.distributor.address.city }, + distributor_postcode: proc { |line_item| line_item.order.distributor.address.zipcode }, + shipping_method: proc { |line_item| line_item.order.shipping_method&.name }, + shipping_instructions: proc { |line_item| line_item.order.special_instructions } + } + end + # rubocop:enable Metrics/AbcSize + + def search + permissions.visible_orders.select("DISTINCT spree_orders.*"). + complete.not_state(:canceled). + ransack(ransack_params) + end + + def query_result + orders = search.result + # Mask non editable order details + editable_orders_ids = permissions.editable_orders.select(&:id).map(&:id) + orders + .filter { |order| order.in?(editable_orders_ids) } + .each { |order| OrderDataMasker.new(order).call } + # Get Line Items + orders.map(&:line_items).flatten + end + + private + + def permissions + @permissions ||= ::Permissions::Order.new(user, ransack_params) + end + end + end + end +end diff --git a/lib/reporting/reports/orders_and_fulfillment/base.rb b/lib/reporting/reports/orders_and_fulfillment/base.rb new file mode 100644 index 0000000000..703f5e12b9 --- /dev/null +++ b/lib/reporting/reports/orders_and_fulfillment/base.rb @@ -0,0 +1,94 @@ +# frozen_string_literal: true + +module Reporting + module Reports + module OrdersAndFulfillment + class Base < ReportTemplate + def message + I18n.t("spree.admin.reports.customer_names_message.customer_names_tip") + end + + def default_params + { + q: { + completed_at_gt: 1.month.ago.beginning_of_day, + completed_at_lt: 1.day.from_now.beginning_of_day + } + } + end + + def search + report_line_items.orders + end + + def query_result + report_line_items.list(line_item_includes).group_by(&:variant_id).values + end + + private + + def order_permissions + return @order_permissions unless @order_permissions.nil? + + @order_permissions = ::Permissions::Order.new(@user, ransack_params) + end + + def report_line_items + @report_line_items ||= Reporting::LineItems.new(order_permissions, params) + end + + def variant_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 hub_name + proc { |line_items| line_items.first.order.distributor.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 ||= {} + @variant_scopers_by_distributor_id[distributor_id] ||= + OpenFoodNetwork::ScopeVariantToHub.new( + distributor_id, + report_variant_overrides[distributor_id] || {}, + ) + 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 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/order_cycle_customer_totals.rb b/lib/reporting/reports/orders_and_fulfillment/order_cycle_customer_totals.rb new file mode 100644 index 0000000000..1c3739aed6 --- /dev/null +++ b/lib/reporting/reports/orders_and_fulfillment/order_cycle_customer_totals.rb @@ -0,0 +1,155 @@ +# frozen_string_literal: true + +# rubocop:disable Metrics/ClassLength +module Reporting + module Reports + module OrdersAndFulfillment + class OrderCycleCustomerTotals < Base + # rubocop:disable Metrics/AbcSize + # rubocop:disable Metrics/MethodLength + # rubocop:disable Metrics/PerceivedComplexity + # rubocop:disable Metrics/CyclomaticComplexity + # rubocop:disable Naming/VariableNumber + def columns + { + hub: hub_name, + customer: proc { |line_items| + bill_address = bill_address(line_items) + "#{bill_address&.firstname} #{bill_address&.lastname}" + }, + email: proc { |line_items| line_items.first.order.email }, + phone: proc { |line_items| bill_address(line_items)&.phone }, + producer: supplier_name, + product: product_name, + variant: variant_name, + + quantity: proc { |line_items| line_items.to_a.sum(&:quantity) }, + item_price: proc { |line_items| line_items.sum(&:amount) }, + item_fees_price: proc { |line_items| line_items.sum(&:amount_with_adjustments) }, + admin_handling_fees: proc { |_line_items| "" }, + ship_price: proc { |_line_items| "" }, + pay_fee_price: proc { |_line_items| "" }, + total_price: proc { |_line_items| "" }, + paid: proc { |line_items| line_items.all? { |li| li.order.paid? } }, + + shipping: proc { |line_items| shipping_method(line_items)&.name }, + delivery: proc { |line_items| delivery?(line_items) }, + + ship_street: proc { |line_items| ship_address(line_items)&.address1 }, + ship_street_2: proc { |line_items| ship_address(line_items)&.address2 }, + ship_city: proc { |line_items| ship_address(line_items)&.city }, + ship_postcode: proc { |line_items| ship_address(line_items)&.zipcode }, + ship_state: proc { |line_items| ship_address(line_items)&.state }, + + comments: proc { |_line_items| "" }, + sku: 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, + + order_cycle: proc { |line_items| line_items.first.order.order_cycle&.name }, + payment_method: proc { |line_items| + payment = line_items.first.order.payments.first + payment&.payment_method&.name + }, + customer_code: proc { |line_items| distributor_customer(line_items)&.code }, + tags: proc { |line_items| distributor_customer(line_items)&.tags&.join(', ') }, + + billing_street: proc { |line_items| bill_address(line_items)&.address1 }, + billing_street_2: proc { |line_items| bill_address(line_items)&.address2 }, + billing_city: proc { |line_items| bill_address(line_items)&.city }, + billing_postcode: proc { |line_items| bill_address(line_items)&.zipcode }, + billing_state: proc { |line_items| bill_address(line_items)&.state }, + + order_number: proc { |line_items| line_items.first.order.number }, + date: 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 + # rubocop:enable Metrics/CyclomaticComplexity + # rubocop:enable Naming/VariableNumber + + def rules + [ + { + group_by: :hub, + header: proc { |key, _items, _rows| "#{I18n.t(:report_header_hub)} #{key}" }, + header_class: "h1", + }, + { + group_by: proc { |line_items, _row| line_items.first.order }, + sort_by: proc { |order| order.bill_address.full_name_reverse }, + header: proc { |_order, _items, rows| row_header(rows.first) }, + fields_used_in_header: [:customer, :email, :phone, :order_cycle, :order_number], + summary_row: proc { |order, _grouped_line_items, rows| summary_row(order, rows) } + }, + ] + end + + 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 row_header(row) + result = row.customer + result += " - #{row.email}" if row.email + result += " - #{row.phone}" if row.phone + result += " | #{row.order_cycle} (#{row.order_number})" + result + end + + def summary_row(order, rows) + { + hub: rows.last.hub, + customer: rows.last.customer, + item_price: rows.sum(&:item_price), + item_fees_price: rows.sum(&:item_fees_price), + admin_handling_fees: order.admin_and_handling_total, + ship_price: order.ship_total, + pay_fee_price: order.payment_fee, + total_price: order.total, + paid: order.paid?, + comments: order.special_instructions, + order_cycle: order.order_cycle&.name, + payment_method: order.payments.first&.payment_method&.name, + order_number: order.number, + date: order.completed_at, + } + end + + def shipping_method(line_items) + return unless shipping_rates = line_items.first.order.shipments.first&.shipping_rates + + shipping_rate = shipping_rates.find(&:selected) || shipping_rates.first + shipping_rate.try(:shipping_method) + end + + def delivery?(line_items) + shipping_method(line_items)&.delivery? + end + + def ship_address(line_items) + line_items.first.order.ship_address if delivery?(line_items) + end + + def bill_address(line_items) + line_items.first.order.bill_address + end + + def distributor_customer(line_items) + distributor = line_items.first.order.distributor + user = line_items.first.order.user + user&.customer_of(distributor) + end + end + end + end +end +# rubocop:enable Metrics/ClassLength diff --git a/lib/reporting/reports/orders_and_fulfillment/order_cycle_distributor_totals_by_supplier.rb b/lib/reporting/reports/orders_and_fulfillment/order_cycle_distributor_totals_by_supplier.rb new file mode 100644 index 0000000000..4c5c093d36 --- /dev/null +++ b/lib/reporting/reports/orders_and_fulfillment/order_cycle_distributor_totals_by_supplier.rb @@ -0,0 +1,50 @@ +# frozen_string_literal: true + +module Reporting + module Reports + module OrdersAndFulfillment + class OrderCycleDistributorTotalsBySupplier < Base + def columns + { + hub: hub_name, + producer: supplier_name, + product: product_name, + variant: variant_name, + quantity: proc { |line_items| line_items.to_a.sum(&:quantity) }, + curr_cost_per_unit: proc { |line_items| line_items.first.price }, + total_cost: proc { |line_items| line_items.sum(&:amount) }, + total_shipping_cost: proc { |_line_items| "" }, + shipping_method: proc { |line_items| line_items.first.order.shipping_method&.name } + } + end + + def rules + [ + { + group_by: :hub, + header: proc { |key, _items, _rows| "#{I18n.t(:report_header_hub)} #{key}" }, + summary_row: proc do |_key, line_items, rows| + { + total_cost: rows.sum(&:total_cost), + total_shipping_cost: line_items.map(&:first).map(&:order).uniq.sum(&:ship_total), + shipping_method: rows.first.shipping_method + } + end + } + ] + end + + 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/order_cycle_supplier_totals.rb b/lib/reporting/reports/orders_and_fulfillment/order_cycle_supplier_totals.rb new file mode 100644 index 0000000000..1a1a085cf0 --- /dev/null +++ b/lib/reporting/reports/orders_and_fulfillment/order_cycle_supplier_totals.rb @@ -0,0 +1,41 @@ +# frozen_string_literal: true + +module Reporting + module Reports + module OrdersAndFulfillment + class OrderCycleSupplierTotals < Base + def columns + { + producer: supplier_name, + product: product_name, + variant: variant_name, + quantity: proc { |line_items| line_items.sum(&:quantity) }, + total_units: proc { |line_items| total_units(line_items) }, + curr_cost_per_unit: proc { |line_items| line_items.first.price }, + total_cost: proc { |line_items| line_items.sum(&:amount) } + } + end + + def rules + [ + { + group_by: :producer, + header: true, + summary_row: proc do |_key, _items, rows| + { + quantity: rows.sum(&:quantity), + total_units: rows.sum(&:total_units), + total_cost: rows.sum(&:total_cost) + } + end + } + ] + end + + def line_item_includes + [{ variant: [{ option_values: :option_type }, { product: :supplier }] }] + end + end + end + end +end diff --git a/lib/reporting/reports/orders_and_fulfillment/order_cycle_supplier_totals_by_distributor.rb b/lib/reporting/reports/orders_and_fulfillment/order_cycle_supplier_totals_by_distributor.rb new file mode 100644 index 0000000000..4152731bcf --- /dev/null +++ b/lib/reporting/reports/orders_and_fulfillment/order_cycle_supplier_totals_by_distributor.rb @@ -0,0 +1,48 @@ +# frozen_string_literal: true + +module Reporting + module Reports + module OrdersAndFulfillment + class OrderCycleSupplierTotalsByDistributor < Base + def columns + { + producer: supplier_name, + product: product_name, + variant: variant_name, + hub: hub_name, + quantity: proc { |line_items| line_items.to_a.sum(&:quantity) }, + curr_cost_per_unit: proc { |line_items| line_items.first.price }, + total_cost: proc { |line_items| line_items.sum(&:amount) }, + shipping_method: proc { |line_items| line_items.first.order.shipping_method&.name } + } + end + + def rules + [ + { + group_by: :producer, + header: true, + }, + { + group_by: proc { |line_items, _row| line_items.first.variant } + }, + { + group_by: :hub, + summary_row: proc do |_key, _items, rows| + { + quantity: rows.sum(&:quantity), + total_cost: rows.sum(&:total_cost) + } + end, + } + ] + 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/packing/base.rb b/lib/reporting/reports/packing/base.rb index 7102d9d553..2513af0d22 100644 --- a/lib/reporting/reports/packing/base.rb +++ b/lib/reporting/reports/packing/base.rb @@ -3,15 +3,13 @@ module Reporting module Reports module Packing - class Base < ReportTemplate - SUBTYPES = ["customer", "supplier"] - - def primary_model - Spree::LineItem + class Base < ReportQueryTemplate + def message + I18n.t("spree.admin.reports.customer_names_message.customer_names_tip") end def report_query - Queries::QueryBuilder.new(primary_model, grouping_fields). + Queries::QueryBuilder.new(Spree::LineItem). scoped_to_orders(visible_orders_relation). scoped_to_line_items(ransacked_line_items_relation). with_managed_orders(managed_orders_relation). @@ -24,16 +22,58 @@ module Reporting joins_product_shipping_category. join_line_item_option_values. selecting(select_fields). - grouped_in_sets(group_sets). ordered_by(ordering_fields) end - def grouping_fields + def columns_format + { price: :currency, quantity: :quantity } + end + + def custom_headers + return {} if html_render? + + # Use non translated headers to avoid breaking changes + @custom_headers ||= report_data.columns.index_by(&:itself).symbolize_keys + end + + def default_params + # Prevent breaking change in this report by hidding new columns by default + { fields_to_hide: ["phone", "price"] } + end + + private + + def select_fields lambda do - [ - order_table[:id], - line_item_table[:id] - ] + { + hub: distributor_alias[:name], + customer_code: masked(customer_table[:code]), + last_name: masked(bill_address_alias[:lastname]), + first_name: masked(bill_address_alias[:firstname]), + phone: masked(bill_address_alias[:phone]), + supplier: supplier_alias[:name], + product: product_table[:name], + variant: variant_full_name, + quantity: line_item_table[:quantity], + price: (line_item_table[:quantity] * line_item_table[:price]), + temp_controlled: shipping_category_table[:temperature_controlled], + } + end + end + + def row_header(row) + result = "#{row.last_name} #{row.first_name}" + result += " (#{row.customer_code})" if row.customer_code + result += " - #{row.phone}" if row.phone + result + end + + def summary_row + proc do |_key, _items, rows| + { + quantity: rows.sum(&:quantity), + price: rows.sum(&:price) + } end end end diff --git a/lib/reporting/reports/packing/customer.rb b/lib/reporting/reports/packing/customer.rb index f12b83a3b5..8aa805829d 100644 --- a/lib/reporting/reports/packing/customer.rb +++ b/lib/reporting/reports/packing/customer.rb @@ -4,20 +4,26 @@ module Reporting module Reports module Packing class Customer < Base - def select_fields - lambda do + def columns + # Reorder default columns + super.slice(:hub, :customer_code, :first_name, :last_name, :phone, + :supplier, :product, :variant, :quantity, :price, :temp_controlled) + end + + def rules + [ { - hub: default_blank(distributor_alias[:name]), - customer_code: default_blank(masked(customer_table[:code])), - first_name: default_blank(masked(bill_address_alias[:firstname])), - last_name: default_blank(masked(bill_address_alias[:lastname])), - supplier: default_blank(supplier_alias[:name]), - product: default_string(product_table[:name], summary_row_title), - variant: default_blank(variant_full_name), - quantity: sum_values(line_item_table[:quantity]), - temp_controlled: boolean_blank(shipping_category_table[:temperature_controlled]), + group_by: :hub, + header: true, + header_class: "h1 with-background text-center", + }, + { + group_by: proc { |_item, row| row_header(row) }, + header: true, + fields_used_in_header: [:first_name, :last_name, :customer_code, :phone], + summary_row: summary_row, } - end + ] end def ordering_fields @@ -26,23 +32,12 @@ module Reporting distributor_alias[:name], bill_address_alias[:lastname], order_table[:id], - sql_grouping(grouping_fields), Arel.sql("supplier"), Arel.sql("product"), Arel.sql("variant"), ] end end - - def group_sets - lambda do - [ - distributor_alias[:name], - bill_address_alias[:lastname], - grouping_sets([parenthesise(order_table[:id]), parenthesise(grouping_fields)]) - ] - end - end end end end diff --git a/lib/reporting/reports/packing/product.rb b/lib/reporting/reports/packing/product.rb new file mode 100644 index 0000000000..83f6bd2e1b --- /dev/null +++ b/lib/reporting/reports/packing/product.rb @@ -0,0 +1,51 @@ +# frozen_string_literal: true + +module Reporting + module Reports + module Packing + class Product < Base + def columns + # Reorder default columns + super.slice(:hub, :supplier, :product, :variant, + :customer_code, :first_name, :last_name, :phone, + :quantity, :price, :temp_controlled) + end + + def rules + [ + { + group_by: :hub, + header: true, + header_class: "h1 with-background text-center", + }, + { + group_by: :supplier, + header: true, + header_class: "h1", + }, + { + group_by: proc { |_item, row| "#{row.product} - #{row.variant}" }, + header: true, + fields_used_in_header: [:product, :variant], + summary_row: summary_row, + header_class: "h3", + } + ] + end + + def ordering_fields + lambda do + [ + distributor_alias[:name], + Arel.sql("supplier"), + Arel.sql("product"), + Arel.sql("variant"), + bill_address_alias[:lastname], + order_table[:id], + ] + end + end + end + end + end +end diff --git a/lib/reporting/reports/packing/supplier.rb b/lib/reporting/reports/packing/supplier.rb index fa173afce8..c8ed63adba 100644 --- a/lib/reporting/reports/packing/supplier.rb +++ b/lib/reporting/reports/packing/supplier.rb @@ -4,30 +4,35 @@ module Reporting module Reports module Packing class Supplier < Base - def select_fields - lambda do - { - hub: default_blank(distributor_alias[:name]), - supplier: default_blank(supplier_alias[:name]), - customer_code: default_blank(customer_table[:code]), - first_name: default_blank(masked(bill_address_alias[:firstname])), - last_name: default_blank(masked(bill_address_alias[:lastname])), - product: default_string(product_table[:name], summary_row_title), - variant: default_blank(variant_full_name), - quantity: sum_values(line_item_table[:quantity]), - temp_controlled: boolean_blank(shipping_category_table[:temperature_controlled]), - } - end + def columns + # Reorder default columns + super.slice(:hub, :supplier, :customer_code, :first_name, :last_name, :phone, + :product, :variant, :quantity, :price, :temp_controlled) end - def group_sets - lambda do - [ - distributor_alias[:name], - supplier_alias[:name], - grouping_sets([parenthesise(supplier_alias[:name]), parenthesise(grouping_fields)]) - ] - end + def rules + [ + { + group_by: :hub, + header: true, + header_class: "h1 with-background text-center", + }, + { + group_by: :supplier, + header: true, + summary_row: summary_row, + summary_row_label: I18n.t('admin.reports.total_by_supplier').upcase + }, + { + group_by: proc { |_item, row| row_header(row) }, + header: true, + header_class: 'h4', + fields_used_in_header: [:first_name, :last_name, :customer_code, :phone], + summary_row: summary_row, + summary_row_class: "", + summary_row_label: I18n.t('admin.reports.total_by_customer') + } + ] end def ordering_fields @@ -35,7 +40,6 @@ module Reporting [ distributor_alias[:name], supplier_alias[:name], - sql_grouping(grouping_fields), Arel.sql("product"), Arel.sql("variant"), Arel.sql("last_name") diff --git a/lib/reporting/reports/payments/base.rb b/lib/reporting/reports/payments/base.rb new file mode 100644 index 0000000000..d2024ada6d --- /dev/null +++ b/lib/reporting/reports/payments/base.rb @@ -0,0 +1,17 @@ +# frozen_string_literal: true + +module Reporting + module Reports + module Payments + class Base < ReportTemplate + def search + Spree::Order.complete.not_state(:canceled).managed_by(@user).ransack(ransack_params) + end + + def query_result + search.result.group_by { |order| [order.payment_state, order.distributor] }.values + end + end + end + end +end diff --git a/lib/reporting/reports/payments/itemised_payment_totals.rb b/lib/reporting/reports/payments/itemised_payment_totals.rb new file mode 100644 index 0000000000..b571b2b97d --- /dev/null +++ b/lib/reporting/reports/payments/itemised_payment_totals.rb @@ -0,0 +1,22 @@ +# frozen_string_literal: true + +module Reporting + module Reports + module Payments + class ItemisedPaymentTotals < Base + def columns + { + payment_state: proc { |orders| orders.first.payment_state }, + distributor: proc { |orders| orders.first.distributor.name }, + product_total_price: proc { |orders| orders.to_a.sum(&:item_total) }, + shipping_total_price: proc { |orders| orders.sum(&:ship_total) }, + outstanding_balance_price: proc do |orders| + orders.sum { |order| order.outstanding_balance.to_f } + end, + total_price: proc { |orders| orders.map(&:total).sum } + } + end + end + end + end +end diff --git a/lib/reporting/reports/payments/payment_totals.rb b/lib/reporting/reports/payments/payment_totals.rb new file mode 100644 index 0000000000..f194aa2aa4 --- /dev/null +++ b/lib/reporting/reports/payments/payment_totals.rb @@ -0,0 +1,32 @@ +# frozen_string_literal: true + +module Reporting + module Reports + module Payments + class PaymentTotals < Base + def columns + { + payment_state: proc { |orders| orders.first.payment_state }, + distributor: proc { |orders| orders.first.distributor.name }, + product_total_price: proc { |orders| orders.to_a.sum(&:item_total) }, + shipping_total_price: proc { |orders| orders.sum(&:ship_total) }, + total_price: proc { |orders| orders.map(&:total).sum }, + eft_price: proc { |orders| total_by_payment_method(orders, "EFT") }, + paypal_price: proc { |orders| total_by_payment_method(orders, "PayPal") }, + outstanding_balance_price: proc { |orders| + orders.sum{ |order| order.outstanding_balance.to_f } + } + } + end + + private + + def total_by_payment_method(orders, pay_method) + orders.map(&:payments).flatten.select { |payment| + payment.completed? && payment.payment_method.name.to_s.include?(pay_method) + }.sum(&:amount) + end + end + end + end +end diff --git a/lib/reporting/reports/payments/payments_by_payment_type.rb b/lib/reporting/reports/payments/payments_by_payment_type.rb new file mode 100644 index 0000000000..766e0efa20 --- /dev/null +++ b/lib/reporting/reports/payments/payments_by_payment_type.rb @@ -0,0 +1,27 @@ +# frozen_string_literal: true + +module Reporting + module Reports + module Payments + class PaymentsByPaymentType < Base + def query_result + payments = search.result.includes(payments: :payment_method).map do |order| + order.payments.select(&:completed?) + end.flatten + payments.group_by { |payment| + [payment.order.payment_state, payment.order.distributor, payment.payment_method] + }.values + end + + def columns + { + payment_state: proc { |payments| payments.first.order.payment_state }, + distributor: proc { |payments| payments.first.order.distributor.name }, + payment_type: proc { |payments| payments.first.payment_method.name }, + total_price: proc { |payments| payments.sum(&:amount) } + } + end + end + end + end +end diff --git a/lib/reporting/reports/products_and_inventory/all_products.rb b/lib/reporting/reports/products_and_inventory/all_products.rb new file mode 100644 index 0000000000..f433f83287 --- /dev/null +++ b/lib/reporting/reports/products_and_inventory/all_products.rb @@ -0,0 +1,13 @@ +# frozen_string_literal: true + +module Reporting + module Reports + module ProductsAndInventory + class AllProducts < Base + def filter_on_hand(variants) + variants # do not filter + end + end + end + end +end diff --git a/lib/reporting/reports/products_and_inventory/base.rb b/lib/reporting/reports/products_and_inventory/base.rb new file mode 100644 index 0000000000..907c7f13f8 --- /dev/null +++ b/lib/reporting/reports/products_and_inventory/base.rb @@ -0,0 +1,95 @@ +# frozen_string_literal: true + +require 'open_food_network/scope_variant_to_hub' + +module Reporting + module Reports + module ProductsAndInventory + class Base < ReportTemplate + def query_result + filter(child_variants) + end + + # rubocop:disable Metrics/AbcSize + def columns + { + supplier: proc { |variant| variant.product.supplier.name }, + producer_suburb: proc { |variant| variant.product.supplier.address.city }, + product: proc { |variant| variant.product.name }, + product_properties: proc { |v| v.product.properties.map(&:name).join(", ") }, + taxons: proc { |variant| variant.product.taxons.map(&:name).join(", ") }, + variant_value: proc { |variant| variant.full_name }, + price: proc { |variant| variant.price }, + group_buy_unit_quantity: proc { |variant| variant.product.group_buy_unit_size }, + amount: proc { |_variant| "" }, + sku: proc { |variant| variant.sku.presence || variant.product.sku }, + } + end + # rubocop:enable Metrics/AbcSize + + def filter(variants) + filter_on_hand filter_to_distributor filter_to_order_cycle filter_to_supplier 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 + + private + + def report_type + params[:report_subtype] + end + + def visible_products + @visible_products ||= permissions.visible_products + end + + def permissions + @permissions ||= OpenFoodNetwork::Permissions.new(@user) + end + + # Using the `in_stock?` method allows overrides by distributors. + def filter_on_hand(variants) + variants.select(&:in_stock?) + 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/products_and_inventory/inventory.rb b/lib/reporting/reports/products_and_inventory/inventory.rb new file mode 100644 index 0000000000..b89fde6c32 --- /dev/null +++ b/lib/reporting/reports/products_and_inventory/inventory.rb @@ -0,0 +1,10 @@ +# frozen_string_literal: true + +module Reporting + module Reports + module ProductsAndInventory + class Inventory < Base + end + end + end +end diff --git a/lib/reporting/reports/products_and_inventory/lettuce_share.rb b/lib/reporting/reports/products_and_inventory/lettuce_share.rb new file mode 100644 index 0000000000..96100b1ecb --- /dev/null +++ b/lib/reporting/reports/products_and_inventory/lettuce_share.rb @@ -0,0 +1,79 @@ +# frozen_string_literal: true + +module Reporting + module Reports + module ProductsAndInventory + class LettuceShare < Base + # NOTE: These are NOT to be translated, they need to be in this exact format + # to work with LettucShare + def custom_headers + { + product: "PRODUCT", + description: "Description", + quantity: "Qty", + pack_size: "Pack Size", + unit: "Unit", + unit_price: "Unit Price", + total: "Total", + gst: "GST incl.", + grower: "Grower and growing method", + taxon: "Taxon" + } + end + + def columns + { + product: proc { |variant| variant.product.name }, + description: proc { |variant| variant.full_name }, + quantity: proc { |_variant| '' }, + pack_size: proc { |variant| VariantUnits::OptionValueNamer.new(variant).value }, + unit: proc { |variant| VariantUnits::OptionValueNamer.new(variant).unit }, + unit_price: proc { |variant| variant.price }, + total: proc { |_variant| '' }, + gst: proc { |variant| gst(variant) }, + grower: proc { |variant| grower_and_method(variant) }, + taxon: proc { |variant| variant.product.primary_taxon.name } + } + 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/sales_tax/base.rb b/lib/reporting/reports/sales_tax/base.rb new file mode 100644 index 0000000000..399ee86231 --- /dev/null +++ b/lib/reporting/reports/sales_tax/base.rb @@ -0,0 +1,35 @@ +# frozen_string_literal: true + +module Reporting + module Reports + module SalesTax + class Base < ReportTemplate + def search + permissions = ::Permissions::Order.new(user) + permissions.editable_orders.complete.not_state(:canceled).ransack(ransack_params) + end + + def query_result + search.result + end + + private + + def relevant_rates + @relevant_rates ||= Spree::TaxRate.distinct + end + + def order_number_column(order) + if html_render? + url = Spree::Core::Engine.routes.url_helpers.edit_admin_order_path(order.number) + <<-HTML + #{order.number} + HTML + else + order.number + end + end + end + end + end +end diff --git a/lib/reporting/reports/sales_tax/tax_rates.rb b/lib/reporting/reports/sales_tax/tax_rates.rb new file mode 100644 index 0000000000..83bd3f2eb0 --- /dev/null +++ b/lib/reporting/reports/sales_tax/tax_rates.rb @@ -0,0 +1,42 @@ +# frozen_string_literal: true + +module Reporting + module Reports + module SalesTax + class TaxRates < Base + # rubocop:disable Rails/OutputSafety + def columns + result = { + order_number: proc { |order| order_number_column(order).html_safe }, + total_excl_vat: proc { |order| order.total - order.total_tax } + } + add_key_for_each_rate(result, proc { |rate| + proc { |order| OrderTaxAdjustmentsFetcher.new(order).totals.fetch(rate, 0) } + }) + other = { + total_tax: proc { |order| order.total_tax }, + total_incl_vat: proc { |order| order.total } + } + result.merge(other) + end + # rubocop:enable Rails/OutputSafety + + def custom_headers + result = {} + add_key_for_each_rate(result, proc { |rate| + "%.1f%% (%s)" % [rate.amount.to_f * 100, currency_symbol] + }) + result + end + + private + + def add_key_for_each_rate(result, proc) + relevant_rates.each { |rate| + result["rate_#{rate.id}"] = proc.call(rate) + } + end + end + end + end +end diff --git a/lib/reporting/reports/sales_tax/tax_types.rb b/lib/reporting/reports/sales_tax/tax_types.rb new file mode 100644 index 0000000000..7c9eda9f02 --- /dev/null +++ b/lib/reporting/reports/sales_tax/tax_types.rb @@ -0,0 +1,61 @@ +# frozen_string_literal: true + +module Reporting + module Reports + module SalesTax + class TaxTypes < Base + # rubocop:disable Metrics/AbcSize + # rubocop:disable Rails/OutputSafety + def columns + { + order_number: proc { |order| order_number_column(order).html_safe }, + date: proc { |order| order.completed_at }, + items: proc { |order| totals_of(order)[:items] }, + items_total: proc { |order| totals_of(order)[:items_total] }, + taxable_items_total: proc { |order| totals_of(order)[:taxable_total] }, + sales_tax: proc { |order| totals_of(order)[:sales_tax] }, + delivery_charge: proc { |order| order.shipments.first&.cost || 0.0 }, + tax_on_delivery: proc { |order| order.shipping_tax }, + tax_on_fees: proc { |order| order.enterprise_fee_tax }, + total_tax: proc { |order| order.total_tax }, + customer: proc { |order| order.bill_address.full_name }, + distributor: proc { |order| order.distributor&.name }, + } + end + # rubocop:enable Metrics/AbcSize + # rubocop:enable Rails/OutputSafety + + private + + def totals_of(order) + @totals ||= {} + return @totals[order.id] if @totals[order.id].present? + + totals = { items: 0, items_total: 0.0, taxable_total: 0.0, sales_tax: 0.0 } + + order.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[order.id] = totals + 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/base.rb b/lib/reporting/reports/users_and_enterprises/base.rb new file mode 100644 index 0000000000..12ab4183a0 --- /dev/null +++ b/lib/reporting/reports/users_and_enterprises/base.rb @@ -0,0 +1,77 @@ +# frozen_string_literal: true + +module Reporting + module Reports + module UsersAndEnterprises + class Base < ReportTemplate + def query_result + sort(owners_and_enterprises.concat(managers_and_enterprises)) + end + + def columns + { + user: proc { |x| x.user_email }, + relationship: proc { |x| x.relationship_type }, + enterprise: proc { |x| x.name }, + is_producer: proc { |x| x.is_primary_producer }, + sells: proc { |x| x.sells }, + visible: proc { |x| x.visible }, + confirmation_date: proc { |x| x.created_at } + } + 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", + "'#{relationship_type}' AS relationship_type"]) + .to_a + 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 (?)", int_list.map(&:to_i)) + end + query + end + + def sort(results) + results.sort do |a, b| + a_date = (a.created_at || Date.new(1970, 1, 1)).in_time_zone + b_date = (b.created_at || Date.new(1970, 1, 1)).in_time_zone + [b_date, a.name, b.relationship_type, a.user_email] <=> + [a_date, b.name, a.relationship_type, b.user_email] + end + end + end + end + end +end diff --git a/lib/reporting/reports/xero_invoices/base.rb b/lib/reporting/reports/xero_invoices/base.rb new file mode 100644 index 0000000000..5ece80fc9f --- /dev/null +++ b/lib/reporting/reports/xero_invoices/base.rb @@ -0,0 +1,253 @@ +# frozen_string_literal: true + +module Reporting + module Reports + module XeroInvoices + class Base < ReportTemplate + def xero_columns + # 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 custom_headers + xero_columns.index_by(&:to_sym) + end + + # This report calculate data in a specific way, so instead of refactoring it + # we just encapsulate the result in the columns method + def columns + result = {} + xero_columns.each_with_index do |header, id| + result[header.to_sym] = proc { |row| row[id] } + end + result + end + + def default_params + { + report_subtype: 'summary', + invoice_date: Time.zone.today, + due_date: Time.zone.today + 1.month, + account_code: 'food sales' + } + end + + def search + permissions = ::Permissions::Order.new(@user) + permissions.editable_orders.complete.not_state(:canceled).ransack(ransack_params) + end + + # In the new way of managing reports, query_result should be an ActiveRecordRelation + # Here we directly transform the ActiveRecordRelation into table_rows without using the + # new ReportGrouper, so we can keep the old report without refactoring it + def query_result + search_result = search.result.reorder('id DESC') + + rows = [] + + search_result.each_with_index do |order, i| + invoice_number = invoice_number_for(order, i) + rows += detail_rows_for_order(order, invoice_number, params) if detail? + rows += summary_rows_for_order(order, invoice_number, params) + end + + rows.compact + end + + private + + 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 + + # rubocop:disable Metrics/AbcSize + 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.to_s, + order.bill_address&.zipcode, + order.bill_address&.country&.name, + invoice_number, + order.number, + opts[:invoice_date].to_date.to_s, + opts[:due_date].to_date.to_s, + sku, + description, + quantity, + amount, + '', + opts[:account_code], + tax_type, + '', + '', + '', + '', + Spree::Config.currency, + '', + order.paid? ? I18n.t(:y) : I18n.t(:n)] + end + # rubocop:enable Metrics/AbcSize + + 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) + if params[:initial_invoice_number].present? + params[:initial_invoice_number].to_i + idx + else + order.number + end + 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? + params[: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/lib/reporting/reports/xero_invoices/detailed.rb b/lib/reporting/reports/xero_invoices/detailed.rb new file mode 100644 index 0000000000..239144d46b --- /dev/null +++ b/lib/reporting/reports/xero_invoices/detailed.rb @@ -0,0 +1,10 @@ +# frozen_string_literal: true + +module Reporting + module Reports + module XeroInvoices + class Detailed < Base + end + end + end +end diff --git a/lib/reporting/reports/xero_invoices/summary.rb b/lib/reporting/reports/xero_invoices/summary.rb new file mode 100644 index 0000000000..6a45aa2149 --- /dev/null +++ b/lib/reporting/reports/xero_invoices/summary.rb @@ -0,0 +1,10 @@ +# frozen_string_literal: true + +module Reporting + module Reports + module XeroInvoices + class Summary < Base + end + end + end +end diff --git a/spec/controllers/spree/admin/reports_controller_spec.rb b/spec/controllers/admin/reports_controller_spec.rb similarity index 66% rename from spec/controllers/spree/admin/reports_controller_spec.rb rename to spec/controllers/admin/reports_controller_spec.rb index 2f9edf9de7..c306451f95 100644 --- a/spec/controllers/spree/admin/reports_controller_spec.rb +++ b/spec/controllers/admin/reports_controller_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' -describe Spree::Admin::ReportsController, type: :controller do +describe Admin::ReportsController, type: :controller do # Given two distributors and two suppliers let(:bill_address) { create(:address) } let(:ship_address) { create(:address) } @@ -21,18 +21,23 @@ describe Spree::Admin::ReportsController, type: :controller do # Given two order cycles with both distributors let(:ocA) { - create(:simple_order_cycle, coordinator: coordinator1, distributors: [distributor1, distributor2], - suppliers: [supplier1, supplier2, supplier3], variants: [product1.master, product3.master]) + create(:simple_order_cycle, coordinator: coordinator1, + distributors: [distributor1, distributor2], + suppliers: [supplier1, supplier2, supplier3], + variants: [product1.master, product3.master]) } let(:ocB) { - create(:simple_order_cycle, coordinator: coordinator2, distributors: [distributor1, distributor2], - suppliers: [supplier1, supplier2, supplier3], variants: [product2.master]) + create(:simple_order_cycle, coordinator: coordinator2, + distributors: [distributor1, distributor2], + suppliers: [supplier1, supplier2, supplier3], + variants: [product2.master]) } # orderA1 can only be accessed by supplier1, supplier3 and distributor1 let(:orderA1) do order = create(:order, distributor: distributor1, bill_address: bill_address, - ship_address: ship_address, special_instructions: instructions, order_cycle: ocA) + ship_address: ship_address, special_instructions: instructions, + order_cycle: ocA) order.line_items << create(:line_item, variant: product1.master) order.line_items << create(:line_item, variant: product3.master) order.finalize! @@ -42,7 +47,8 @@ describe Spree::Admin::ReportsController, type: :controller do # orderA2 can only be accessed by supplier2 and distributor2 let(:orderA2) do order = create(:order, distributor: distributor2, bill_address: bill_address, - ship_address: ship_address, special_instructions: instructions, order_cycle: ocA) + ship_address: ship_address, special_instructions: instructions, + order_cycle: ocA) order.line_items << create(:line_item, variant: product2.master) order.finalize! order.save @@ -51,7 +57,8 @@ describe Spree::Admin::ReportsController, type: :controller do # orderB1 can only be accessed by supplier1, supplier3 and distributor1 let(:orderB1) do order = create(:order, distributor: distributor1, bill_address: bill_address, - ship_address: ship_address, special_instructions: instructions, order_cycle: ocB) + ship_address: ship_address, special_instructions: instructions, + order_cycle: ocB) order.line_items << create(:line_item, variant: product1.master) order.line_items << create(:line_item, variant: product3.master) order.finalize! @@ -61,7 +68,8 @@ describe Spree::Admin::ReportsController, type: :controller do # orderB2 can only be accessed by supplier2 and distributor2 let(:orderB2) do order = create(:order, distributor: distributor2, bill_address: bill_address, - ship_address: ship_address, special_instructions: instructions, order_cycle: ocB) + ship_address: ship_address, special_instructions: instructions, + order_cycle: ocB) order.line_items << create(:line_item, variant: product2.master) order.finalize! order.save @@ -70,8 +78,9 @@ describe Spree::Admin::ReportsController, type: :controller do # Results let(:resulting_orders_prelim) { assigns(:report).search.result } - let(:resulting_orders) { assigns(:report).table_items.map(&:order) } - let(:resulting_products) { assigns(:report).table_items.map(&:product) } + let(:resulting_line_items) { assigns(:report).query_result.flatten } + let(:resulting_orders) { resulting_line_items.map(&:order).uniq } + let(:resulting_products) { resulting_line_items.map(&:product).uniq } # As manager of a coordinator (coordinator1) context "Coordinator Enterprise User" do @@ -81,8 +90,7 @@ describe Spree::Admin::ReportsController, type: :controller do describe 'Orders & Fulfillment' do it "shows all orders in order cycles I coordinate" do - spree_post :orders_and_fulfillment, q: {} - + spree_post :show, report_type: :orders_and_fulfillment, q: {} expect(resulting_orders).to include orderA1, orderA2 expect(resulting_orders).not_to include orderB1, orderB2 end @@ -97,11 +105,11 @@ describe Spree::Admin::ReportsController, type: :controller do let!(:present_objects) { [orderA1, orderA2, orderB1, orderB2] } it "only shows orders that I have access to" do - spree_post :orders_and_distributors + spree_post :show, report_type: :orders_and_distributors - expect(assigns(:search).result).to include(orderA1, orderB1) - expect(assigns(:search).result).not_to include(orderA2) - expect(assigns(:search).result).not_to include(orderB2) + expect(assigns(:report).search.result).to include(orderA1, orderB1) + expect(assigns(:report).search.result).not_to include(orderA2) + expect(assigns(:report).search.result).not_to include(orderB2) end end @@ -109,7 +117,7 @@ describe Spree::Admin::ReportsController, type: :controller do let!(:present_objects) { [orderA1, orderA2, orderB1, orderB2] } it "only shows orders that I have access to" do - spree_post :payments + spree_post :show, report_type: :payments expect(resulting_orders_prelim).to include(orderA1, orderB1) expect(resulting_orders_prelim).not_to include(orderA2) @@ -122,7 +130,7 @@ describe Spree::Admin::ReportsController, type: :controller do let!(:present_objects) { [orderA1, orderA2, orderB1, orderB2] } it "only shows orders that I distribute" do - spree_post :orders_and_fulfillment, q: {} + spree_post :show, report_type: :orders_and_fulfillment, q: {} expect(resulting_orders).to include orderA1, orderB1 expect(resulting_orders).not_to include orderA2, orderB2 @@ -133,7 +141,8 @@ describe Spree::Admin::ReportsController, type: :controller do let!(:present_objects) { [orderA1, orderB1] } it "only shows the selected order cycle" do - spree_post :orders_and_fulfillment, q: { order_cycle_id_in: [ocA.id.to_s] } + spree_post :show, report_type: :orders_and_fulfillment, + q: { order_cycle_id_in: [ocA.id.to_s] } expect(resulting_orders).to include(orderA1) expect(resulting_orders).not_to include(orderB1) @@ -151,7 +160,8 @@ describe Spree::Admin::ReportsController, type: :controller do spree_get :index report_types = assigns(:reports).keys - expect(report_types).to include :orders_and_fulfillment, :products_and_inventory, :packing # and others + expect(report_types).to include :orders_and_fulfillment, + :products_and_inventory, :packing # and others expect(report_types).to_not include :sales_tax end end @@ -166,14 +176,14 @@ describe Spree::Admin::ReportsController, type: :controller do end it "only shows product line items that I am supplying" do - spree_post :orders_and_fulfillment, q: {} + spree_post :show, report_type: :orders_and_fulfillment, q: {} expect(resulting_products).to include product1 expect(resulting_products).not_to include product2, product3 end it "only shows the selected order cycle" do - spree_post :orders_and_fulfillment, q: { order_cycle_id_eq: ocA.id } + spree_post :show, report_type: :orders_and_fulfillment, q: { order_cycle_id_eq: ocA.id } expect(resulting_orders_prelim).to include(orderA1) expect(resulting_orders_prelim).not_to include(orderB1) @@ -183,10 +193,9 @@ describe Spree::Admin::ReportsController, type: :controller do before { orderA1.line_items.first.product.destroy } it "only shows product line items that I am supplying" do - spree_post :orders_and_fulfillment, q: {} + spree_post :show, report_type: :orders_and_fulfillment, q: {} - table_items = assigns(:report).table_items - variant = Spree::Variant.unscoped.find(table_items.first.variant_id) + variant = Spree::Variant.unscoped.find(resulting_line_items.first.variant_id) expect(variant.product).to eq(product1) end @@ -195,7 +204,7 @@ describe Spree::Admin::ReportsController, type: :controller do context "where I have not granted P-OC to the distributor" do it "does not show me line_items I supply" do - spree_post :orders_and_fulfillment + spree_post :show, report_type: :orders_and_fulfillment expect(resulting_products).not_to include product1, product2, product3 end @@ -212,13 +221,13 @@ describe Spree::Admin::ReportsController, type: :controller do let!(:present_objects) { [distributors, suppliers] } it "should build distributors for the current user" do - spree_get :products_and_inventory - expect(assigns(:distributors)).to match_array distributors + spree_get :show, report_type: :products_and_inventory + expect(assigns(:data).distributors).to match_array distributors end it "builds suppliers for the current user" do - spree_get :products_and_inventory - expect(assigns(:suppliers)).to match_array suppliers + spree_get :show, report_type: :products_and_inventory + expect(assigns(:data).suppliers).to match_array suppliers end end @@ -226,25 +235,22 @@ describe Spree::Admin::ReportsController, type: :controller do let!(:order_cycles) { [ocA, ocB] } it "builds order cycles for the current user" do - spree_get :products_and_inventory - expect(assigns(:order_cycles)).to match_array order_cycles + spree_get :show, report_type: :products_and_inventory + expect(assigns(:data).order_cycles).to match_array order_cycles end end it "assigns report types" do - spree_get :products_and_inventory - expect(assigns(:report_types)).to eq(subject.report_types[:products_and_inventory]) + spree_get :show, report_type: :products_and_inventory + expect(assigns(:report_subtypes)).to eq(subject.reports[:products_and_inventory]) end it "creates a ProductAndInventoryReport" do - expect(OpenFoodNetwork::ProductsAndInventoryReport).to receive(:new) - .with(@admin_user, - { "test" => "foo", "controller" => "spree/admin/reports", "report" => {}, - "action" => "products_and_inventory", "use_route" => "main_app" }, false) + allow(Reporting::Reports::ProductsAndInventory::Base).to receive(:new) .and_return(report = double(:report)) - allow(report).to receive(:header).and_return [] - allow(report).to receive(:table).and_return [] - spree_get :products_and_inventory, test: "foo" + allow(report).to receive(:table_headers).and_return [] + allow(report).to receive(:table_rows).and_return [] + spree_get :show, report_type: :products_and_inventory, test: "foo" expect(assigns(:report)).to eq(report) end end @@ -253,10 +259,10 @@ describe Spree::Admin::ReportsController, type: :controller do before { controller_login_as_admin } it "should have report types for customers" do - expect(subject.report_types[:customers]).to eq([ - ["Mailing List", :mailing_list], - ["Addresses", :addresses] - ]) + expect(subject.reports[:customers]).to eq([ + ["Mailing List", :mailing_list], + ["Addresses", :addresses] + ]) end context "with distributors and suppliers" do @@ -265,13 +271,13 @@ describe Spree::Admin::ReportsController, type: :controller do let!(:present_objects) { [distributors, suppliers] } it "should build distributors for the current user" do - spree_get :customers - expect(assigns(:distributors)).to match_array distributors + spree_get :show, report_type: :customers + expect(assigns(:data).distributors).to match_array distributors end it "builds suppliers for the current user" do - spree_get :customers - expect(assigns(:suppliers)).to match_array suppliers + spree_get :show, report_type: :customers + expect(assigns(:data).suppliers).to match_array suppliers end end @@ -279,25 +285,22 @@ describe Spree::Admin::ReportsController, type: :controller do let!(:order_cycles) { [ocA, ocB] } it "builds order cycles for the current user" do - spree_get :customers - expect(assigns(:order_cycles)).to match_array order_cycles + spree_get :show, report_type: :customers + expect(assigns(:data).order_cycles).to match_array order_cycles end end it "assigns report types" do - spree_get :customers - expect(assigns(:report_types)).to eq(subject.report_types[:customers]) + spree_get :show, report_type: :customers + expect(assigns(:report_subtypes)).to eq(subject.reports[:customers]) end - it "creates a CustomersReport" do - expect(OpenFoodNetwork::CustomersReport).to receive(:new) - .with(@admin_user, { "test" => "foo", "controller" => "spree/admin/reports", - "action" => "customers", "use_route" => "main_app", - "report" => {} }, false) + it "creates a report object" do + allow(Reporting::Reports::Customers::Base).to receive(:new) .and_return(report = double(:report)) - allow(report).to receive(:header).and_return [] - allow(report).to receive(:table).and_return [] - spree_get :customers, test: "foo" + allow(report).to receive(:table_headers).and_return [] + allow(report).to receive(:table_rows).and_return [] + spree_get :show, report_type: :customers, test: "foo" expect(assigns(:report)).to eq(report) end end @@ -310,10 +313,11 @@ describe Spree::Admin::ReportsController, type: :controller do end it 'renders the delivery report' do - spree_post :order_cycle_management, { + spree_post :show, { q: { completed_at_lt: 1.day.ago }, shipping_method_in: ["123"], # We just need to search for shipping methods - report_type: "delivery", + report_type: :order_cycle_management, + report_subtype: "delivery", } expect(response).to have_http_status(:ok) @@ -327,20 +331,20 @@ describe Spree::Admin::ReportsController, type: :controller do let!(:present_objects) { [coordinator1] } it "shows report search forms" do - spree_get :users_and_enterprises - expect(assigns(:report).table).to eq [] + spree_get :show, report_type: :users_and_enterprises + expect(response).to have_http_status(:ok) end it "shows report data" do - spree_post :users_and_enterprises, q: {} - expect(assigns(:report).table.empty?).to be false + spree_post :show, report_type: :users_and_enterprises, q: {} + expect(assigns(:report).table_rows.empty?).to be false end end describe "sales_tax" do it "shows report search forms" do - spree_get :sales_tax - expect(assigns(:report).table).to eq [] + spree_get :show, report_type: :sales_tax + expect(response).to have_http_status(:ok) end end end diff --git a/spec/controllers/api/v0/reports/packing_report_spec.rb b/spec/controllers/api/v0/reports/packing_report_spec.rb index 4becf70c79..971b3d5832 100644 --- a/spec/controllers/api/v0/reports/packing_report_spec.rb +++ b/spec/controllers/api/v0/reports/packing_report_spec.rb @@ -6,6 +6,9 @@ describe Api::V0::ReportsController, type: :controller do let(:params) { { report_type: 'packing', + # rspec seems to remove empty values to setting something dummy so the + # default_params will not overwritting this params + fields_to_hide: [:none], q: { order_created_at_lt: Time.zone.now } } } @@ -56,7 +59,7 @@ describe Api::V0::ReportsController, type: :controller do results << __send__("#{user_type}_report_row", line_item) end - results << summary_row(order) + results end def distributor_report_row(line_item) @@ -69,8 +72,9 @@ describe Api::V0::ReportsController, type: :controller do "product" => line_item.product.name, "variant" => line_item.full_name, "quantity" => line_item.quantity, - "temp_controlled" => - line_item.product.shipping_category&.temperature_controlled ? I18n.t(:yes) : I18n.t(:no) + "price" => (line_item.quantity * line_item.price).to_s, + "phone" => line_item.order.bill_address.phone, + "temp_controlled" => line_item.product.shipping_category&.temperature_controlled } end @@ -80,26 +84,13 @@ describe Api::V0::ReportsController, type: :controller do "customer_code" => I18n.t("hidden_field", scope: i18n_scope), "first_name" => I18n.t("hidden_field", scope: i18n_scope), "last_name" => I18n.t("hidden_field", scope: i18n_scope), + "phone" => I18n.t("hidden_field", scope: i18n_scope), "supplier" => line_item.product.supplier.name, "product" => line_item.product.name, "variant" => line_item.full_name, "quantity" => line_item.quantity, - "temp_controlled" => - line_item.product.shipping_category&.temperature_controlled ? I18n.t(:yes) : I18n.t(:no) - } - end - - def summary_row(order) - { - "hub" => "", - "customer_code" => "", - "first_name" => "", - "last_name" => "", - "supplier" => "", - "product" => I18n.t("total_items", scope: i18n_scope), - "variant" => "", - "quantity" => order.line_items.sum(&:quantity), - "temp_controlled" => "", + "price" => (line_item.quantity * line_item.price).to_s, + "temp_controlled" => line_item.product.shipping_category&.temperature_controlled } end diff --git a/spec/controllers/api/v0/reports_controller_spec.rb b/spec/controllers/api/v0/reports_controller_spec.rb index 23ddfabf21..09ba607f4b 100644 --- a/spec/controllers/api/v0/reports_controller_spec.rb +++ b/spec/controllers/api/v0/reports_controller_spec.rb @@ -63,7 +63,6 @@ describe Api::V0::ReportsController, type: :controller do it "returns an error" do api_get :show, report_type: "packing" - expect(response.status).to eq 422 expect(json_response["error"]).to eq( I18n.t('errors.missing_ransack_params', scope: i18n_scope) diff --git a/spec/helpers/spree/admin/reports_helper_spec.rb b/spec/helpers/admin/reports_helper_spec.rb similarity index 94% rename from spec/helpers/spree/admin/reports_helper_spec.rb rename to spec/helpers/admin/reports_helper_spec.rb index a4460e2b4a..9ea424bc48 100644 --- a/spec/helpers/spree/admin/reports_helper_spec.rb +++ b/spec/helpers/admin/reports_helper_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' -describe Spree::ReportsHelper, type: :helper do +describe ReportsHelper, type: :helper do describe "#report_payment_method_options" do let(:order_with_payments) { create(:order_ready_to_ship) } let(:order_without_payments) { create(:order_with_line_items) } diff --git a/spec/helpers/navigation_helper_spec.rb b/spec/helpers/navigation_helper_spec.rb index 90199a1fe6..ee01b3204a 100644 --- a/spec/helpers/navigation_helper_spec.rb +++ b/spec/helpers/navigation_helper_spec.rb @@ -14,8 +14,8 @@ module Spree expect(helper.klass_for('lions')).to eq(:lion) end - it "returns Spree::Admin::ReportsController for reports" do - expect(helper.klass_for('reports')).to eq(Spree::Admin::ReportsController) + it "returns Admin::ReportsController for reports" do + expect(helper.klass_for('reports')).to eq(::Admin::ReportsController) end it "returns :overview for the dashboard" do 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 f1df21332f..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_type: "mailing_list") - end - - it "returns headers for mailing_list" do - expect(subject.header).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).to eq([[ - "test@test.com", "Firsty", "Lasty", "Suburbia" - ]]) - end - end - - describe "addresses report" do - before do - allow(subject).to receive(:params).and_return(report_type: "addresses") - end - - it "returns headers for addresses" do - expect(subject.header).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).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/group_buy_report_spec.rb b/spec/lib/open_food_network/group_buy_report_spec.rb deleted file mode 100644 index cbb633f6aa..0000000000 --- a/spec/lib/open_food_network/group_buy_report_spec.rb +++ /dev/null @@ -1,105 +0,0 @@ -# frozen_string_literal: true - -require 'spec_helper' -require 'open_food_network/group_buy_report' - -module OpenFoodNetwork - describe GroupBuyReport do - before(:each) do - @orders = [] - bill_address = create(:address) - distributor_address = create(:address, address1: "distributor address", city: 'The Shire', - zipcode: "1234") - distributor = create(:distributor_enterprise, address: distributor_address) - - @supplier1 = create(:supplier_enterprise) - @variant1 = create(:variant) - @variant1.product.supplier = @supplier1 - @variant1.product.save! - @variant1.reload - - shipping_instructions = "pick up on thursday please!" - - order1 = create(:order, distributor: distributor, bill_address: bill_address, - special_instructions: shipping_instructions) - line_item11 = create(:line_item, variant: @variant1, order: order1) - @orders << order1.reload - - order2 = create(:order, distributor: distributor, bill_address: bill_address, - special_instructions: shipping_instructions) - line_item21 = create(:line_item, variant: @variant1, order: order2) - - @variant2 = create(:variant) - @variant2.product.supplier = @supplier1 - @variant2.product.save! - - line_item22 = create(:line_item, variant: @variant2, order: order2) - @orders << order2.reload - - @supplier2 = create(:supplier_enterprise) - @variant3 = create(:variant, weight: nil) - @variant3.product.supplier = @supplier2 - @variant3.product.save! - - order3 = create(:order, distributor: distributor, bill_address: bill_address, - special_instructions: shipping_instructions) - line_item31 = create(:line_item, variant: @variant3, order: order3) - @orders << order3.reload - end - - it "should return a header row describing the report" do - subject = GroupBuyReport.new [@order1] - header = subject.header - expect(header).to eq(["Supplier", "Product", "Unit Size", "Variant", "Weight", - "Total Ordered", "Total Max"]) - end - - it "should provide the required variant and quantity information in a table" do - subject = GroupBuyReport.new @orders - - table = subject.table - - line_items = @orders.map(&:line_items).flatten.select{ |li| - li.product.supplier == @supplier1 && li.variant == @variant1 - } - - sum_quantities = line_items.map(&:quantity).sum - sum_max_quantities = line_items.map { |li| li.max_quantity || 0 }.sum - - expect(table[0]).to eq([@variant1.product.supplier.name, @variant1.product.name, "UNITSIZE", - @variant1.options_text, @variant1.weight, sum_quantities, sum_max_quantities]) - end - - it "should return a table wherein each rows contains the same number of columns as the heading" do - subject = GroupBuyReport.new @orders - - table = subject.table - columns = subject.header.length - - table.each do |r| - expect(r.length).to eq(columns) - end - end - - it "should split and group line items from multiple suppliers and of multiple variants" do - subject = GroupBuyReport.new @orders - - table_row_objects = subject.variants_and_quantities - - variant_rows = table_row_objects.select { |r| - r.instance_of?(OpenFoodNetwork::GroupBuyVariantRow) - } - product_rows = table_row_objects.select { |r| - r.instance_of?(OpenFoodNetwork::GroupBuyProductRow) - } - - supplier_groups = variant_rows.group_by { |r| r.variant.product.supplier } - variant_groups = variant_rows.group_by(&:variant) - product_groups = product_rows.group_by(&:product) - - expect(supplier_groups.length).to eq(2) - expect(variant_groups.length).to eq(3) - expect(product_groups.length).to eq(3) - 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 7f32110299..0000000000 --- a/spec/lib/open_food_network/lettuce_share_report_spec.rb +++ /dev/null @@ -1,85 +0,0 @@ -# frozen_string_literal: true - -require 'spec_helper' - -require 'open_food_network/lettuce_share_report' - -module OpenFoodNetwork - describe LettuceShareReport do - let(:user) { create(:user) } - let(:report) { LettuceShareReport.new user, {}, true } - 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).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(report).to receive(:child_variants) { - Spree::Variant.where(id: [variant, variant2, variant3]) - } - expect(report.table.count).to eq 3 - end - - it "only available items" do - variant.on_hand = 0 - allow(report).to receive(:child_variants) { - Spree::Variant.where(id: [variant, variant2, variant3, variant4]) - } - expect(report.table.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(report).to receive(:child_variants) { - Spree::Variant.where(id: [variant, variant2, variant3]) - } - allow(report).to receive(:params) { { distributor_id: hub.id } } - rows = report.table - 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_and_distributor_report_spec.rb b/spec/lib/open_food_network/order_and_distributor_report_spec.rb deleted file mode 100644 index 80bc46f913..0000000000 --- a/spec/lib/open_food_network/order_and_distributor_report_spec.rb +++ /dev/null @@ -1,88 +0,0 @@ -# frozen_string_literal: true - -require 'spec_helper' -require 'open_food_network/order_and_distributor_report' - -module OpenFoodNetwork - describe OrderAndDistributorReport do - describe 'orders and distributors report' do - it 'should return a header row describing the report' do - subject = OrderAndDistributorReport.new nil - - header = subject.header - expect(header).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 = OrderAndDistributorReport.new create(:admin_user), {}, true - - table = subject.table - - 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 = OrderAndDistributorReport.new(create(:admin_user), {}, true) - - table = subject.table - expect(table.size).to eq 2 - 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 435b799ceb..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_items' 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_type: '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_items).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_items).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/order_grouper_spec.rb b/spec/lib/open_food_network/order_grouper_spec.rb deleted file mode 100644 index db6538efec..0000000000 --- a/spec/lib/open_food_network/order_grouper_spec.rb +++ /dev/null @@ -1,176 +0,0 @@ -# frozen_string_literal: true - -require 'spec_helper' -require 'open_food_network/order_grouper' - -module OpenFoodNetwork - describe OrderGrouper do - before(:each) do - @items = [1, 2, 3, 4] - end - - context "constructing the table" do - it "should build a tree then build a table" do - rules = [{ group_by: proc { |sentence| - sentence.paragraph.chapter - }, sort_by: proc { |chapter| - chapter.name - }, summary_columns: [proc { |is| - is.first.paragraph.chapter.name - }, proc { |_is| - "TOTAL" - }, proc { |_is| - "" - }, proc { |is| - is.sum(&:property1) - }] }, - { group_by: proc { |sentence| sentence.paragraph }, sort_by: proc { |paragraph| - paragraph.name - } }] - columns = [proc { |is| is.first.paragraph.chapter.name }, proc { |is| - is.first.paragraph.name - }, proc { |is| - is.first.name - }, proc { |is| - is.sum(&:property1) - }] - - subject = OrderGrouper.new rules, columns - - tree = double(:tree) - expect(subject).to receive(:build_tree).with(@items, rules).and_return(tree) - expect(subject).to receive(:build_table).with(tree) - - subject.table(@items) - end - end - - context "grouping items without rules" do - it "returns the original array when no rules are provided" do - rules = [] - column1 = double(:col1) - column2 = double(:col2) - columns = [column1, column2] - subject = OrderGrouper.new rules, columns - - expect(rules).to receive(:clone).and_return(rules) - expect(subject.build_tree(@items, rules)).to eq(@items) - end - end - - context "grouping items with rules" do - before(:each) do - @rule1 = double(:rule1) - rule2 = double(:rule2) - @rules = [@rule1, rule2] - @remaining_rules = [rule2] - column1 = double(:col1) - column2 = double(:col2) - @columns = [column1, column2] - end - - it "builds branches by removing a rule from 'rules' and running group_and_sort" do - subject = OrderGrouper.new @rules, @columns - - expect(@rules).to receive(:clone).and_return(@rules) - expect(@rules).to receive(:delete_at).with(0) - grouped_tree = double(:grouped_tree) - expect(subject).to receive(:group_and_sort).and_return(grouped_tree) - - expect(subject.build_tree(@items, @rules)).to eq(grouped_tree) - end - - it "separates the first rule from rules before sending to group_and_sort" do - subject = OrderGrouper.new @rules, @columns - - grouped_tree = double(:grouped_tree) - expect(subject).to receive(:group_and_sort).with(@rule1, @rules[1..-1], - @items).and_return(grouped_tree) - - expect(subject.build_tree(@items, @rules)).to eq(grouped_tree) - end - - it "should group, then sort, send each group to build_tree, and return a branch" do - summary_columns_object = double(:summary_columns) - allow(@rule1).to receive(:[]).with(:summary_columns) { summary_columns_object } - - subject = OrderGrouper.new @rules, @columns - - number_of_categories = 3 - groups = double(:groups) - 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 ) - } - expect(groups).to receive(:sort_by).and_return(sorted_groups) - group = { group1: 1, group2: 2, group3: 3 } - expect(subject).to receive(:build_tree).exactly(number_of_categories).times.and_return(group) - - group_tree = {} - 1.upto(number_of_categories) { |i| group_tree[i] = group } - 1.upto(number_of_categories) { |i| group_tree[i][:summary_row] = summary_columns_object } - expect(subject.group_and_sort(@rule1, @remaining_rules, @items)).to eq(group_tree) - end - end - - context "building the table Array" do - before(:each) do - rule1 = double(:rule1) - rule2 = double(:rule2) - @rules = [rule1, rule2] - @column1 = double(:col1, call: "Column1") - @column2 = double(:col2, call: "Column2") - @columns = [@column1, @column2] - - sumcol1 = double(:sumcol1, call: "SumColumn1") - sumcol2 = double(:sumcol2, call: "SumColumn2") - @sumcols = [sumcol1, sumcol2] - - item1 = double(:item1) - item2 = double(:item2) - item3 = double(:item3) - @items1 = [item1, item2] - @items2 = [item2, item3] - @items3 = [item3, item1] - end - it "should return columns when given an Array" do - subject = OrderGrouper.new @rules, @columns - - expect(@column1).to receive(:call) - expect(@column2).to receive(:call) - - expect(subject.build_table(@items1)).to eq([["Column1", "Column2"]]) - end - - it "should return a row for each key-value pair when given a Hash" do - groups = { items1: @items1, items2: @items2, items3: @items3 } - - subject = OrderGrouper.new @rules, @columns - - # subject.should_receive(:build_table).exactly(2).times - - expected_return = [] - groups.length.times { expected_return << ["Column1", "Column2"] } - expect(subject.build_table(groups)).to eq(expected_return) - end - - it "should return an extra row when a :summary_row key appears in a given Hash" do - groups = { items1: @items1, items2: @items2, items3: @items3, - summary_row: { items: { items2: @items2, items3: @items3 }, columns: @sumcols } } - - subject = OrderGrouper.new @rules, @columns - - expected_return = [] - groups.each do |key, _group| - expected_return << if key == :summary_row - ["SumColumn1", "SumColumn2"] - else - ["Column1", "Column2"] - end - end - expect(subject.build_table(groups)).to eq(expected_return) - end - end - end -end diff --git a/spec/lib/open_food_network/orders_and_fulfillments_report/customer_totals_report_spec.rb b/spec/lib/open_food_network/orders_and_fulfillments_report/customer_totals_report_spec.rb deleted file mode 100644 index 8f4a7f6a21..0000000000 --- a/spec/lib/open_food_network/orders_and_fulfillments_report/customer_totals_report_spec.rb +++ /dev/null @@ -1,128 +0,0 @@ -# frozen_string_literal: true - -require "spec_helper" -require 'open_food_network/orders_and_fulfillments_report' -require 'open_food_network/orders_and_fulfillments_report/customer_totals_report' -require 'open_food_network/order_grouper' - -RSpec.describe OpenFoodNetwork::OrdersAndFulfillmentsReport::CustomerTotalsReport do - let!(:distributor) { create(:distributor_enterprise) } - let!(:customer) { create(:customer, enterprise: distributor) } - let(:current_user) { distributor.owner } - - let(:report) do - report_options = { report_type: described_class::REPORT_TYPE } - OpenFoodNetwork::OrdersAndFulfillmentsReport.new(current_user, report_options, true) - end - - let(:report_table) do - OpenFoodNetwork::OrderGrouper.new(report.rules, report.columns).table(report.table_items) - 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_fulfillments_report/distributor_totals_by_supplier_report_spec.rb b/spec/lib/open_food_network/orders_and_fulfillments_report/distributor_totals_by_supplier_report_spec.rb deleted file mode 100644 index 7c5975eb7a..0000000000 --- a/spec/lib/open_food_network/orders_and_fulfillments_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_fulfillments_report/distributor_totals_by_supplier_report' - -RSpec.describe OpenFoodNetwork::OrdersAndFulfillmentsReport::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_type: described_class::REPORT_TYPE } - OpenFoodNetwork::OrdersAndFulfillmentsReport.new(current_user, report_options, true) - end - - let(:report_table) do - OpenFoodNetwork::OrderGrouper.new(report.rules, report.columns).table(report.table_items) - 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_fulfillments_report/supplier_totals_by_distributor_report_spec.rb b/spec/lib/open_food_network/orders_and_fulfillments_report/supplier_totals_by_distributor_report_spec.rb deleted file mode 100644 index 2ad62ba230..0000000000 --- a/spec/lib/open_food_network/orders_and_fulfillments_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_fulfillments_report/supplier_totals_by_distributor_report' - -RSpec.describe OpenFoodNetwork::OrdersAndFulfillmentsReport::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_type: described_class::REPORT_TYPE } - OpenFoodNetwork::OrdersAndFulfillmentsReport.new(current_user, report_options, true) - end - - let(:report_table) do - OpenFoodNetwork::OrderGrouper.new(report.rules, report.columns).table(report.table_items) - 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_fulfillments_report/supplier_totals_report_spec.rb b/spec/lib/open_food_network/orders_and_fulfillments_report/supplier_totals_report_spec.rb deleted file mode 100644 index a3ec8baea9..0000000000 --- a/spec/lib/open_food_network/orders_and_fulfillments_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_fulfillments_report/supplier_totals_report' - -RSpec.describe OpenFoodNetwork::OrdersAndFulfillmentsReport::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_type: described_class::REPORT_TYPE } - OpenFoodNetwork::OrdersAndFulfillmentsReport.new(current_user, report_options, true) - end - - let(:report_table) do - OpenFoodNetwork::OrderGrouper.new(report.rules, report.columns).table(report.table_items) - 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 51343ddcab..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_fulfillments_report' -require 'open_food_network/order_grouper' - -describe OpenFoodNetwork::OrdersAndFulfillmentsReport 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_type: report_type) - expect(report.header.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) { - report = described_class.new(admin_user, { report_type: "order_cycle_customer_totals" }, true) - OpenFoodNetwork::OrderGrouper.new(report.rules, report.columns).table(report.table_items) - } - - 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_report_spec.rb b/spec/lib/open_food_network/products_and_inventory_report_spec.rb deleted file mode 100644 index 48a1ba27e1..0000000000 --- a/spec/lib/open_food_network/products_and_inventory_report_spec.rb +++ /dev/null @@ -1,259 +0,0 @@ -# frozen_string_literal: true - -require 'spec_helper' -require 'open_food_network/products_and_inventory_report' - -module OpenFoodNetwork - describe ProductsAndInventoryReport 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.header).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).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_type: '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_type: '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 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 7b8f827cb0..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({}, 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({}, 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 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 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 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 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 bcc77347c2..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_type: '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/reports/bulk_coop_report_spec.rb b/spec/lib/reports/bulk_coop_report_spec.rb new file mode 100644 index 0000000000..b648e6cbe6 --- /dev/null +++ b/spec/lib/reports/bulk_coop_report_spec.rb @@ -0,0 +1,197 @@ +# frozen_string_literal: true + +require 'spec_helper' + +# rubocop:disable Metrics/ModuleLength +module Reporting + module Reports + module BulkCoop + describe Base do + subject { Base.new user, params } + let(:user) { create(:admin_user) } + + describe '#query_result' do + let(:params) { {} } + let(:d1) { create(:distributor_enterprise) } + let(:oc1) { create(:simple_order_cycle) } + let(:o1) { create(:order, completed_at: 1.day.ago, order_cycle: oc1, distributor: d1) } + let(:li1) { build(:line_item_with_shipment) } + + before { o1.line_items << li1 } + + context "as a site admin" do + context 'when searching' do + let(:params) { + { q: { completed_at_gt: '', completed_at_lt: '', distributor_id_in: [] } } + } + + it "fetches completed orders" do + o2 = create(:order, state: 'cart') + o2.line_items << build(:line_item) + expect(subject.table_items).to eq([li1]) + end + + it 'shows canceled orders' do + o2 = create(:order, state: 'canceled', completed_at: 1.day.ago, order_cycle: oc1, + distributor: d1) + line_item = build(:line_item_with_shipment) + o2.line_items << line_item + expect(subject.table_items).to include(line_item) + end + end + + context 'when not searching' do + let(:params) { {} } + + it "fetches completed orders" do + o2 = create(:order, state: 'cart') + o2.line_items << build(:line_item) + expect(subject.table_items).to eq([li1]) + end + + it 'shows canceled orders' do + o2 = create(:order, state: 'canceled', completed_at: 1.day.ago, order_cycle: oc1, + distributor: d1) + line_item = build(:line_item_with_shipment) + o2.line_items << line_item + expect(subject.table_items).to include(line_item) + end + end + end + + context "filtering by date" do + it do + user = create(:admin_user) + o2 = create(:order, completed_at: 3.days.ago, order_cycle: oc1, distributor: d1) + li2 = build(:line_item_with_shipment) + o2.line_items << li2 + + report = Base.new user, {} + expect(report.table_items).to match_array [li1, li2] + + report = Base.new( + user, { q: { completed_at_gt: 2.days.ago } } + ) + expect(report.table_items).to eq([li1]) + + report = Base.new( + user, { q: { completed_at_lt: 2.days.ago } } + ) + expect(report.table_items).to eq([li2]) + end + end + + context "filtering by distributor" do + it do + user = create(:admin_user) + d2 = create(:distributor_enterprise) + o2 = create(:order, distributor: d2, order_cycle: oc1, + completed_at: Time.zone.now) + li2 = build(:line_item_with_shipment) + o2.line_items << li2 + + report = Base.new user, {} + expect(report.table_items).to match_array [li1, li2] + + report = Base.new( + user, { q: { distributor_id_in: [d1.id] } } + ) + expect(report.table_items).to eq([li1]) + + report = Base.new( + user, { q: { distributor_id_in: [d2.id] } } + ) + expect(report.table_items).to eq([li2]) + end + end + + context "as a manager of a supplier" do + let!(:user) { create(:user) } + subject { Base.new user, {} } + + 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) do + create(:order, distributor: d1, completed_at: 1.day.ago, + bill_address: create(:address), + ship_address: create(:address)) + end + let(:li2) do + build(:line_item_with_shipment, product: create(:simple_product, supplier: s1)) + end + + before do + o2.line_items << li2 + create(:enterprise_relationship, parent: s1, child: d1, + 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 + end + + context "that has not granted P-OC to the distributor" do + let(:o2) do + create(:order, distributor: d1, completed_at: 1.day.ago, + bill_address: create(:address), + ship_address: create(:address)) + end + let(:li2) do + build(:line_item_with_shipment, product: create(:simple_product, supplier: s1)) + end + + 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 + end + end + end + + describe '#columns' do + context 'when report type is bulk_coop_customer_payments' do + subject { CustomerPayments.new user } + + it 'returns' do + expect(subject.columns.values).to match_array( + [ + :order_billing_address_name, + :order_completed_at, + :customer_payments_total_cost, + :customer_payments_amount_owed, + :customer_payments_amount_paid, + ] + ) + end + end + end + + # Yes, I know testing a private method is bad practice but report's design, tighly coupling + # 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) { {} } + let(:user) { build(:user) } + let!(:line_item) { create(:line_item) } + let(:order) { line_item.order } + + it 'calls #new_outstanding_balance' do + expect_any_instance_of(Spree::Order).to receive(:new_outstanding_balance) + CustomerPayments.new(user).__send__(:customer_payments_amount_owed, [line_item]) + end + end + end + end + end +end +# rubocop:enable Metrics/ModuleLength diff --git a/spec/lib/reports/customers_report_spec.rb b/spec/lib/reports/customers_report_spec.rb new file mode 100644 index 0000000000..f9a1c1a77e --- /dev/null +++ b/spec/lib/reports/customers_report_spec.rb @@ -0,0 +1,162 @@ +# frozen_string_literal: true + +require 'spec_helper' + +module Reporting + module Reports + module Customers + describe Base 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 { Base.new user, {} } + + describe "mailing list report" do + subject { MailingList.new user, {} } + + 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(:query_result).and_return [order] + + expect(subject.table_rows).to eq([[ + "test@test.com", "Firsty", "Lasty", "Suburbia" + ]]) + end + end + + describe "addresses report" do + subject { Addresses.new user, {} } + + 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(:query_result).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.query_result).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.query_result).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 { Base.new user, {} } + + 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.query_result).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.query_result).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/report_service_spec.rb b/spec/lib/reports/enterprise_fee_summary/enterprise_fee_summary_report_spec.rb similarity index 96% rename from engines/order_management/spec/services/order_management/reports/enterprise_fee_summary/report_service_spec.rb rename to spec/lib/reports/enterprise_fee_summary/enterprise_fee_summary_report_spec.rb index 28d240a296..0487954d33 100644 --- a/engines/order_management/spec/services/order_management/reports/enterprise_fee_summary/report_service_spec.rb +++ b/spec/lib/reports/enterprise_fee_summary/enterprise_fee_summary_report_spec.rb @@ -2,8 +2,8 @@ require "spec_helper" -describe OrderManagement::Reports::EnterpriseFeeSummary::ReportService do - let(:report_klass) { OrderManagement::Reports::EnterpriseFeeSummary } +describe Reporting::Reports::EnterpriseFeeSummary::Base do + let(:report_module) { Reporting::Reports::EnterpriseFeeSummary } # Basic data. let!(:shipping_method) do @@ -41,10 +41,12 @@ describe OrderManagement::Reports::EnterpriseFeeSummary::ReportService do # Setup up permissions and report. let!(:current_user) { create(:admin_user) } - - let(:permissions) { report_klass::Permissions.new(current_user) } - let(:parameters) { report_klass::Parameters.new } - let(:service) { described_class.new(permissions, parameters) } + let(:parameters) { report_module::Parameters.new } + let(:subject) { + report = described_class.new(current_user) + allow(report).to receive(:parameters).and_return(parameters) + report + } describe "grouping and sorting of entries" do let!(:order_cycle) do @@ -98,7 +100,7 @@ describe OrderManagement::Reports::EnterpriseFeeSummary::ReportService do let!(:other_customer_order) { prepare_order(customer: another_customer) } it "groups and sorts entries correctly" do - totals = service.list + totals = subject.query_result expect(totals.length).to eq(16) @@ -165,7 +167,7 @@ describe OrderManagement::Reports::EnterpriseFeeSummary::ReportService do end it "is included" do - totals = service.list + totals = subject.query_result expect(totals.length).to eq(1) @@ -200,7 +202,7 @@ describe OrderManagement::Reports::EnterpriseFeeSummary::ReportService do end it "is included" do - totals = service.list + totals = subject.query_result expect(totals.length).to eq(2) @@ -225,7 +227,7 @@ describe OrderManagement::Reports::EnterpriseFeeSummary::ReportService do let!(:customer_order) { prepare_order(customer: customer) } it "is included" do - totals = service.list + totals = subject.query_result expect(totals.length).to eq(1) @@ -272,7 +274,7 @@ describe OrderManagement::Reports::EnterpriseFeeSummary::ReportService do let!(:customer_order) { prepare_order(customer: customer) } it "fetches data correctly" do - totals = service.list + totals = subject.query_result expect(totals.length).to eq(6) @@ -349,7 +351,7 @@ describe OrderManagement::Reports::EnterpriseFeeSummary::ReportService do let!(:customer_order) { prepare_order(customer: customer) } it "fetches data correctly" do - totals = service.list + totals = subject.query_result expect(totals.length).to eq(11) @@ -412,7 +414,7 @@ describe OrderManagement::Reports::EnterpriseFeeSummary::ReportService do let!(:current_user) { create(:admin_user) } it "includes all order cycles" do - totals = service.list + totals = subject.query_result expect_total_matches(totals, 2, fee_type: "Shipment") expect_total_matches(totals, 1, fee_type: "Shipment", enterprise_name: "Distributor A") @@ -424,7 +426,7 @@ describe OrderManagement::Reports::EnterpriseFeeSummary::ReportService do let!(:current_user) { distributor_a.owner } it "does not include unrelated order cycles" do - totals = service.list + totals = subject.query_result expect_total_matches(totals, 1, fee_type: "Shipment") expect_total_matches(totals, 1, fee_type: "Shipment", enterprise_name: "Distributor A") @@ -433,7 +435,7 @@ describe OrderManagement::Reports::EnterpriseFeeSummary::ReportService do end describe "filters entries correctly" do - let(:parameters) { report_klass::Parameters.new(parameters_attributes) } + let(:parameters) { report_module::Parameters.new(parameters_attributes) } context "filtering by completion date" do let(:timestamp) { Time.zone.local(2018, 1, 5, 14, 30, 5) } @@ -464,7 +466,7 @@ describe OrderManagement::Reports::EnterpriseFeeSummary::ReportService do let(:parameters_attributes) { { start_at: timestamp } } it "filters entries" do - totals = service.list + totals = subject.query_result expect_total_matches(totals, 0, fee_type: "Shipment", customer_name: "Customer A") expect_total_matches(totals, 1, fee_type: "Shipment", customer_name: "Customer B") @@ -476,7 +478,7 @@ describe OrderManagement::Reports::EnterpriseFeeSummary::ReportService do let(:parameters_attributes) { { end_at: timestamp } } it "filters entries" do - totals = service.list + totals = subject.query_result expect_total_matches(totals, 1, fee_type: "Shipment", customer_name: "Customer A") expect_total_matches(totals, 1, fee_type: "Shipment", customer_name: "Customer B") @@ -506,7 +508,7 @@ describe OrderManagement::Reports::EnterpriseFeeSummary::ReportService do let(:parameters_attributes) { { distributor_ids: [distributor_a.id, distributor_b.id] } } it "filters entries" do - totals = service.list + totals = subject.query_result expect_total_matches(totals, 1, fee_type: "Shipment", enterprise_name: "Distributor A") expect_total_matches(totals, 1, fee_type: "Shipment", enterprise_name: "Distributor B") @@ -544,7 +546,7 @@ describe OrderManagement::Reports::EnterpriseFeeSummary::ReportService do let(:parameters_attributes) { { producer_ids: [producer_a.id, producer_b.id] } } it "filters entries" do - totals = service.list + totals = subject.query_result expect_total_matches(totals, 1, fee_name: "Fee A", enterprise_name: "Producer A") expect_total_matches(totals, 1, fee_name: "Fee B", enterprise_name: "Producer B") @@ -581,7 +583,7 @@ describe OrderManagement::Reports::EnterpriseFeeSummary::ReportService do let(:parameters_attributes) { { order_cycle_ids: [order_cycle_a.id, order_cycle_b.id] } } it "filters entries" do - totals = service.list + totals = subject.query_result expect_total_matches(totals, 1, fee_type: "Shipment", enterprise_name: "Distributor A") expect_total_matches(totals, 1, fee_type: "Shipment", enterprise_name: "Distributor B") @@ -602,7 +604,7 @@ describe OrderManagement::Reports::EnterpriseFeeSummary::ReportService do let(:parameters_attributes) { { enterprise_fee_ids: [fee_a.id, fee_b.id] } } it "filters entries" do - totals = service.list + totals = subject.query_result expect_total_matches(totals, 1, fee_name: "Fee A") expect_total_matches(totals, 1, fee_name: "Fee B") @@ -634,7 +636,7 @@ describe OrderManagement::Reports::EnterpriseFeeSummary::ReportService do end it "filters entries" do - totals = service.list + totals = subject.query_result expect_total_matches(totals, 1, fee_name: "Shipping A") expect_total_matches(totals, 1, fee_name: "Shipping B") @@ -666,7 +668,7 @@ describe OrderManagement::Reports::EnterpriseFeeSummary::ReportService do end it "filters entries" do - totals = service.list + totals = subject.query_result expect_total_matches(totals, 1, fee_name: "Payment A") expect_total_matches(totals, 1, fee_name: "Payment B") 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/spec/lib/reports/enterprise_fee_summary/renderers/csv_renderer_spec.rb b/spec/lib/reports/enterprise_fee_summary/renderers/csv_renderer_spec.rb new file mode 100644 index 0000000000..91046372fd --- /dev/null +++ b/spec/lib/reports/enterprise_fee_summary/renderers/csv_renderer_spec.rb @@ -0,0 +1,96 @@ +# frozen_string_literal: true + +require "spec_helper" + +# 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 } +# let!(:service) { report_klass::ReportService.new(permissions, parameters) } +# let!(:renderer) { described_class.new(service) } + +# # Context which will be passed to the renderer. The response object +# # is not automatically prepared, +# # so this has to be assigned explicitly. +# let!(:response) { ActionDispatch::TestResponse.new } +# let!(:request) { double(Rack::Request) } +# let!(:controller) do +# ActionController::Base.new.tap do |controller_mock| +# controller_mock.instance_variable_set(:@_response, response) +# controller_mock.instance_variable_set(:@_request, request) +# end +# end + +# let!(:enterprise_fee_type_totals) do +# [ +# report_klass::ReportData::EnterpriseFeeTypeTotal.new( +# fee_type: "Fee Type A", +# enterprise_name: "Enterprise A", +# fee_name: "Fee A", +# customer_name: "Custoemr A", +# fee_placement: "Fee Placement A", +# fee_calculated_on_transfer_through_name: "Transfer Enterprise A", +# tax_category_name: "Tax Category A", +# total_amount: "1.00" +# ), +# report_klass::ReportData::EnterpriseFeeTypeTotal.new( +# fee_type: "Fee Type B", +# enterprise_name: "Enterprise B", +# fee_name: "Fee C", +# customer_name: "Custoemr D", +# fee_placement: "Fee Placement E", +# fee_calculated_on_transfer_through_name: "Transfer Enterprise F", +# tax_category_name: "Tax Category G", +# total_amount: "2.00" +# ) +# ] +# end + +# let(:current_user) { nil } + +# before do +# allow(service).to receive(:list) { enterprise_fee_type_totals } +# allow(request).to receive_messages(variant: double(Spree::Variant), +# should_apply_vary_header?: true) +# end + +# it "generates CSV header" do +# renderer.render(controller) +# result = response.body +# csv = CSV.parse(result) +# header_row = csv[0] + +# # Test all header cells have values +# expect(header_row.length).to eq(8) +# expect(header_row.all?(&:present?)).to be_truthy +# end + +# it "generates CSV data rows" do +# renderer.render(controller) +# result = response.body +# csv = CSV.parse(result, headers: true) + +# expect(csv.length).to eq(2) + +# # Test random cells +# expect(csv[0][i18n_translate("header.fee_type")]).to eq("Fee Type A") +# expect(csv[0][i18n_translate("header.total_amount")]).to eq("1.00") +# expect(csv[1][i18n_translate("header.total_amount")]).to eq("2.00") +# end + +# it "generates filename correctly" do +# Timecop.freeze(Time.zone.local(2018, 10, 9, 7, 30, 0)) do +# filename = renderer.__send__(:filename) +# expect(filename).to eq("enterprise_fee_summary_20181009.csv") +# end +# end + +# def i18n_translate(key) +# I18n.t(key, scope: i18n_scope) +# end + +# def i18n_scope +# "order_management.reports.enterprise_fee_summary.formats.csv" +# end +# end diff --git a/spec/lib/reports/enterprise_fee_summary/renderers/html_renderer_spec.rb b/spec/lib/reports/enterprise_fee_summary/renderers/html_renderer_spec.rb new file mode 100644 index 0000000000..b396458e0f --- /dev/null +++ b/spec/lib/reports/enterprise_fee_summary/renderers/html_renderer_spec.rb @@ -0,0 +1,72 @@ +# frozen_string_literal: true + +require "spec_helper" + +# 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) { Reporting::Reports::EnterpriseFeeSummariesController.new } +# let!(:service) { report_klass::ReportService.new(permissions, parameters) } +# let!(:renderer) { described_class.new(service) } + +# let!(:enterprise_fee_type_totals) do +# [ +# report_klass::ReportData::EnterpriseFeeTypeTotal.new( +# fee_type: "Fee Type A", +# enterprise_name: "Enterprise A", +# fee_name: "Fee A", +# customer_name: "Custoemr A", +# fee_placement: "Fee Placement A", +# fee_calculated_on_transfer_through_name: "Transfer Enterprise A", +# tax_category_name: "Tax Category A", +# total_amount: "1.00" +# ), +# report_klass::ReportData::EnterpriseFeeTypeTotal.new( +# fee_type: "Fee Type B", +# enterprise_name: "Enterprise B", +# fee_name: "Fee C", +# customer_name: "Custoemr D", +# fee_placement: "Fee Placement E", +# fee_calculated_on_transfer_through_name: "Transfer Enterprise F", +# tax_category_name: "Tax Category G", +# total_amount: "2.00" +# ) +# ] +# end + +# let(:current_user) { nil } + +# before do +# allow(service).to receive(:list) { enterprise_fee_type_totals } +# end + +# it "generates header values" do +# header_row = renderer.header + +# # Test all header cells have values +# expect(header_row.length).to eq(8) +# expect(header_row.all?(&:present?)).to be_truthy +# end + +# it "generates data rows" do +# header_row = renderer.header +# result = renderer.data_rows + +# expect(result.length).to eq(2) + +# # Test random cells +# expect(result[0][header_row.index(i18n_translate("header.fee_type"))]).to eq("Fee Type A") +# expect(result[0][header_row.index(i18n_translate("header.total_amount"))]).to eq("1.00") +# expect(result[1][header_row.index(i18n_translate("header.total_amount"))]).to eq("2.00") +# end + +# def i18n_translate(key) +# I18n.t(key, scope: i18n_scope) +# end + +# def i18n_scope +# "order_management.reports.enterprise_fee_summary.formats.csv" +# end +# end 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..d9af25be08 --- /dev/null +++ b/spec/lib/reports/lettuce_share_report_spec.rb @@ -0,0 +1,91 @@ +# frozen_string_literal: true + +require 'spec_helper' + +module Reporting + module Reports + module ProductsAndInventory + describe LettuceShare do + let(:user) { create(:user) } + let(:report) { LettuceShare.new(user) } + 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(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(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(report).to receive(:child_variants) { + Spree::Variant.where(id: [variant, variant2, variant3]) + } + allow(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..6fe005beda --- /dev/null +++ b/spec/lib/reports/order_cycle_management_report_spec.rb @@ -0,0 +1,218 @@ +# frozen_string_literal: true + +require 'spec_helper' + +module Reporting + module Reports + module OrderCycleManagement + describe Base do + context "as a site admin" do + subject { Base.new(user, params) } + 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 { Base.new user, {} } + + 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 { Base.new(user, params) } + + let(:distributor) { create(:distributor_enterprise) } + before { distributor.enterprise_roles.create!(user: user) } + + context 'when the report type is payment_methods' do + subject { PaymentMethods.new(user) } + + let!(:order) do + create( + :completed_order_with_totals, + distributor: distributor, + completed_at: 1.day.ago + ) + end + + it 'returns rows with payment information' do + allow(subject).to receive(:raw_render?).and_return(true) + expect(subject.table_rows).to eq([[ + order.billing_address.firstname, + order.billing_address.lastname, + 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 delivery' do + subject { Delivery.new(user) } + let!(:order) do + create( + :completed_order_with_totals, + distributor: distributor, + completed_at: 1.day.ago + ) + end + + it 'returns rows with delivery information' do + allow(subject).to receive(:raw_render?).and_return(true) + expect(subject.table_rows).to eq([[ + order.ship_address.firstname, + order.ship_address.lastname, + 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/reports/orders_and_distributors_report_spec.rb b/spec/lib/reports/orders_and_distributors_report_spec.rb new file mode 100644 index 0000000000..243b45005b --- /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 Base do + describe 'orders and distributors report' do + it 'should return a header row describing the report' do + subject = Base.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 = Base.new create(:admin_user), {} + allow(subject).to receive(:raw_render?).and_return(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 = Base.new(create(:admin_user)) + + 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/order_cycle_customer_totals_report_spec.rb b/spec/lib/reports/orders_and_fulfillment/order_cycle_customer_totals_report_spec.rb new file mode 100644 index 0000000000..40297f34c8 --- /dev/null +++ b/spec/lib/reports/orders_and_fulfillment/order_cycle_customer_totals_report_spec.rb @@ -0,0 +1,112 @@ +# frozen_string_literal: true + +require "spec_helper" + +module Reporting + module Reports + module OrdersAndFulfillment + describe OrderCycleCustomerTotals do + let!(:distributor) { create(:distributor_enterprise) } + let!(:customer) { create(:customer, enterprise: distributor) } + let(:current_user) { distributor.owner } + let(:params) { { display_summary_row: true } } + let(:report) { OrderCycleCustomerTotals.new(current_user, params) } + + 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 + end + + it 'includes the order number and date in item rows' do + expect(report.rows.first.order_number).to eq order.number + expect(report.rows.first.date).to eq 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 + expect(report.rows.first.shipping).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 + allow(report).to receive(:raw_render?).and_return(true) + expect(report.rows.last.pay_fee_price).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 + expect(report.rows.first.sku).to eq overidden_sku + end + end + end + end + end +end diff --git a/spec/lib/reports/orders_and_fulfillment/order_cycle_distributor_totals_by_supplier_report_spec.rb b/spec/lib/reports/orders_and_fulfillment/order_cycle_distributor_totals_by_supplier_report_spec.rb new file mode 100644 index 0000000000..39283c6fd3 --- /dev/null +++ b/spec/lib/reports/orders_and_fulfillment/order_cycle_distributor_totals_by_supplier_report_spec.rb @@ -0,0 +1,40 @@ +# frozen_string_literal: true + +require 'spec_helper' + +module Reporting + module Reports + module OrdersAndFulfillment + describe OrderCycleDistributorTotalsBySupplier 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(:params) { { display_summary_row: true } } + let(:report) do + OrderCycleDistributorTotalsBySupplier.new(current_user, params) + 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 + end + end + end + end +end diff --git a/spec/lib/reports/orders_and_fulfillment/order_cycle_supplier_totals_by_distributor_report_spec.rb b/spec/lib/reports/orders_and_fulfillment/order_cycle_supplier_totals_by_distributor_report_spec.rb new file mode 100644 index 0000000000..d99bd2b28f --- /dev/null +++ b/spec/lib/reports/orders_and_fulfillment/order_cycle_supplier_totals_by_distributor_report_spec.rb @@ -0,0 +1,37 @@ +# frozen_string_literal: true + +require 'spec_helper' + +module Reporting + module Reports + module OrdersAndFulfillment + describe OrderCycleSupplierTotalsByDistributor 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(:params) { { display_summary_row: true } } + let(:report) do + OrderCycleSupplierTotalsByDistributor.new(current_user, params) + 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 + expect(report.rows.first.producer).to eq supplier.name + expect(report.rows.first.hub).to eq distributor.name + end + end + end + end +end diff --git a/spec/lib/reports/orders_and_fulfillment/orders_cycle_supplier_totals_report_spec.rb b/spec/lib/reports/orders_and_fulfillment/orders_cycle_supplier_totals_report_spec.rb new file mode 100644 index 0000000000..2c2c7047d7 --- /dev/null +++ b/spec/lib/reports/orders_and_fulfillment/orders_cycle_supplier_totals_report_spec.rb @@ -0,0 +1,37 @@ +# frozen_string_literal: true + +require 'spec_helper' + +module Reporting + module Reports + module OrdersAndFulfillment + describe OrderCycleSupplierTotals 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(:params) { { display_summary_row: false } } + let(:report) do + OrderCycleSupplierTotals.new(current_user, params) + 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/packing/packing_report_spec.rb b/spec/lib/reports/packing/packing_report_spec.rb index d661fbb586..959c2c5546 100644 --- a/spec/lib/reports/packing/packing_report_spec.rb +++ b/spec/lib/reports/packing/packing_report_spec.rb @@ -20,7 +20,7 @@ describe "Packing Reports" do let(:report_contents) { subject.report_data.rows.flatten } let(:row_count) { subject.report_data.rows.count } - subject { Reporting::Reports::Packing::Customer.new user, params } + subject { Reporting::Reports::Packing::Customer.new user, { q: params } } before do order.line_items << line_item @@ -174,7 +174,7 @@ describe "Packing Reports" do it "groups and orders by distributor and order" do expect(subject.report_data.rows.map(&:first)).to eq( - [order.distributor.name, "", order2.distributor.name, order2.distributor.name, ""] + [order.distributor.name, order2.distributor.name, order2.distributor.name] ) end end diff --git a/spec/lib/reports/products_and_inventory_report_spec.rb b/spec/lib/reports/products_and_inventory_report_spec.rb new file mode 100644 index 0000000000..87739af47e --- /dev/null +++ b/spec/lib/reports/products_and_inventory_report_spec.rb @@ -0,0 +1,271 @@ +# frozen_string_literal: true + +require 'spec_helper' + +module Reporting + module Reports + module ProductsAndInventory + describe Base 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 + Base.new user, {} + 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(:query_result).and_return [variant] + allow(subject).to receive(:raw_render?).and_return(true) + + 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.query_result).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 + Base.new enterprise_user, {} + 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) + + subject = Inventory.new enterprise_user + 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 + ] + ) + + subject = Inventory.new enterprise_user + allow(subject).to receive(:params).and_return( + order_cycle_id: order_cycle.id, + supplier_id: supplier.id, + distributor_id: distributor.id + ) + + expect(subject.filter(variants)).to match_array [not_filtered_variant] + + # And it integrates with the ordering of the `variants` method. + expect(subject.query_result).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") + allow(subject).to receive(:query_result).and_return([variant]) + } + + context "when the variant has an SKU set" do + before { variant.update_attribute(:sku, "Variant SKU") } + it "returns it" do + expect(subject.rows.first.sku).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.rows.first.sku).to eq "Product SKU" + end + end + end + end + end + end + end +end diff --git a/spec/lib/reports/report_loader_spec.rb b/spec/lib/reports/report_loader_spec.rb index 5341ae641f..930c5af1ac 100644 --- a/spec/lib/reports/report_loader_spec.rb +++ b/spec/lib/reports/report_loader_spec.rb @@ -9,6 +9,10 @@ module Reporting class Green; end class Yellow; end end + + module Orange + class OrangeReport; end + end end end @@ -17,10 +21,6 @@ describe Reporting::ReportLoader do let(:report_base_class) { Reporting::Reports::Bananas::Base } let(:report_subtypes) { ["green", "yellow"] } - before do - allow(report_base_class).to receive(:report_subtypes).and_return(report_subtypes) - end - describe "#report_class" do describe "given report type and subtype" do let(:arguments) { ["bananas", "yellow"] } @@ -31,17 +31,16 @@ describe Reporting::ReportLoader do end describe "given report type only" do - context "when the report has multiple subtypes" do + context "when the report has no subtypes" do let(:arguments) { ["bananas"] } - it "returns first listed report type" do - expect(service.report_class).to eq Reporting::Reports::Bananas::Green + it "returns base class" do + expect(service.report_class).to eq Reporting::Reports::Bananas::Base end end - context "when the report has no subtypes" do - let(:arguments) { ["bananas"] } - let(:report_subtypes) { [] } + context "when the subtype is not implemented, fallback to base" do + let(:arguments) { ["bananas", "not_existing"] } it "returns base class" do expect(service.report_class).to eq Reporting::Reports::Bananas::Base @@ -50,7 +49,6 @@ describe Reporting::ReportLoader do context "given a report type that does not exist" do let(:arguments) { ["apples"] } - let(:report_subtypes) { [] } it "raises an error" do expect{ service.report_class }.to raise_error(Reporting::Errors::ReportNotFound) @@ -58,51 +56,4 @@ describe Reporting::ReportLoader do end end end - - describe "#default_report_subtype" do - context "when the report has multiple subtypes" do - let(:arguments) { ["bananas"] } - - it "returns the first report type" do - expect(service.default_report_subtype).to eq report_base_class.report_subtypes.first - end - end - - context "when the report has no subtypes" do - let(:arguments) { ["bananas"] } - let(:report_subtypes) { [] } - - it "returns base" do - expect(service.default_report_subtype).to eq "base" - end - end - - context "given a report type that does not exist" do - let(:arguments) { ["apples"] } - let(:report_subtypes) { [] } - - it "raises an error" do - expect{ service.report_class }.to raise_error(Reporting::Errors::ReportNotFound) - end - end - end - - describe "#report_subtypes" do - context "when the report has multiple subtypes" do - let(:arguments) { ["bananas"] } - - it "returns a list of report subtypes for a given report" do - expect(service.report_subtypes).to eq report_subtypes - end - end - - context "when the report has no subtypes" do - let(:arguments) { ["bananas"] } - let(:report_subtypes) { [] } - - it "returns an empty array" do - expect(service.report_subtypes).to eq [] - end - end - end end diff --git a/spec/lib/reports/report_renderer_spec.rb b/spec/lib/reports/report_renderer_spec.rb index 2377c96950..eda37744ca 100644 --- a/spec/lib/reports/report_renderer_spec.rb +++ b/spec/lib/reports/report_renderer_spec.rb @@ -9,75 +9,29 @@ describe Reporting::ReportRenderer do { "id" => 2, "name" => "onions", "quantity" => 6 } ] } - let(:report_data) { ActiveRecord::Result.new(data.first.keys, data.map(&:values)) } - let(:report) { OpenStruct.new(report_data: report_data) + let(:report) { + OpenStruct.new( + columns: { + id: proc { |row| row["id"] }, + name: proc { |row| row["name"] }, + quantity: proc { |row| row["quantity"] }, + }, + rows: data, + table_headers: data.first.keys, + table_rows: data.map(&:values) + ) } - let(:service) { described_class.new(report) } + let(:subject) { described_class.new(report) } - describe "#table_headers" do - it "returns the report's table headers" do - expect(service.table_headers).to eq ["id", "name", "quantity"] - end - end - - describe "#table_rows" do - it "returns the report's table rows" do - expect(service.table_rows).to eq [ - [1, "carrots", 3], - [2, "onions", 6] - ] - end - end - - describe "#as_json" do + describe ".as_json" do it "returns the report's data as hashes" do - expect(service.as_json).to eq data.as_json + expect(subject.as_json).to eq data.as_json end end - describe "#as_arrays" do - it "returns the report's data as arrays" do - expect(service.as_arrays).to eq [ - ["id", "name", "quantity"], - [1, "carrots", 3], - [2, "onions", 6] - ] - end - end - - describe "exporting to different formats" do - let(:spreadsheet_architect) { SpreadsheetArchitect } - before do - allow(spreadsheet_architect).to receive(:to_csv) {} - allow(spreadsheet_architect).to receive(:to_ods) {} - allow(spreadsheet_architect).to receive(:to_xlsx) {} - end - - describe "#to_csv" do - it "exports as csv" do - service.to_csv - - expect(spreadsheet_architect).to have_received(:to_csv). - with(headers: service.table_headers, data: service.table_rows) - end - end - - describe "#to_ods" do - it "exports as ods" do - service.to_ods - - expect(spreadsheet_architect).to have_received(:to_ods). - with(headers: service.table_headers, data: service.table_rows) - end - end - - describe "#to_xslx" do - it "exports as xlsx" do - service.to_xlsx - - expect(spreadsheet_architect).to have_received(:to_xlsx). - with(headers: service.table_headers, data: service.table_rows) - end + describe ".render_as" do + it "raise an error if format is not supported" do + expect { subject.render_as("give_me_everything") }.to raise_error end end end diff --git a/spec/lib/reports/report_spec.rb b/spec/lib/reports/report_spec.rb new file mode 100644 index 0000000000..c5d31dc369 --- /dev/null +++ b/spec/lib/reports/report_spec.rb @@ -0,0 +1,278 @@ +# frozen_string_literal: true + +require 'spec_helper' + +# rubocop:disable Metrics/ModuleLength +module Reporting + describe ReportTemplate do + let(:user) { create(:user) } + let(:params) { {} } + subject { described_class.new(user, params) } + + # rubocop:disable Metrics/AbcSize + def check_report + # Mock using instance variables + allow(subject).to receive(:columns).and_return(@columns) + allow(subject).to receive(:query_result).and_return(@query_result) + allow(subject).to receive(:rules).and_return(@rules) if @rules.present? + if @custom_headers.present? + allow(subject).to receive(:custom_headers).and_return(@custom_headers) + end + + # Check result depending on existing instance variables + expect(subject.rows.map(&:to_h)).to eq(@expected_rows) if @expected_rows.present? + expect(subject.table_rows).to eq(@expected_table_rows) if @expected_table_rows.present? + expect(subject.table_headers).to eq(@expected_headers) if @expected_headers.present? + end + # rubocop:enable Metrics/AbcSize + + describe ".default_params" do + it "use correctly the default values" do + default_params = { + filter: "default__filter", + other_filter: "default_other_filter", + q: { hub: "default_hub", customer: "default_customer" } + } + real_params = { + filter: "test_filter", + q: { hub: "test_hub" } + } + expected_params = { + filter: "test_filter", + other_filter: "default_other_filter", + q: { hub: "test_hub", customer: "default_customer" } + } + allow_any_instance_of(described_class).to receive(:default_params) + .and_return(default_params) + report = described_class.new(user, real_params) + expect(report.params).to eq(expected_params) + end + end + + describe ".columns" do + before do + @query_result = [ + OpenStruct.new(hub: { name: "My Hub" }, product: { name: "Apple", price: 5 }) + ] + end + + it "handle procs" do + @columns = { + hub: proc { |item| item.hub[:name] } + } + @expected_rows = [ + { hub: "My Hub" } + ] + check_report + end + + it "handles symbols" do + @columns = { + hub: :hub_name + } + allow(subject).to receive(:hub_name).and_return("Transformed Hub Name") + @expected_rows = [ + { hub: "Transformed Hub Name" } + ] + check_report + end + end + + describe ".table_headers" do + before do + @columns = { + hub: proc { |item| item.hub[:name] }, + product: proc { |item| item.product[:name] }, + price: proc { |item| item.product[:price] }, + } + end + + it "uses the columns keys" do + @expected_headers = ['Hub', 'Product', 'Price'] + check_report + end + + it "handles custom_headers" do + @custom_headers = { + product: 'Custom Product', + not_existing_key: "My Key" + } + @expected_headers = ['Hub', 'Custom Product', 'Price'] + check_report + end + + describe "fields_to_hide" do + let(:params) { { fields_to_hide: [:product] } } + + it "works" do + @expected_headers = ['Hub', 'Price'] + check_report + end + end + end + + describe ".table_rows" do + before do + @columns = { + price: proc { |item| item.product[:price] }, + hub: proc { |item| item.hub[:name] } + } + @query_result = [ + OpenStruct.new(hub: { name: "My Hub" }, product: { name: "Apple", price: 5 }), + OpenStruct.new(hub: { name: "My Other Hub" }, product: { name: "Apple", price: 12 }) + ] + end + + it "get correct data" do + allow(subject).to receive(:raw_render?).and_return(true) + @expected_table_rows = [ + [5, "My Hub"], + [12, "My Other Hub"], + ] + check_report + end + end + + describe ".rules" do + describe "#group_by" do + before do + @columns = { + hub: proc { |item| item.hub }, + customer: proc { |item| item.customer }, + product: proc { |item| item.product }, + quantity: proc { |item| item.quantity }, + } + @query_result = [ + OpenStruct.new(hub: "Hub 1", customer: "John", product: "Apple", quantity: 4), + OpenStruct.new(hub: "Hub 2", customer: "John", product: "Pear", quantity: 3), + OpenStruct.new(hub: "Hub 2", customer: "John", product: "Apple", quantity: 5), + OpenStruct.new(hub: "Hub 1", customer: "Abby", product: "Orange", quantity: 6), + ] + end + + it "works with symbol or proc" do + @rules = [ + { group_by: proc { |_i, row| row.hub }, fields_used_in_header: [:hub], header: true }, + { group_by: :customer, header: true } + ] + allow(subject).to receive(:display_header_row?).and_return(true) + allow(subject).to receive(:raw_render?).and_return(true) + @expected_rows = [ + { header: "Hub 1" }, + { header: "Abby" }, + { product: "Orange", quantity: 6 }, + { header: "John" }, + { product: "Apple", quantity: 4 }, + { header: "Hub 2" }, + { header: "John" }, + { product: "Pear", quantity: 3 }, + { product: "Apple", quantity: 5 }, + ] + check_report + end + end + + describe "#sort_by" do + before do + @columns = { + hub_name: proc { |item| item.hub[:name] } + } + hub1 = { name: "Hub 1", popularity: 5 } + hub2 = { name: "Hub 2", popularity: 2 } + @query_result = [ + OpenStruct.new(hub: hub2), + OpenStruct.new(hub: hub1) + ] + end + + it "use default sort" do + @rules = [{ + group_by: proc { |item, _row| item.hub } + }] + @expected_rows = [ + { hub_name: "Hub 1" }, + { hub_name: "Hub 2" }, + ] + check_report + end + + it "use sort_by proc" do + @rules = [{ + group_by: proc { |item, _row| item.hub }, + sort_by: proc { |hub| hub[:popularity] } + }] + @expected_rows = [ + { hub_name: "Hub 2" }, + { hub_name: "Hub 1" } + ] + check_report + end + end + + describe "#summary_row" do + before do + @query_result = [ + OpenStruct.new(hub: "Hub 1", customer: "John", product: "Apple", quantity: 4), + OpenStruct.new(hub: "Hub 2", customer: "John", product: "Pear", quantity: 3), + OpenStruct.new(hub: "Hub 2", customer: "John", product: "Apple", quantity: 5), + OpenStruct.new(hub: "Hub 1", customer: "Abby", product: "Orange", quantity: 6), + ] + end + + it "groups and sum" do + @columns = { + hub: proc { |item| item.hub }, + quantity: proc { |item| item.quantity }, + count: proc { |_item| "" }, + } + @rules = [{ + group_by: :hub, + summary_row: proc do |group_key, items, rows| + { count: "#{group_key} count=#{items.count}", quantity: rows.sum(&:quantity) } + end, + summary_row_label: "TOTAL" + }] + @expetec_rows = [ + { hub: "Hub 1", quantity: 4, count: "" }, + { hub: "Hub 1", quantity: 6, count: "" }, + { hub: "TOTAL", quantity: 10, count: "Hub 1 count=2" }, + { hub: "Hub 2", quantity: 3, count: "" }, + { hub: "Hub 2", quantity: 5, count: "" }, + { hub: "TOTAL", quantity: 8, count: "Hub 2 count=2" } + ] + check_report + end + end + + describe "should not group when for JSON" do + before do + @query_result = [ + OpenStruct.new(hub: "Hub 1", customer: "John", quantity: 4) + ] + @columns = { + hub: proc { |item| item.hub }, + customer: proc { |item| item.customer }, + quantity: proc { |item| item.quantity }, + } + @rules = [{ + group_by: :hub, + header: true, + summary_row: proc do |_group_key, _items, rows| + { quantity: rows.sum(&:quantity) } + end + }] + end + + let(:params) { { fields_to_hide: [:customer], report_format: 'json' } } + + it "works" do + @expetec_rows = [ + { hub: "Hub 1", quantity: 4 } + ] + check_report + end + end + end + end +end +# rubocop:enable Metrics/ModuleLength 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..aa8b6b7885 --- /dev/null +++ b/spec/lib/reports/sales_tax_report_spec.rb @@ -0,0 +1,68 @@ +# frozen_string_literal: true + +require 'spec_helper' + +module Reporting + module Reports + module SalesTax + describe TaxTypes do + let(:user) { create(:user) } + let(:report) { TaxTypes.new(user, {}) } + + 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(:order) { double(:order, id: 1, line_items: [li1, li2]) } + let(:totals) { report.__send__(:totals_of, order) } + + 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..0bcc77dc93 --- /dev/null +++ b/spec/lib/reports/users_and_enterprises_report_spec.rb @@ -0,0 +1,136 @@ +# frozen_string_literal: true + +require 'spec_helper' + +module Reporting + module Reports + module UsersAndEnterprises + describe Base do + describe "query_result" do + let!(:owners_and_enterprises) { double(:owners_and_enterprises) } + let!(:managers_and_enterprises) { double(:managers_and_enterprises) } + let!(:subject) { Base.new(nil, {}) } + + 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.query_result + end + end + + describe "sorting results" do + let!(:subject) { Base.new(nil, {}) } + + it "sorts by creation date" do + uae_mock = [ + OpenStruct.new({ created_at: Date.new(2015, 1, 1), name: "aaa" }), + OpenStruct.new({ created_at: Date.new(2015, 1, 2), name: "bbb" }) + ] + expect(subject.sort(uae_mock)).to eq [uae_mock[1], uae_mock[0]] + end + + it "sorts by creation date when nil date" do + uae_mock = [ + OpenStruct.new({ created_at: nil, name: "aaa" }), + OpenStruct.new({ created_at: Date.new(2015, 1, 2), name: "bbb" }) + ] + expect(subject.sort(uae_mock)).to eq [uae_mock[1], uae_mock[0]] + end + + it "then sorts by name" do + uae_mock = [ + OpenStruct.new({ name: "aaa", relationship_type: "bbb", user_email: "bbb" }), + OpenStruct.new({ 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 = [ + OpenStruct.new({ name: "aaa", relationship_type: "bbb", user_email: "bbb" }), + OpenStruct.new({ name: "aaa", relationship_type: "aaa", user_email: "aaa" }), + OpenStruct.new({ 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 = [ + OpenStruct.new({ name: "aaa", relationship_type: "bbb", user_email: "aaa" }), + OpenStruct.new({ name: "aaa", relationship_type: "aaa", user_email: "aaa" }), + OpenStruct.new({ 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) { Base.new nil, params } + + 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) { Base.new nil, params } + + 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) { Base.new nil, params } + + 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) { Base.new nil, params } + + 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..a8670a0f00 --- /dev/null +++ b/spec/lib/reports/xero_invoices_report_spec.rb @@ -0,0 +1,97 @@ +# frozen_string_literal: true + +require 'spec_helper' + +module Reporting + module Reports + module XeroInvoices + describe Base do + subject { Base.new user, {} } + + let(:user) { create(:user) } + + describe "option defaults" do + let(:report) { Base.new user } + + 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.params).to eq(invoice_date: Date.civil(2015, 5, 5), + due_date: Date.civil(2015, 6, 5), + account_code: 'food sales', + report_subtype: 'summary', + q: {}) + end + end + + describe "summary rows" do + let(:report) { + Base.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 { Base.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/models/spree/ability_spec.rb b/spec/models/spree/ability_spec.rb index 07805b6f9a..c4f754f6d9 100644 --- a/spec/models/spree/ability_spec.rb +++ b/spec/models/spree/ability_spec.rb @@ -5,8 +5,6 @@ require 'cancan/matchers' require 'support/ability_helpers' describe Spree::Ability do - include ::AbilityHelper - let(:user) { create(:user) } let(:subject) { Spree::Ability.new(user) } let(:token) { nil } @@ -441,8 +439,11 @@ describe Spree::Ability do it "should be able to read some reports" do is_expected.to have_ability( - [:admin, :index, :customers, :bulk_coop, :orders_and_fulfillment, :products_and_inventory, - :order_cycle_management], for: Spree::Admin::ReportsController + [:admin, :index, :show], for: Admin::ReportsController + ) + is_expected.to have_ability( + [:customers, :bulk_coop, :orders_and_fulfillment, :products_and_inventory, + :order_cycle_management], for: :report ) end @@ -451,7 +452,7 @@ describe Spree::Ability do it "should not be able to read other reports" do is_expected.not_to have_ability( [:group_buys, :payments, :orders_and_distributors, :users_and_enterprises, - :xero_invoices], for: Spree::Admin::ReportsController + :xero_invoices], for: :report ) end @@ -671,8 +672,12 @@ describe Spree::Ability do it "should be able to read some reports" do is_expected.to have_ability( - [:admin, :index, :customers, :sales_tax, :group_buys, :bulk_coop, :payments, - :orders_and_distributors, :orders_and_fulfillment, :products_and_inventory, :order_cycle_management, :xero_invoices], for: Spree::Admin::ReportsController + [:admin, :index, :show], for: Admin::ReportsController + ) + is_expected.to have_ability( + [:customers, :sales_tax, :group_buys, :bulk_coop, :payments, + :orders_and_distributors, :orders_and_fulfillment, :products_and_inventory, + :order_cycle_management, :xero_invoices], for: :report ) end @@ -680,7 +685,7 @@ describe Spree::Ability do it "should not be able to read other reports" do is_expected.not_to have_ability([:users_and_enterprises], - for: Spree::Admin::ReportsController) + for: :report) end it "should be able to access customer actions" do diff --git a/spec/support/ability_helper.rb b/spec/support/ability_helper.rb deleted file mode 100644 index aa1d17fd14..0000000000 --- a/spec/support/ability_helper.rb +++ /dev/null @@ -1,18 +0,0 @@ -# frozen_string_literal: true - -module AbilityHelper - shared_examples "allows access to Enterprise Fee Summary" do - it "should be able to see link and read report" do - is_expected.to have_link_to_enterprise_fee_summary - is_expected.to have_direct_access_to_enterprise_fee_summary - end - - def have_link_to_enterprise_fee_summary - have_ability([:enterprise_fee_summary], for: Spree::Admin::ReportsController) - end - - def have_direct_access_to_enterprise_fee_summary - have_ability([:admin, :new, :create], for: :enterprise_fee_summary) - end - end -end diff --git a/spec/support/ability_helpers.rb b/spec/support/ability_helpers.rb index 4dcb840fe7..c474c8363d 100644 --- a/spec/support/ability_helpers.rb +++ b/spec/support/ability_helpers.rb @@ -105,3 +105,18 @@ shared_examples_for 'update only' do expect(subject).to_not be_able_to(:index, resource) end end + +shared_examples "allows access to Enterprise Fee Summary" do + it "should be able to see link and read report" do + is_expected.to have_link_to_enterprise_fee_summary + is_expected.to have_direct_access_to_enterprise_fee_summary + end + + def have_link_to_enterprise_fee_summary + have_ability([:enterprise_fee_summary], for: :report) + end + + def have_direct_access_to_enterprise_fee_summary + have_ability([:show, :index], for: Admin::ReportsController) + end +end diff --git a/spec/support/matchers/table_matchers.rb b/spec/support/matchers/table_matchers.rb index 7c33b3a5c6..037a0d5d1d 100644 --- a/spec/support/matchers/table_matchers.rb +++ b/spec/support/matchers/table_matchers.rb @@ -11,8 +11,8 @@ RSpec::Matchers.define :have_table_row do |row| !rows_under(node).include? row # Robust check of columns end - failure_message do |_text| - "expected to find table row #{@row}" + failure_message do |text| + "expected to find table row #{@row}, got #{text}" end failure_message_when_negated do |_text| diff --git a/engines/order_management/spec/features/order_management/reports/enterprise_fee_summaries_spec.rb b/spec/system/admin/reports/enterprise_fee_summaries_spec.rb similarity index 85% rename from engines/order_management/spec/features/order_management/reports/enterprise_fee_summaries_spec.rb rename to spec/system/admin/reports/enterprise_fee_summaries_spec.rb index 8d66b93bfc..51bf9b41f2 100644 --- a/engines/order_management/spec/features/order_management/reports/enterprise_fee_summaries_spec.rb +++ b/spec/system/admin/reports/enterprise_fee_summaries_spec.rb @@ -23,7 +23,7 @@ feature "enterprise fee summaries", js: true do it "shows link and allows access to the report" do visit spree.admin_reports_path click_on I18n.t("admin.reports.enterprise_fee_summary.name") - expect(page).to have_button(I18n.t("filters.generate_report", scope: i18n_scope)) + expect(page).to have_button("Go") end end @@ -33,7 +33,7 @@ feature "enterprise fee summaries", js: true do it "shows link and allows access to the report" do visit spree.admin_reports_path click_on I18n.t("admin.reports.enterprise_fee_summary.name") - expect(page).to have_button(I18n.t("filters.generate_report", scope: i18n_scope)) + expect(page).to have_button("Go") end end @@ -43,7 +43,7 @@ feature "enterprise fee summaries", js: true do it "does not allow access to the report" do visit spree.admin_reports_path expect(page).to have_no_link(I18n.t("admin.reports.enterprise_fee_summary.name")) - visit main_app.new_order_management_reports_enterprise_fee_summary_path + visit main_app.admin_report_path(report_type: 'enterprise_fee_summary') expect(page).to have_content(I18n.t("unauthorized")) end end @@ -51,14 +51,14 @@ feature "enterprise fee summaries", js: true do describe "smoke test for filters" do before do - visit main_app.new_order_management_reports_enterprise_fee_summary_path + visit main_app.admin_report_path(report_type: 'enterprise_fee_summary') end context "when logged in as admin" do let(:current_user) { create(:admin_user) } it "shows all available options" do - expect(page).to have_select "report_order_cycle_ids", with_options: [order_cycle.name] + expect(page).to have_select "q_order_cycle_ids", with_options: [order_cycle.name] end end @@ -70,7 +70,7 @@ feature "enterprise fee summaries", js: true do let(:current_user) { distributor.owner } it "shows available options for the enterprise" do - expect(page).to have_select "report_order_cycle_ids", options: [order_cycle.name] + expect(page).to have_select "q_order_cycle_ids", options: [order_cycle.name] end end end @@ -82,7 +82,7 @@ feature "enterprise fee summaries", js: true do describe "smoke test for generation of report based on permissions" do before do - visit main_app.new_order_management_reports_enterprise_fee_summary_path + visit main_app.admin_report_path(report_type: 'enterprise_fee_summary') end context "when logged in as admin" do @@ -94,7 +94,7 @@ feature "enterprise fee summaries", js: true do it "generates file with data for all enterprises" do check I18n.t("filters.report_format_csv", scope: i18n_scope) - click_on I18n.t("filters.generate_report", scope: i18n_scope) + click_on "Go" expect(downloaded_filename).to include ".csv" expect(downloaded_content).to have_content(distributor.name) @@ -114,7 +114,7 @@ feature "enterprise fee summaries", js: true do it "generates file with data for the enterprise" do check I18n.t("filters.report_format_csv", scope: i18n_scope) - click_on I18n.t("filters.generate_report", scope: i18n_scope) + click_on "Go" expect(downloaded_filename).to include ".csv" csv_content = downloaded_content @@ -140,13 +140,13 @@ feature "enterprise fee summaries", js: true do let(:current_user) { create(:admin_user) } before do - visit main_app.new_order_management_reports_enterprise_fee_summary_path + visit main_app.admin_report_path(report_type: 'enterprise_fee_summary') end it "generates file with data for selected order cycle" do select order_cycle.name, from: "report_order_cycle_ids" check I18n.t("filters.report_format_csv", scope: i18n_scope) - click_on I18n.t("filters.generate_report", scope: i18n_scope) + click_on "Go" expect(downloaded_filename).to include ".csv" csv_content = downloaded_content diff --git a/spec/system/admin/reports/packing_report_spec.rb b/spec/system/admin/reports/packing_report_spec.rb index dcd5660641..efe64b1901 100644 --- a/spec/system/admin/reports/packing_report_spec.rb +++ b/spec/system/admin/reports/packing_report_spec.rb @@ -46,22 +46,22 @@ describe "Packing Reports", js: true do click_link "Pack By Customer" fill_in 'q_completed_at_gt', with: '2013-04-25 13:00:00' fill_in 'q_completed_at_lt', with: '2013-04-25 16:00:00' - click_button 'Search' + click_button 'Go' - rows = find("table#listing_orders").all("thead tr") + rows = find("table.report__table").all("thead tr") table = rows.map { |r| r.all("th").map { |c| c.text.strip } } expect(table).to eq([ ["Hub", "Code", "First Name", "Last Name", "Supplier", "Product", "Variant", "Quantity", "TempControlled?"].map(&:upcase) ]) - expect(page).to have_selector 'table#listing_orders tbody tr', count: 5 # Totals row per order + expect(page).to have_selector 'table.report__table tbody tr', count: 5 # Totals row per order end it "sorts alphabetically" do click_link "Pack By Customer" - click_button 'Search' + click_button 'Go' - rows = find("table#listing_orders").all("tr") + rows = find("table.report__table").all("tr") table = rows.map { |r| r.all("th,td").map { |c| c.text.strip }[3] } expect(table).to eq([ "LAST NAME", @@ -79,15 +79,15 @@ describe "Packing Reports", js: true do click_link "Pack By Supplier" fill_in 'q_completed_at_gt', with: '2013-04-25 13:00:00' fill_in 'q_completed_at_lt', with: '2013-04-25 16:00:00' - click_button 'Search' + click_button 'Go' - rows = find("table#listing_orders").all("thead tr") + rows = find("table.report__table").all("thead tr") table = rows.map { |r| r.all("th").map { |c| c.text.strip } } expect(table).to eq([ ["Hub", "Supplier", "Code", "First Name", "Last Name", "Product", "Variant", "Quantity", "TempControlled?"].map(&:upcase) ]) - expect(all('table#listing_orders tbody tr').count).to eq(4) # Totals row per supplier + expect(all('table.report__table tbody tr').count).to eq(4) # Totals row per supplier end end end diff --git a/spec/system/admin/reports/payments_report_spec.rb b/spec/system/admin/reports/payments_report_spec.rb index 21dede0dca..d2e4396466 100644 --- a/spec/system/admin/reports/payments_report_spec.rb +++ b/spec/system/admin/reports/payments_report_spec.rb @@ -37,10 +37,10 @@ describe "Payments Reports" do it "shows orders with payment state, their balance and totals" do visit spree.payments_admin_reports_path - select I18n.t(:report_itemised_payment), from: "report_type" + select I18n.t(:report_itemised_payment), from: "report_subtype" find("[type='submit']").click - expect(page.find("#listing_orders thead tr").text).to have_content([ + expect(page.find("table.report__table thead tr").text).to have_content([ I18n.t(:report_header_payment_state), I18n.t(:report_header_distributor), I18n.t(:report_header_product_total_price, currency: currency_symbol), @@ -49,7 +49,7 @@ describe "Payments Reports" do I18n.t(:report_header_total_price, currency: currency_symbol) ].join(" ").upcase) - expect(page.find("#listing_orders tbody tr").text).to have_content([ + expect(page.find("table.report__table tbody tr").text).to have_content([ order.payment_state, order.distributor.name, order.item_total.to_f + other_order.item_total.to_f, @@ -74,10 +74,10 @@ describe "Payments Reports" do it 'shows orders with payment state, their balance and and payment totals' do visit spree.payments_admin_reports_path - select I18n.t(:report_payment_totals), from: "report_type" + select I18n.t(:report_payment_totals), from: "report_subtype" find("[type='submit']").click - expect(page.find("#listing_orders thead tr").text).to have_content([ + expect(page.find("table.report__table thead tr").text).to have_content([ I18n.t(:report_header_payment_state), I18n.t(:report_header_distributor), I18n.t(:report_header_product_total_price, currency: currency_symbol), @@ -88,7 +88,7 @@ describe "Payments Reports" do I18n.t(:report_header_outstanding_balance_price, currency: currency_symbol), ].join(" ").upcase) - expect(page.find("#listing_orders tbody tr").text).to have_content([ + expect(page.find("table.report__table tbody tr").text).to have_content([ order.payment_state, order.distributor.name, order.item_total.to_f + other_order.item_total.to_f, diff --git a/spec/system/admin/reports_spec.rb b/spec/system/admin/reports_spec.rb index b6c0c7717c..0c2676f64c 100644 --- a/spec/system/admin/reports_spec.rb +++ b/spec/system/admin/reports_spec.rb @@ -33,16 +33,15 @@ describe ' describe "Customers report" do before do - login_as_admin_and_visit spree.admin_reports_path + login_as_admin_and_visit admin_reports_path end it "customers report" do click_link "Mailing List" - expect(page).to have_select('report_type', selected: 'Mailing List') - expect(page).to have_content "click on GO" + expect(page).to have_select('report_subtype', selected: 'Mailing List') click_button "Go" - rows = find("table#listing_customers").all("thead tr") + rows = find("table.report__table").all("thead tr") table = rows.map { |r| r.all("th").map { |c| c.text.strip } } expect(table.sort).to eq([ ["Email", "First Name", "Last Name", "Suburb"].map(&:upcase) @@ -51,10 +50,10 @@ describe ' it "customers report" do click_link "Addresses" - expect(page).to have_select('report_type', selected: 'Addresses') + expect(page).to have_select('report_subtype', selected: 'Addresses') click_button "Go" - rows = find("table#listing_customers").all("thead tr") + rows = find("table.report__table").all("thead tr") table = rows.map { |r| r.all("th").map { |c| c.text.strip } } expect(table.sort).to eq([ ["First Name", "Last Name", "Billing Address", "Email", "Phone", "Hub", "Hub Address", @@ -65,27 +64,27 @@ describe ' describe "Order cycle management report" do before do - login_as_admin_and_visit spree.admin_reports_path + login_as_admin_and_visit admin_reports_path end it "payment method report" do click_link "Payment Methods Report" - click_button "Search" - rows = find("table#listing_ocm_orders").all("thead tr") + click_button "Go" + rows = find("table.report__table").all("thead tr") table = rows.map { |r| r.all("th").map { |c| c.text.strip } } expect(table.sort).to eq([ - ["First Name", "Last Name", "Hub", "Hub Code", "Email", "Phone", "Shipping Method", + ["First Name", "Last Name", "Hub", "Customer Code", "Email", "Phone", "Shipping Method", "Payment Method", "Amount", "Balance"].map(&:upcase) ].sort) end it "delivery report" do click_link "Delivery Report" - click_button "Search" - rows = find("table#listing_ocm_orders").all("thead tr") + click_button "Go" + rows = find("table.report__table").all("thead tr") table = rows.map { |r| r.all("th").map { |c| c.text.strip } } expect(table.sort).to eq([ - ["First Name", "Last Name", "Hub", "Hub Code", "Delivery Address", "Delivery Postcode", + ["First Name", "Last Name", "Hub", "Customer Code", "Delivery Address", "Delivery Postcode", "Phone", "Shipping Method", "Payment Method", "Amount", "Balance", "Temp Controlled Items?", "Special Instructions"].map(&:upcase) ].sort) @@ -93,17 +92,17 @@ describe ' end it "orders and distributors report" do - login_as_admin_and_visit spree.admin_reports_path + login_as_admin_and_visit admin_reports_path click_link 'Orders And Distributors' - click_button 'Search' + click_button 'Go' expect(page).to have_content 'ORDER DATE' end it "payments reports" do - login_as_admin_and_visit spree.admin_reports_path + login_as_admin_and_visit admin_reports_path click_link 'Payment Reports' - click_button 'Search' + click_button 'Go' expect(page).to have_content 'PAYMENT STATE' end @@ -163,9 +162,9 @@ describe ' payment_method: create(:payment_method, distributors: [distributor1])) break unless order1.next! until order1.complete? - login_as_admin_and_visit spree.admin_reports_path + login_as_admin_and_visit admin_reports_path click_link "Sales Tax" - select("Tax types", from: "report_type") + select("Tax Types", from: "report_subtype") end it "reports" do @@ -177,7 +176,7 @@ describe ' # When I filter to just one distributor select user1.enterprises.first.name, from: 'q_distributor_id_eq' - click_button 'Search' + click_button 'Go' # Then I should see the relevant order expect(page).to have_content order1.number.to_s @@ -199,7 +198,7 @@ describe ' describe "orders & fulfilment reports" do it "loads the report page" do - login_as_admin_and_visit spree.admin_reports_path + login_as_admin_and_visit admin_reports_path click_link 'Orders & Fulfillment Reports' expect(page).to have_content 'Supplier' @@ -238,15 +237,17 @@ describe ' it "is precise to time of day, not just date" do # When I generate a customer report # with a timeframe that includes one order but not the other - login_as_admin_and_visit spree.orders_and_fulfillment_admin_reports_path + login_as_admin_and_visit admin_reports_path + click_link 'Orders & Fulfillment Reports' + click_button 'Go' pick_datetime "#q_completed_at_gt", datetime_start pick_datetime "#q_completed_at_lt", datetime_end - select 'Order Cycle Customer Totals', from: 'report_type' - click_button 'Search' + select 'Order Cycle Customer Totals', from: 'report_subtype' + click_button 'Go' # Then I should see the rows for the first order but not the second - expect(all('table#listing_orders tbody tr').count).to eq(4) # Two rows per order + expect(all('table.report__table tbody tr').count).to eq(4) # Two rows per order end end @@ -256,7 +257,9 @@ describe ' orders_open_at: Time.zone.now, orders_close_at: nil) o = create(:order, order_cycle: oc, distributor: distributor) - login_as_admin_and_visit spree.orders_and_fulfillment_admin_reports_path + login_as_admin_and_visit admin_reports_path + click_link 'Orders & Fulfillment Reports' + click_button 'Go' expect(page).to have_content "My Order Cycle" end @@ -294,7 +297,7 @@ describe ' end it "shows products and inventory report" do - login_as_admin_and_visit spree.admin_reports_path + login_as_admin_and_visit admin_reports_path expect(page).to have_content "All products" expect(page).to have_content "Inventory (on hand)" @@ -307,29 +310,29 @@ describe ' expect(page).to have_table_row [product1.supplier.name, product1.supplier.address.city, "Product Name", product1.properties.map(&:presentation).join(", "), - product1.primary_taxon.name, "Test", "100.0", + product1.primary_taxon.name, "Test", "$100.00", product1.group_buy_unit_size.to_s, "", "sku1"] expect(page).to have_table_row [product1.supplier.name, product1.supplier.address.city, "Product Name", product1.properties.map(&:presentation).join(", "), - product1.primary_taxon.name, "Something", "80.0", + product1.primary_taxon.name, "Something", "$80.00", product1.group_buy_unit_size.to_s, "", "sku2"] expect(page).to have_table_row [product2.supplier.name, product1.supplier.address.city, "Product 2", product1.properties.map(&:presentation).join(", "), - product2.primary_taxon.name, "100g", "99.0", + product2.primary_taxon.name, "100g", "$99.00", product1.group_buy_unit_size.to_s, "", "product_sku"] end it "shows the LettuceShare report" do - login_as_admin_and_visit spree.admin_reports_path + login_as_admin_and_visit admin_reports_path click_link 'LettuceShare' click_button "Go" expect(page).to have_table_row ['PRODUCT', 'Description', 'Qty', 'Pack Size', 'Unit', 'Unit Price', 'Total', 'GST incl.', 'Grower and growing method', 'Taxon'].map(&:upcase) - expect(page).to have_table_row ['Product 2', '100g', '', '100', 'g', '99.0', '', '0', + expect(page).to have_table_row ['Product 2', '100g', '', '100', 'g', '$99.00', '', '0', 'Supplier Name (Organic - NASAA 12345)', 'Taxon Name'] end end @@ -342,15 +345,15 @@ describe ' before do enterprise3.enterprise_roles.build( user: enterprise1.owner ).save - login_as_admin_and_visit spree.admin_reports_path + login_as_admin_and_visit admin_reports_path click_link 'Users & Enterprises' end it "shows users and enterprises report" do - click_button "Search" + click_button "Go" - rows = find("table#users_and_enterprises").all("tr") + rows = find("table.report__table").all("tr") table = rows.map { |r| r.all("th,td").map { |c| c.text.strip }[0..2] } expect(table.sort).to eq([ @@ -369,9 +372,9 @@ describe ' select enterprise3.name, from: "enterprise_id_in" select enterprise1.owner.email, from: "user_id_in" - click_button "Search" + click_button "Go" - rows = find("table#users_and_enterprises").all("tr") + rows = find("table.report__table").all("tr") table = rows.map { |r| r.all("th,td").map { |c| c.text.strip }[0..2] } expect(table.sort).to eq([ @@ -381,6 +384,76 @@ describe ' end end + describe 'bulk coop report' do + before do + login_as_admin_and_visit admin_reports_path + click_link 'Bulk Co-Op' + end + + it "generating Bulk Co-op Supplier Report" do + select "Bulk Co-op Supplier Report", from: "report_subtype" + click_button 'Go' + + expect(page).to have_table_row [ + "Supplier", + "Product", + "Bulk Unit Size", + "Variant", + "Variant Value", + "Variant Unit", + "Weight", + "Sum Total", + "Units Required", + "Unallocated", + "Max Quantity Excess" + ].map(&:upcase) + end + + it "generating Bulk Co-op Allocation report" do + select "Bulk Co-op Allocation", from: "report_subtype" + click_button 'Go' + + expect(page).to have_table_row [ + "Customer", + "Product", + "Bulk Unit Size", + "Variant", + "Variant Value", + "Variant Unit", + "Weight", + "Sum Total", + "Total available", + "Unallocated", + "Max Quantity Excess" + ].map(&:upcase) + end + + it "generating Bulk Co-op Packing Sheets report" do + select "Bulk Co-op Packing Sheets", from: "report_subtype" + click_button 'Go' + + expect(page).to have_table_row [ + "Customer", + "Product", + "Variant", + "Sum Total" + ].map(&:upcase) + end + + it "generating Bulk Co-op Customer Payments report" do + select "Bulk Co-op Customer Payments", from: "report_subtype" + click_button 'Go' + + expect(page).to have_table_row [ + "Customer", + "Date of Order", + "Total Cost", + "Amount Owing", + "Amount Paid" + ].map(&:upcase) + end + end + describe "Xero invoices report" do let(:distributor1) { create(:distributor_enterprise, with_payment_and_shipping: true, charges_sales_tax: true) @@ -470,7 +543,7 @@ describe ' order1.reload order1.create_tax_charge! - login_as_admin_and_visit spree.admin_reports_path + login_as_admin_and_visit admin_reports_path click_link 'Xero Invoices' end @@ -481,7 +554,7 @@ describe ' end it "shows Xero invoices report" do - click_button "Search" + click_button "Go" expect(xero_invoice_table).to match_table [ xero_invoice_header, xero_invoice_summary_row('Total untaxable produce (no tax)', 12.54, @@ -507,10 +580,10 @@ describe ' pick_datetime '#due_date', Date.new(2021, 3, 12) fill_in 'account_code', with: 'abc123' - click_button 'Search' + click_button 'Go' - opts = { invoice_number: '5', invoice_date: '2021-02-12 00:00', - due_date: '2021-03-12 00:00', account_code: 'abc123' } + opts = { invoice_number: '5', invoice_date: '2021-02-12', + due_date: '2021-03-12', account_code: 'abc123' } expect(xero_invoice_table).to match_table [ xero_invoice_header, @@ -532,8 +605,8 @@ describe ' end it "generates a detailed report" do - select 'Detailed', from: 'report_type' - click_button 'Search' + select 'Detailed', from: 'report_subtype' + click_button 'Go' opts = {} @@ -556,7 +629,7 @@ describe ' private def xero_invoice_table - find("table#listing_invoices") + find("table.report__table") end def xero_invoice_header 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