mirror of
https://github.com/openfoodfoundation/openfoodnetwork
synced 2026-03-04 02:31:33 +00:00
Pull in reports POC work replacing Packing reports
This commit is contained in:
1
Gemfile
1
Gemfile
@@ -96,6 +96,7 @@ gem 'wkhtmltopdf-binary'
|
||||
|
||||
gem 'immigrant'
|
||||
gem 'roo', '~> 2.8.3'
|
||||
gem 'spreadsheet_architect'
|
||||
|
||||
gem 'whenever', require: false
|
||||
|
||||
|
||||
19
Gemfile.lock
19
Gemfile.lock
@@ -159,6 +159,9 @@ GEM
|
||||
aws-sdk-v1 (1.67.0)
|
||||
json (~> 1.4)
|
||||
nokogiri (~> 1)
|
||||
axlsx_styler (1.1.0)
|
||||
activesupport (>= 3.1)
|
||||
caxlsx (>= 2.0.2)
|
||||
bcrypt (3.1.16)
|
||||
bigdecimal (3.0.2)
|
||||
bindex (0.8.1)
|
||||
@@ -184,6 +187,11 @@ GEM
|
||||
rack-test (>= 0.6.3)
|
||||
regexp_parser (>= 1.5, < 3.0)
|
||||
xpath (~> 3.2)
|
||||
caxlsx (3.1.1)
|
||||
htmlentities (~> 4.3, >= 4.3.4)
|
||||
marcel (~> 1.0)
|
||||
nokogiri (~> 1.10, >= 1.10.4)
|
||||
rubyzip (>= 1.3.0, < 3)
|
||||
childprocess (4.1.0)
|
||||
chronic (0.10.2)
|
||||
chunky_png (1.4.0)
|
||||
@@ -255,6 +263,7 @@ GEM
|
||||
dotenv-rails (2.7.6)
|
||||
dotenv (= 2.7.6)
|
||||
railties (>= 3.2)
|
||||
dry-inflector (0.2.1)
|
||||
e2mmap (0.1.0)
|
||||
erubi (1.10.0)
|
||||
et-orbi (1.2.4)
|
||||
@@ -334,6 +343,7 @@ GEM
|
||||
hashery (2.1.2)
|
||||
highline (2.0.3)
|
||||
hiredis (0.6.3)
|
||||
htmlentities (4.3.4)
|
||||
i18n (1.8.10)
|
||||
concurrent-ruby (~> 1.0)
|
||||
i18n-js (3.9.0)
|
||||
@@ -509,6 +519,10 @@ GEM
|
||||
roadie-rails (2.2.0)
|
||||
railties (>= 5.1, < 6.2)
|
||||
roadie (>= 3.1, < 5.0)
|
||||
rodf (1.1.1)
|
||||
builder (>= 3.0)
|
||||
dry-inflector (~> 0.1)
|
||||
rubyzip (>= 1.0)
|
||||
roo (2.8.3)
|
||||
nokogiri (~> 1)
|
||||
rubyzip (>= 1.3.0, < 3.0.0)
|
||||
@@ -601,6 +615,10 @@ GEM
|
||||
simplecov_json_formatter (~> 0.1)
|
||||
simplecov-html (0.12.3)
|
||||
simplecov_json_formatter (0.1.3)
|
||||
spreadsheet_architect (4.2.0)
|
||||
axlsx_styler (>= 1.0.0, < 2)
|
||||
caxlsx (>= 2.0.2, < 4)
|
||||
rodf (>= 1.0.0, < 2)
|
||||
spring (3.0.0)
|
||||
spring-commands-rspec (1.0.4)
|
||||
spring (>= 0.9.1)
|
||||
@@ -790,6 +808,7 @@ DEPENDENCIES
|
||||
sidekiq
|
||||
sidekiq-scheduler
|
||||
simplecov
|
||||
spreadsheet_architect
|
||||
spring
|
||||
spring-commands-rspec
|
||||
state_machines-activerecord
|
||||
|
||||
@@ -15,3 +15,17 @@
|
||||
.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;
|
||||
}
|
||||
}
|
||||
|
||||
54
app/controllers/admin/reports_controller.rb
Normal file
54
app/controllers/admin/reports_controller.rb
Normal file
@@ -0,0 +1,54 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module Admin
|
||||
class ReportsController < Spree::Admin::BaseController
|
||||
include ReportsActions
|
||||
helper ReportsHelper
|
||||
|
||||
before_action :authorize_report
|
||||
|
||||
def show
|
||||
render_report && return if ransack_params.blank?
|
||||
|
||||
@report = report_class.new(spree_current_user, ransack_params, report_options)
|
||||
|
||||
if export_spreadsheet?
|
||||
export_report
|
||||
else
|
||||
render_report
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def export_report
|
||||
render report_format.to_sym => @report.public_send("to_#{report_format}"),
|
||||
:filename => report_filename
|
||||
end
|
||||
|
||||
def render_report
|
||||
assign_view_data
|
||||
load_form_options
|
||||
|
||||
render report_type
|
||||
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
|
||||
|
||||
def load_form_options
|
||||
return unless form_options_required?
|
||||
|
||||
form_options = Reports::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
|
||||
end
|
||||
end
|
||||
end
|
||||
38
app/controllers/api/v0/reports_controller.rb
Normal file
38
app/controllers/api/v0/reports_controller.rb
Normal file
@@ -0,0 +1,38 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module Api
|
||||
module V0
|
||||
class ReportsController < Api::V0::BaseController
|
||||
include ReportsActions
|
||||
|
||||
rescue_from Reports::Errors::Base, with: :render_error
|
||||
|
||||
before_action :validate_report, :authorize_report, :validate_query
|
||||
|
||||
def show
|
||||
@report = report_class.new(current_api_user, ransack_params, report_options)
|
||||
|
||||
render_report
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def render_report
|
||||
render json: @report.as_hashes
|
||||
end
|
||||
|
||||
def render_error(error)
|
||||
render json: { error: error.message }, status: :unprocessable_entity
|
||||
end
|
||||
|
||||
def validate_report
|
||||
raise Reports::Errors::NoReportType if report_type.blank?
|
||||
raise Reports::Errors::ReportNotFound if report_class.blank?
|
||||
end
|
||||
|
||||
def validate_query
|
||||
raise Reports::Errors::MissingQueryParams if ransack_params.blank?
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
62
app/controllers/concerns/reports_actions.rb
Normal file
62
app/controllers/concerns/reports_actions.rb
Normal file
@@ -0,0 +1,62 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module ReportsActions
|
||||
extend ActiveSupport::Concern
|
||||
|
||||
private
|
||||
|
||||
def authorize_report
|
||||
authorize! report_type&.to_sym, :report
|
||||
end
|
||||
|
||||
def report_class
|
||||
return if report_type.blank?
|
||||
|
||||
report_loader.report_class
|
||||
end
|
||||
|
||||
def report_loader
|
||||
@report_loader ||= Reports::ReportLoader.new(report_type, report_subtype)
|
||||
end
|
||||
|
||||
def report_type
|
||||
params[:report_type]
|
||||
end
|
||||
|
||||
def report_subtype
|
||||
params[:report_subtype]
|
||||
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
|
||||
|
||||
def file_timestamp
|
||||
Time.zone.now.strftime("%Y%m%d")
|
||||
end
|
||||
|
||||
def i18n_scope
|
||||
'admin.reports'
|
||||
end
|
||||
end
|
||||
@@ -11,7 +11,6 @@ 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/packing_report'
|
||||
require 'open_food_network/sales_tax_report'
|
||||
require 'open_food_network/xero_invoices_report'
|
||||
require 'open_food_network/payments_report'
|
||||
@@ -21,6 +20,7 @@ module Spree
|
||||
module Admin
|
||||
class ReportsController < Spree::Admin::BaseController
|
||||
include Spree::ReportsHelper
|
||||
helper ::ReportsHelper
|
||||
|
||||
ORDER_MANAGEMENT_ENGINE_REPORTS = [
|
||||
:bulk_coop,
|
||||
@@ -31,8 +31,8 @@ module Spree
|
||||
|
||||
before_action :cache_search_state
|
||||
# Fetches user's distributors, suppliers and order_cycles
|
||||
before_action :load_data,
|
||||
only: [:customers, :products_and_inventory, :order_cycle_management, :packing]
|
||||
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
|
||||
|
||||
@@ -69,22 +69,6 @@ module Spree
|
||||
"order_cycle_management_#{timestamp}.csv")
|
||||
end
|
||||
|
||||
def packing
|
||||
raw_params[:q] ||= {}
|
||||
|
||||
@report_types = report_types[:packing]
|
||||
@report_type = params[:report_type]
|
||||
|
||||
# Add distributors/suppliers distributing/supplying products I distribute/supply
|
||||
add_appropriate_distributors_and_suppliers
|
||||
|
||||
# -- Build Report with Order Grouper
|
||||
@report = OpenFoodNetwork::PackingReport.new spree_current_user, raw_params, render_content?
|
||||
@table = order_grouper_table
|
||||
|
||||
render_report(@report.header, @table, params[:csv], "packing_#{timestamp}.csv")
|
||||
end
|
||||
|
||||
def orders_and_distributors
|
||||
@report = OpenFoodNetwork::OrderAndDistributorReport.new spree_current_user,
|
||||
raw_params,
|
||||
@@ -119,11 +103,6 @@ module Spree
|
||||
def orders_and_fulfillment
|
||||
raw_params[:q] ||= orders_and_fulfillment_default_filters
|
||||
|
||||
# Add distributors/suppliers distributing/supplying products I distribute/supply
|
||||
add_appropriate_distributors_and_suppliers
|
||||
|
||||
@order_cycles = my_order_cycles
|
||||
|
||||
@report_types = report_types[:orders_and_fulfillment]
|
||||
@report_type = params[:report_type]
|
||||
|
||||
@@ -218,13 +197,12 @@ module Spree
|
||||
# Rendering HTML is the default.
|
||||
end
|
||||
|
||||
def add_appropriate_distributors_and_suppliers
|
||||
# -- Prepare Form Options
|
||||
permissions = OpenFoodNetwork::Permissions.new(spree_current_user)
|
||||
# My distributors and any distributors distributing products I supply
|
||||
@distributors = permissions.visible_enterprises_for_order_reports.is_distributor
|
||||
# My suppliers and any suppliers supplying products I distribute
|
||||
@suppliers = permissions.visible_enterprises_for_order_reports.is_primary_producer
|
||||
def load_associated_data
|
||||
form_options = Reports::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)
|
||||
@@ -234,7 +212,7 @@ module Spree
|
||||
end
|
||||
end
|
||||
|
||||
def load_data
|
||||
def load_basic_data
|
||||
@distributors = my_distributors
|
||||
@suppliers = my_suppliers | suppliers_of_products_distributed_by(@distributors)
|
||||
@order_cycles = my_order_cycles
|
||||
@@ -310,7 +288,7 @@ module Spree
|
||||
spree.public_send("#{report}_admin_reports_url".to_sym)
|
||||
end
|
||||
rescue NoMethodError
|
||||
url_for([:new, :admin, :reports, report.to_s.singularize])
|
||||
main_app.admin_reports_url(report_type: report)
|
||||
end
|
||||
|
||||
# List of reports that have been moved to the Order Management engine
|
||||
|
||||
15
app/helpers/reports_helper.rb
Normal file
15
app/helpers/reports_helper.rb
Normal file
@@ -0,0 +1,15 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module ReportsHelper
|
||||
def report_order_cycle_options(order_cycles)
|
||||
order_cycles.map do |oc|
|
||||
orders_open_at = oc.orders_open_at&.to_s(:short) || 'NA'
|
||||
orders_close_at = oc.orders_close_at&.to_s(:short) || 'NA'
|
||||
["#{oc.name} (#{orders_open_at} - #{orders_close_at})".html_safe, oc.id]
|
||||
end
|
||||
end
|
||||
|
||||
def report_subtypes(report)
|
||||
Reports::ReportLoader.new(report).report_subtypes
|
||||
end
|
||||
end
|
||||
@@ -4,14 +4,6 @@ require 'spree/money'
|
||||
|
||||
module Spree
|
||||
module ReportsHelper
|
||||
def report_order_cycle_options(order_cycles)
|
||||
order_cycles.map do |oc|
|
||||
orders_open_at = oc.orders_open_at&.to_s(:short) || 'NA'
|
||||
orders_close_at = oc.orders_close_at&.to_s(:short) || 'NA'
|
||||
["#{oc.name} (#{orders_open_at} - #{orders_close_at})".html_safe, oc.id]
|
||||
end
|
||||
end
|
||||
|
||||
def report_payment_method_options(orders)
|
||||
orders.map do |order|
|
||||
payment_method = order.payments.first&.payment_method
|
||||
|
||||
@@ -239,6 +239,7 @@ module Spree
|
||||
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
|
||||
end
|
||||
|
||||
9
app/views/admin/reports/_date_range_form.html.haml
Normal file
9
app/views/admin/reports/_date_range_form.html.haml
Normal file
@@ -0,0 +1,9 @@
|
||||
.row.date-range-filter
|
||||
= label_tag nil, t(:date_range)
|
||||
%br
|
||||
= label_tag nil, t(:start), :class => 'inline'
|
||||
= text_field_tag "q[completed_at_gt]", params.dig(:q, :completed_at_gt), :class => 'datetimepicker datepicker-from'
|
||||
%span.range-divider
|
||||
%i.icon-arrow-right
|
||||
= text_field_tag "q[completed_at_lt]", params.dig(:q, :completed_at_lt), :class => 'datetimepicker datepicker-to'
|
||||
= label_tag nil, t(:end), :class => 'inline'
|
||||
8
app/views/admin/reports/_rendering_options.html.haml
Normal file
8
app/views/admin/reports/_rendering_options.html.haml
Normal file
@@ -0,0 +1,8 @@
|
||||
.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'})
|
||||
|
||||
.inline-checkbox
|
||||
= check_box_tag "options[exclude_summaries]", true, params[:options].andand[:exclude_summaries]
|
||||
= label_tag t(".hide_summary_rows")
|
||||
20
app/views/admin/reports/_table.html.haml
Normal file
20
app/views/admin/reports/_table.html.haml
Normal file
@@ -0,0 +1,20 @@
|
||||
- if params[:q].present?
|
||||
%table.report__table{id: id}
|
||||
%thead
|
||||
%tr
|
||||
- @report.table_headers.each do |heading|
|
||||
%th
|
||||
= t("admin.reports.table.headings.#{heading}")
|
||||
%tbody
|
||||
- @report.table_rows.each do |row|
|
||||
- if row
|
||||
%tr
|
||||
- row.each do |cell|
|
||||
%td
|
||||
= cell
|
||||
- if @report.table_rows.empty?
|
||||
%tr
|
||||
%td{colspan: @report.table_headers.count}= t(:none)
|
||||
- else
|
||||
%p.report__message
|
||||
= t(".select_and_search", option: msg_option.upcase)
|
||||
31
app/views/admin/reports/packing.html.haml
Normal file
31
app/views/admin/reports/packing.html.haml
Normal file
@@ -0,0 +1,31 @@
|
||||
= 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", "distributor_id_in", @distributors, :id, :name, {selected: params.dig(:q, :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[: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)
|
||||
@@ -1,4 +1,5 @@
|
||||
%ul{style: "margin-left: 12pt"}
|
||||
- report_types.each do |report_type|
|
||||
- report_subtypes("packing").each do |report_subtype|
|
||||
%li
|
||||
= link_to report_type[0], "#{packing_admin_reports_url}?report_type=#{report_type[1]}"
|
||||
= link_to t("admin.reports.packing.#{report_subtype}_report"),
|
||||
main_app.admin_reports_url(report_type: 'packing', report_subtype: report_subtype)
|
||||
|
||||
@@ -1,30 +0,0 @@
|
||||
= form_for @report.search, :url => spree.packing_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)
|
||||
@@ -152,6 +152,14 @@ module Openfoodnetwork
|
||||
#{config.root}/app/jobs
|
||||
)
|
||||
|
||||
initializer "ofn.reports" do |_app|
|
||||
module ::Reports; end
|
||||
loader = Zeitwerk::Loader.new
|
||||
loader.push_dir("#{Rails.root}/lib/reports", namespace: ::Reports)
|
||||
loader.setup
|
||||
loader.eager_load
|
||||
end
|
||||
|
||||
config.paths["config/routes.rb"] = %w(
|
||||
config/routes/api.rb
|
||||
config/routes.rb
|
||||
|
||||
@@ -1191,11 +1191,39 @@ en:
|
||||
xero_invoices:
|
||||
name: Xero Invoices
|
||||
description: Invoices for import into Xero
|
||||
packing:
|
||||
name: Packing Reports
|
||||
enterprise_fee_summary:
|
||||
name: "Enterprise Fee Summary"
|
||||
description: "Summary of Enterprise Fees collected"
|
||||
errors:
|
||||
no_report_type: "Please specify a report type"
|
||||
report_not_found: "Report not found"
|
||||
missing_ransack_params: "Please supply Ransack search params in the request"
|
||||
hidden_field: "< Hidden >"
|
||||
summary_row:
|
||||
total: "TOTAL"
|
||||
table:
|
||||
select_and_search: "Select filters and click on %{option} to access your data."
|
||||
headings:
|
||||
hub: "Hub"
|
||||
customer_code: "Code"
|
||||
first_name: "First Name"
|
||||
last_name: "Last Name"
|
||||
supplier: "Supplier"
|
||||
product: "Product"
|
||||
variant: "Variant"
|
||||
quantity: "Quantity"
|
||||
is_temperature_controlled: "TempControlled?"
|
||||
rendering_options:
|
||||
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"
|
||||
packing:
|
||||
name: "Packing Reports"
|
||||
customer_report: "Pack By Customer"
|
||||
supplier_report: "Pack By Supplier"
|
||||
subscriptions:
|
||||
index:
|
||||
title: "Subscriptions"
|
||||
|
||||
@@ -114,5 +114,7 @@ Openfoodnetwork::Application.routes.draw do
|
||||
put :cancel, on: :member, format: :json
|
||||
put :resume, on: :member, format: :json
|
||||
end
|
||||
|
||||
match '/reports/:report_type(/:report_subtype)', to: 'reports#show', via: [:get, :post], as: :reports
|
||||
end
|
||||
end
|
||||
|
||||
@@ -77,6 +77,8 @@ Openfoodnetwork::Application.routes.draw do
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
get '/reports/:report_type(/:report_subtype)', to: 'reports#show'
|
||||
end
|
||||
|
||||
match '*path', to: redirect(path: "/api/v0/%{path}"), via: :all, constraints: { path: /(?!v[0-9]).+/ }
|
||||
|
||||
@@ -33,7 +33,6 @@ Spree::Core::Engine.routes.draw do
|
||||
|
||||
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/packing' => 'admin/reports#packing', :as => "packing_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]
|
||||
@@ -127,7 +126,7 @@ Spree::Core::Engine.routes.draw do
|
||||
end
|
||||
end
|
||||
|
||||
resources :reports
|
||||
resources :reports, only: :index
|
||||
|
||||
resources :users do
|
||||
member do
|
||||
|
||||
@@ -1,160 +0,0 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
require "open_food_network/reports/line_items"
|
||||
|
||||
module OpenFoodNetwork
|
||||
class PackingReport
|
||||
attr_reader :params
|
||||
|
||||
def initialize(user, params = {}, render_table = false)
|
||||
@params = params
|
||||
@user = user
|
||||
@render_table = render_table
|
||||
end
|
||||
|
||||
def header
|
||||
if is_by_customer?
|
||||
[
|
||||
I18n.t(:report_header_hub),
|
||||
I18n.t(:report_header_code),
|
||||
I18n.t(:report_header_first_name),
|
||||
I18n.t(:report_header_last_name),
|
||||
I18n.t(:report_header_supplier),
|
||||
I18n.t(:report_header_product),
|
||||
I18n.t(:report_header_variant),
|
||||
I18n.t(:report_header_quantity),
|
||||
I18n.t(:report_header_temp_controlled),
|
||||
]
|
||||
else
|
||||
[
|
||||
I18n.t(:report_header_hub),
|
||||
I18n.t(:report_header_supplier),
|
||||
I18n.t(:report_header_code),
|
||||
I18n.t(:report_header_first_name),
|
||||
I18n.t(:report_header_last_name),
|
||||
I18n.t(:report_header_product),
|
||||
I18n.t(:report_header_variant),
|
||||
I18n.t(:report_header_quantity),
|
||||
I18n.t(:report_header_temp_controlled),
|
||||
]
|
||||
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
|
||||
if is_by_customer?
|
||||
[
|
||||
{ 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.lastname.downcase },
|
||||
summary_columns: [proc { |_line_items| "" },
|
||||
proc { |_line_items| "" },
|
||||
proc { |_line_items| "" },
|
||||
proc { |_line_items| "" },
|
||||
proc { |_line_items| "" },
|
||||
proc { |_line_items| I18n.t('admin.reports.total_items') },
|
||||
proc { |_line_items| "" },
|
||||
proc { |line_items| line_items.to_a.sum(&:quantity) },
|
||||
proc { |_line_items| "" }] },
|
||||
{ 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 } },
|
||||
{ group_by: proc { |line_item| line_item.full_name },
|
||||
sort_by: proc { |full_name| full_name } }
|
||||
]
|
||||
else
|
||||
[{ group_by: proc { |line_item| line_item.order.distributor },
|
||||
sort_by: proc { |distributor| distributor.name } },
|
||||
{ group_by: proc { |line_item| line_item.product.supplier },
|
||||
sort_by: proc { |supplier| supplier.name },
|
||||
summary_columns: [proc { |_line_items| "" },
|
||||
proc { |_line_items| "" },
|
||||
proc { |_line_items| "" },
|
||||
proc { |_line_items| "" },
|
||||
proc { |_line_items| "" },
|
||||
proc { |_line_items| I18n.t('admin.reports.total_items') },
|
||||
proc { |_line_items| "" },
|
||||
proc { |line_items| line_items.to_a.sum(&:quantity) },
|
||||
proc { |_line_items| "" }] },
|
||||
{ group_by: proc { |line_item| line_item.product },
|
||||
sort_by: proc { |product| product.name } },
|
||||
{ group_by: proc { |line_item| line_item.full_name },
|
||||
sort_by: proc { |full_name| full_name } },
|
||||
{ group_by: proc { |line_item| line_item.order },
|
||||
sort_by: proc { |order| order.bill_address.lastname.downcase } }]
|
||||
end
|
||||
end
|
||||
|
||||
def columns
|
||||
if is_by_customer?
|
||||
[proc { |line_items| line_items.first.order.distributor.name },
|
||||
proc { |line_items| customer_code(line_items.first.order) },
|
||||
proc { |line_items| line_items.first.order.bill_address.firstname },
|
||||
proc { |line_items| line_items.first.order.bill_address.lastname },
|
||||
proc { |line_items| line_items.first.product.supplier.name },
|
||||
proc { |line_items| line_items.first.product.name },
|
||||
proc { |line_items| line_items.first.full_name },
|
||||
proc { |line_items| line_items.to_a.sum(&:quantity) },
|
||||
proc { |line_items| is_temperature_controlled?(line_items.first) }]
|
||||
else
|
||||
[
|
||||
proc { |line_items| line_items.first.order.distributor.name },
|
||||
proc { |line_items| line_items.first.product.supplier.name },
|
||||
proc { |line_items| customer_code(line_items.first.order) },
|
||||
proc { |line_items| line_items.first.order.bill_address.firstname },
|
||||
proc { |line_items| line_items.first.order.bill_address.lastname },
|
||||
proc { |line_items| line_items.first.product.name },
|
||||
proc { |line_items| line_items.first.full_name },
|
||||
proc { |line_items| line_items.to_a.sum(&:quantity) },
|
||||
proc { |line_items| is_temperature_controlled?(line_items.first) }
|
||||
]
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def line_item_includes
|
||||
[{ option_values: :option_type,
|
||||
order: [:bill_address, :distributor, :customer],
|
||||
variant: { product: [:supplier, :shipping_category] } }]
|
||||
end
|
||||
|
||||
def order_permissions
|
||||
return @order_permissions unless @order_permissions.nil?
|
||||
|
||||
@order_permissions = ::Permissions::Order.new(@user, @params[:q])
|
||||
end
|
||||
|
||||
def is_temperature_controlled?(line_item)
|
||||
if line_item.product.shipping_category&.temperature_controlled
|
||||
"Yes"
|
||||
else
|
||||
"No"
|
||||
end
|
||||
end
|
||||
|
||||
def is_by_customer?
|
||||
params[:report_type] == "pack_by_customer"
|
||||
end
|
||||
|
||||
def customer_code(order)
|
||||
customer = order.customer
|
||||
customer.nil? ? "" : customer.code
|
||||
end
|
||||
|
||||
def report_line_items
|
||||
@report_line_items ||= Reports::LineItems.new(order_permissions, @params)
|
||||
end
|
||||
end
|
||||
end
|
||||
29
lib/reports/errors.rb
Normal file
29
lib/reports/errors.rb
Normal file
@@ -0,0 +1,29 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module Reports
|
||||
module Errors
|
||||
class Base < StandardError
|
||||
def i18n_error_scope
|
||||
'admin.reports.errors'
|
||||
end
|
||||
end
|
||||
|
||||
class NoReportType < Base
|
||||
def message
|
||||
I18n.t('no_report_type', scope: i18n_error_scope)
|
||||
end
|
||||
end
|
||||
|
||||
class ReportNotFound < Base
|
||||
def message
|
||||
I18n.t('report_not_found', scope: i18n_error_scope)
|
||||
end
|
||||
end
|
||||
|
||||
class MissingQueryParams < Base
|
||||
def message
|
||||
I18n.t('missing_ransack_params', scope: i18n_error_scope)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
34
lib/reports/frontend_data.rb
Normal file
34
lib/reports/frontend_data.rb
Normal file
@@ -0,0 +1,34 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module Reports
|
||||
class FrontendData
|
||||
def initialize(current_user)
|
||||
@current_user = current_user
|
||||
end
|
||||
|
||||
def distributors
|
||||
permissions.visible_enterprises_for_order_reports.is_distributor.
|
||||
select("enterprises.id, enterprises.name")
|
||||
end
|
||||
|
||||
def suppliers
|
||||
permissions.visible_enterprises_for_order_reports.is_primary_producer.
|
||||
select("enterprises.id, enterprises.name")
|
||||
end
|
||||
|
||||
def order_cycles
|
||||
OrderCycle.
|
||||
active_or_complete.
|
||||
visible_by(current_user).
|
||||
order('order_cycles.orders_close_at DESC')
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
attr_reader :current_user
|
||||
|
||||
def permissions
|
||||
@permissions ||= OpenFoodNetwork::Permissions.new(current_user)
|
||||
end
|
||||
end
|
||||
end
|
||||
17
lib/reports/packing/base.rb
Normal file
17
lib/reports/packing/base.rb
Normal file
@@ -0,0 +1,17 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module Reports
|
||||
module Packing
|
||||
class Base < ReportTemplate
|
||||
SUBTYPES = ["customer", "supplier"]
|
||||
|
||||
|
||||
|
||||
private
|
||||
|
||||
def i18n_scope
|
||||
"admin.reports"
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
9
lib/reports/packing/customer.rb
Normal file
9
lib/reports/packing/customer.rb
Normal file
@@ -0,0 +1,9 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module Reports
|
||||
module Packing
|
||||
class Customer < Base
|
||||
|
||||
end
|
||||
end
|
||||
end
|
||||
9
lib/reports/packing/supplier.rb
Normal file
9
lib/reports/packing/supplier.rb
Normal file
@@ -0,0 +1,9 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module Reports
|
||||
module Packing
|
||||
class Supplier < Base
|
||||
|
||||
end
|
||||
end
|
||||
end
|
||||
40
lib/reports/report_loader.rb
Normal file
40
lib/reports/report_loader.rb
Normal file
@@ -0,0 +1,40 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module Reports
|
||||
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
|
||||
rescue NameError
|
||||
raise Reports::Errors::ReportNotFound
|
||||
end
|
||||
|
||||
def default_report_subtype
|
||||
report_subtypes.first || "base"
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
attr_reader :report_type, :report_subtype
|
||||
|
||||
def report_module
|
||||
"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 Reports::Errors::ReportNotFound
|
||||
end
|
||||
end
|
||||
end
|
||||
67
lib/reports/report_renderer.rb
Normal file
67
lib/reports/report_renderer.rb
Normal file
@@ -0,0 +1,67 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
require 'spreadsheet_architect'
|
||||
|
||||
module Reports
|
||||
class ReportRenderer
|
||||
def initialize(report)
|
||||
@report = report
|
||||
end
|
||||
|
||||
def table_headers
|
||||
as_arrays.first
|
||||
end
|
||||
|
||||
def table_rows
|
||||
as_arrays.drop(1)
|
||||
end
|
||||
|
||||
def as_hashes
|
||||
report_rows
|
||||
end
|
||||
|
||||
def as_arrays
|
||||
@as_arrays ||= rows_as_arrays
|
||||
end
|
||||
|
||||
def to_csv
|
||||
SpreadsheetArchitect.to_csv(headers: table_headers, data: table_rows)
|
||||
end
|
||||
|
||||
def to_ods
|
||||
SpreadsheetArchitect.to_ods(headers: table_headers, data: table_rows)
|
||||
end
|
||||
|
||||
def to_xlsx
|
||||
SpreadsheetArchitect.to_xlsx(headers: table_headers, data: table_rows)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def report_rows
|
||||
@report.report_rows
|
||||
end
|
||||
|
||||
def rows_as_arrays
|
||||
report_array = [header_row]
|
||||
|
||||
report_rows.each do |row|
|
||||
report_array << row_or_summary(row)
|
||||
end
|
||||
|
||||
report_array
|
||||
end
|
||||
|
||||
def header_row
|
||||
report_rows.first.keys - [:summary_row_title]
|
||||
end
|
||||
|
||||
def row_or_summary(row)
|
||||
summary_row_title = row.delete :summary_row_title
|
||||
row_values = row.values
|
||||
row_values[0] = summary_row_title if summary_row_title
|
||||
|
||||
row_values
|
||||
end
|
||||
end
|
||||
end
|
||||
42
lib/reports/report_template.rb
Normal file
42
lib/reports/report_template.rb
Normal file
@@ -0,0 +1,42 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module Reports
|
||||
class ReportTemplate
|
||||
delegate :as_hashes, :as_arrays, :table_headers, :table_rows,
|
||||
:to_csv, :to_xlsx, :to_ods, :to_json, to: :report_renderer
|
||||
|
||||
attr_reader :options
|
||||
attr_accessor :report_rows
|
||||
|
||||
SUBTYPES = []
|
||||
|
||||
def self.report_subtypes
|
||||
self::SUBTYPES
|
||||
end
|
||||
|
||||
def initialize(current_user, ransack_params, options = {})
|
||||
@current_user = current_user
|
||||
@ransack_params = ransack_params.with_indifferent_access
|
||||
@options = ( options || {} ).with_indifferent_access
|
||||
@report_rows = []
|
||||
|
||||
build_report
|
||||
end
|
||||
|
||||
def headers
|
||||
report_rows.first&.keys || []
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
attr_reader :current_user, :ransack_params
|
||||
|
||||
def build_report
|
||||
# TODO
|
||||
end
|
||||
|
||||
def report_renderer
|
||||
@report_renderer ||= ReportRenderer.new(self)
|
||||
end
|
||||
end
|
||||
end
|
||||
119
spec/controllers/api/v0/reports/packing_report_spec.rb
Normal file
119
spec/controllers/api/v0/reports/packing_report_spec.rb
Normal file
@@ -0,0 +1,119 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
require "spec_helper"
|
||||
|
||||
describe Api::V0::ReportsController, type: :controller do
|
||||
let(:params) {
|
||||
{
|
||||
report_type: 'packing',
|
||||
q: { created_at_lt: Time.zone.now }
|
||||
}
|
||||
}
|
||||
|
||||
before do
|
||||
allow(controller).to receive(:spree_current_user) { current_user }
|
||||
end
|
||||
|
||||
describe "packing report" do
|
||||
context "as a regular user" do
|
||||
let(:current_user) { create(:user) }
|
||||
|
||||
it "does not show reports" do
|
||||
api_get :show, params
|
||||
|
||||
assert_unauthorized!
|
||||
end
|
||||
end
|
||||
|
||||
context "as an enterprise user with full order permissions (distributor)" do
|
||||
let!(:distributor) { create(:distributor_enterprise) }
|
||||
let!(:order) { create(:completed_order_with_totals, distributor: distributor) }
|
||||
let(:current_user) { distributor.owner }
|
||||
|
||||
it "renders results" do
|
||||
api_get :show, params
|
||||
|
||||
expect(response.status).to eq 200
|
||||
expect(json_response).to match_array report_output(order, "distributor")
|
||||
end
|
||||
end
|
||||
|
||||
context "as an enterprise user with partial order permissions (supplier with P-OC)" do
|
||||
let!(:order) { create(:completed_order_with_totals) }
|
||||
let(:supplier) { order.line_items.first.product.supplier }
|
||||
let(:current_user) { supplier.owner }
|
||||
let!(:perms) {
|
||||
create(:enterprise_relationship, parent: supplier, child: order.distributor,
|
||||
permissions_list: [:add_to_order_cycle])
|
||||
}
|
||||
|
||||
it "renders results" do
|
||||
api_get :show, params
|
||||
|
||||
expect(response.status).to eq 200
|
||||
expect(json_response).to match_array report_output(order, "supplier")
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def report_output(order, user_type)
|
||||
results = []
|
||||
|
||||
order.line_items.each do |line_item|
|
||||
results << __send__("#{user_type}_report_row", line_item)
|
||||
end
|
||||
|
||||
results << summary_row(order)
|
||||
end
|
||||
|
||||
def distributor_report_row(line_item)
|
||||
{
|
||||
"hub" => line_item.order.distributor.name,
|
||||
"customer_code" => line_item.order.customer&.code,
|
||||
"first_name" => line_item.order.bill_address.firstname,
|
||||
"last_name" => line_item.order.bill_address.lastname,
|
||||
"supplier" => line_item.product.supplier.name,
|
||||
"product" => line_item.product.name,
|
||||
"variant" => line_item.full_name,
|
||||
"quantity" => line_item.quantity,
|
||||
"is_temperature_controlled" =>
|
||||
line_item.product.shipping_category&.temperature_controlled ? I18n.t(:yes) : I18n.t(:no)
|
||||
}
|
||||
end
|
||||
|
||||
def supplier_report_row(line_item)
|
||||
{
|
||||
"hub" => line_item.order.distributor.name,
|
||||
"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),
|
||||
"supplier" => line_item.product.supplier.name,
|
||||
"product" => line_item.product.name,
|
||||
"variant" => line_item.full_name,
|
||||
"quantity" => line_item.quantity,
|
||||
"is_temperature_controlled" =>
|
||||
line_item.product.shipping_category&.temperature_controlled ? I18n.t(:yes) : I18n.t(:no)
|
||||
}
|
||||
end
|
||||
|
||||
def summary_row(order)
|
||||
{
|
||||
"summary_row_title" => I18n.t("summary_row.total", scope: i18n_scope),
|
||||
"hub" => "",
|
||||
"customer_code" => "",
|
||||
"first_name" => "",
|
||||
"last_name" => "",
|
||||
"supplier" => "",
|
||||
"product" => "",
|
||||
"variant" => "",
|
||||
"quantity" => order.line_items.sum(&:quantity),
|
||||
"is_temperature_controlled" => "",
|
||||
}
|
||||
end
|
||||
|
||||
def i18n_scope
|
||||
"admin.reports"
|
||||
end
|
||||
end
|
||||
80
spec/controllers/api/v0/reports_controller_spec.rb
Normal file
80
spec/controllers/api/v0/reports_controller_spec.rb
Normal file
@@ -0,0 +1,80 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
require "spec_helper"
|
||||
|
||||
describe Api::V0::ReportsController, type: :controller do
|
||||
let(:enterprise_user) { create(:user, enterprises: create(:enterprise)) }
|
||||
let(:params) {
|
||||
{
|
||||
report_type: 'packing',
|
||||
q: { created_at_lt: Time.zone.now }
|
||||
}
|
||||
}
|
||||
|
||||
before do
|
||||
allow(controller).to receive(:spree_current_user) { current_user }
|
||||
end
|
||||
|
||||
describe "fetching reports" do
|
||||
context "when the user is not authenticated" do
|
||||
let(:current_user) { nil }
|
||||
|
||||
it "returns unauthorised response" do
|
||||
api_get :show, params
|
||||
|
||||
assert_unauthorized!
|
||||
end
|
||||
end
|
||||
|
||||
context "when the user has no enterprises" do
|
||||
let(:current_user) { create(:user) }
|
||||
|
||||
it "returns unauthorised response" do
|
||||
api_get :show, params
|
||||
|
||||
assert_unauthorized!
|
||||
end
|
||||
end
|
||||
|
||||
context "when no report type is given" do
|
||||
let(:current_user) { enterprise_user }
|
||||
|
||||
it "returns an error" do
|
||||
api_get :show, q: { example: 'test' }
|
||||
|
||||
expect(response.status).to eq 422
|
||||
expect(json_response["error"]).to eq I18n.t('errors.no_report_type', scope: i18n_scope)
|
||||
end
|
||||
end
|
||||
|
||||
context "given a report type that doesn't exist" do
|
||||
let(:current_user) { enterprise_user }
|
||||
|
||||
it "returns an error" do
|
||||
api_get :show, report_type: "xxxxxx", q: { example: 'test' }
|
||||
|
||||
expect(response.status).to eq 422
|
||||
expect(json_response["error"]).to eq I18n.t('errors.report_not_found', scope: i18n_scope)
|
||||
end
|
||||
end
|
||||
|
||||
context "with no query params provided" do
|
||||
let(:current_user) { enterprise_user }
|
||||
|
||||
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)
|
||||
)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def i18n_scope
|
||||
"admin.reports"
|
||||
end
|
||||
end
|
||||
@@ -1,135 +0,0 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
require 'spec_helper'
|
||||
require 'open_food_network/packing_report'
|
||||
|
||||
include AuthenticationHelper
|
||||
|
||||
module OpenFoodNetwork
|
||||
describe PackingReport do
|
||||
describe "fetching orders" do
|
||||
let(:distributor) { create(:distributor_enterprise) }
|
||||
let(:order_cycle) { create(:simple_order_cycle) }
|
||||
let(:order) {
|
||||
create(:order, completed_at: 1.day.ago, order_cycle: order_cycle, distributor: distributor)
|
||||
}
|
||||
let(:line_item) { build(:line_item_with_shipment) }
|
||||
|
||||
before { order.line_items << line_item }
|
||||
|
||||
context "as a site admin" do
|
||||
let(:user) { create(:admin_user) }
|
||||
subject { PackingReport.new user, {}, true }
|
||||
|
||||
it "fetches completed orders" do
|
||||
order2 = create(:order)
|
||||
order2.line_items << build(:line_item)
|
||||
expect(subject.table_items).to eq([line_item])
|
||||
end
|
||||
|
||||
it "does not show cancelled orders" do
|
||||
order2 = create(:order, state: "canceled", completed_at: 1.day.ago)
|
||||
order2.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
|
||||
let!(:user) { create(:user) }
|
||||
subject { PackingReport.new user, {}, true }
|
||||
|
||||
let(:supplier) { create(:supplier_enterprise) }
|
||||
|
||||
before do
|
||||
supplier.enterprise_roles.create!(user: user)
|
||||
end
|
||||
|
||||
context "that has granted P-OC to the distributor" do
|
||||
let(:order2) {
|
||||
create(:order, distributor: distributor, completed_at: 1.day.ago,
|
||||
bill_address: create(:address), ship_address: create(:address))
|
||||
}
|
||||
let(:line_item2) {
|
||||
build(:line_item_with_shipment, product: create(:simple_product, supplier: supplier))
|
||||
}
|
||||
|
||||
before do
|
||||
order2.line_items << line_item2
|
||||
create(:enterprise_relationship, parent: supplier, 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([line_item2])
|
||||
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([line_item2])
|
||||
expect(subject.table_items.first.order.bill_address.firstname).
|
||||
to eq(order2.bill_address.firstname)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context "that has not granted P-OC to the distributor" do
|
||||
let(:order2) {
|
||||
create(:order, distributor: distributor, completed_at: 1.day.ago,
|
||||
bill_address: create(:address), ship_address: create(:address))
|
||||
}
|
||||
let(:line_item2) {
|
||||
build(:line_item_with_shipment, product: create(:simple_product, supplier: supplier))
|
||||
}
|
||||
|
||||
before do
|
||||
order2.line_items << line_item2
|
||||
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
|
||||
let!(:user) { create(:user) }
|
||||
subject { PackingReport.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
|
||||
distributor2 = create(:distributor_enterprise)
|
||||
distributor2.enterprise_roles.create!(user: create(:user))
|
||||
order2 = create(:order, distributor: distributor2, completed_at: 1.day.ago)
|
||||
order2.line_items << build(:line_item_with_shipment)
|
||||
expect(subject.table_items).to eq([line_item])
|
||||
end
|
||||
|
||||
it "only shows the selected order cycle" do
|
||||
order_cycle2 = create(:simple_order_cycle)
|
||||
order2 = create(:order, distributor: distributor, order_cycle: order_cycle2)
|
||||
order2.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
|
||||
end
|
||||
end
|
||||
112
spec/lib/reports/packing/packing_report_spec.rb
Normal file
112
spec/lib/reports/packing/packing_report_spec.rb
Normal file
@@ -0,0 +1,112 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
require 'spec_helper'
|
||||
|
||||
describe "Packing Reports" do
|
||||
include AuthenticationHelper
|
||||
|
||||
describe "fetching orders" do
|
||||
let(:distributor) { create(:distributor_enterprise) }
|
||||
let(:order_cycle) { create(:simple_order_cycle) }
|
||||
let(:order) {
|
||||
create(:order, completed_at: 1.day.ago, order_cycle: order_cycle, distributor: distributor)
|
||||
}
|
||||
let(:line_item) { build(:line_item_with_shipment) }
|
||||
|
||||
before { order.line_items << line_item }
|
||||
|
||||
context "as a site admin" do
|
||||
let(:user) { create(:admin_user) }
|
||||
subject { Reports::Packing::Customer.new user, {} }
|
||||
|
||||
it "fetches completed orders" do
|
||||
order2 = create(:order)
|
||||
order2.line_items << build(:line_item)
|
||||
expect(subject.collection).to eq([line_item])
|
||||
end
|
||||
|
||||
it "does not show cancelled orders" do
|
||||
order2 = create(:order, state: "canceled", completed_at: 1.day.ago)
|
||||
order2.line_items << build(:line_item_with_shipment)
|
||||
expect(subject.collection).to eq([line_item])
|
||||
end
|
||||
end
|
||||
|
||||
context "as a manager of a supplier" do
|
||||
let!(:user) { create(:user) }
|
||||
subject { Reports::Packing::Customer.new user, {} }
|
||||
|
||||
let(:supplier) { create(:supplier_enterprise) }
|
||||
|
||||
before do
|
||||
supplier.enterprise_roles.create!(user: user)
|
||||
end
|
||||
|
||||
context "that has granted P-OC to the distributor" do
|
||||
let(:order2) {
|
||||
create(:order, distributor: distributor, completed_at: 1.day.ago,
|
||||
bill_address: create(:address), ship_address: create(:address))
|
||||
}
|
||||
let(:line_item2) {
|
||||
build(:line_item_with_shipment, product: create(:simple_product, supplier: supplier))
|
||||
}
|
||||
|
||||
before do
|
||||
order2.line_items << line_item2
|
||||
create(:enterprise_relationship, parent: supplier, child: distributor,
|
||||
permissions_list: [:add_to_order_cycle])
|
||||
end
|
||||
|
||||
it "shows line items supplied by my producers, with names hidden" do
|
||||
expect(subject.collection).to eq([line_item2])
|
||||
expect(subject.as_hashes.first[:first_name]).to eq(
|
||||
I18n.t('admin.reports.hidden_field')
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
context "that has not granted P-OC to the distributor" do
|
||||
let(:order2) {
|
||||
create(:order, distributor: distributor, completed_at: 1.day.ago,
|
||||
bill_address: create(:address), ship_address: create(:address))
|
||||
}
|
||||
let(:line_item2) {
|
||||
build(:line_item_with_shipment, product: create(:simple_product, supplier: supplier))
|
||||
}
|
||||
|
||||
before do
|
||||
order2.line_items << line_item2
|
||||
end
|
||||
|
||||
it "does not show line items supplied by my producers" do
|
||||
expect(subject.collection).to eq([])
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context "as a manager of a distributor" do
|
||||
let!(:user) { create(:user) }
|
||||
subject { Reports::Packing::Customer.new user, {} }
|
||||
|
||||
before do
|
||||
distributor.enterprise_roles.create!(user: user)
|
||||
end
|
||||
|
||||
it "only shows line items distributed by enterprises managed by the current user" do
|
||||
distributor2 = create(:distributor_enterprise)
|
||||
distributor2.enterprise_roles.create!(user: create(:user))
|
||||
order2 = create(:order, distributor: distributor2, completed_at: 1.day.ago)
|
||||
order2.line_items << build(:line_item_with_shipment)
|
||||
expect(subject.collection).to eq([line_item])
|
||||
end
|
||||
|
||||
it "only shows the selected order cycle" do
|
||||
order_cycle2 = create(:simple_order_cycle)
|
||||
order2 = create(:order, distributor: distributor, order_cycle: order_cycle2)
|
||||
order2.line_items << build(:line_item)
|
||||
allow(subject).to receive(:params).and_return(order_cycle_id_in: order_cycle.id)
|
||||
expect(subject.collection).to eq([line_item])
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
106
spec/lib/reports/report_loader_spec.rb
Normal file
106
spec/lib/reports/report_loader_spec.rb
Normal file
@@ -0,0 +1,106 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
require 'spec_helper'
|
||||
|
||||
module Reports
|
||||
module Bananas
|
||||
class Base; end
|
||||
class Green; end
|
||||
class Yellow; end
|
||||
end
|
||||
end
|
||||
|
||||
describe Reports::ReportLoader do
|
||||
let(:service) { Reports::ReportLoader.new(*arguments) }
|
||||
let(:report_base_class) { 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"] }
|
||||
|
||||
it "returns a report class when given type and subtype" do
|
||||
expect(service.report_class).to eq Reports::Bananas::Yellow
|
||||
end
|
||||
end
|
||||
|
||||
describe "given report type only" do
|
||||
context "when the report has multiple subtypes" do
|
||||
let(:arguments) { ["bananas"] }
|
||||
|
||||
it "returns first listed report type" do
|
||||
expect(service.report_class).to eq Reports::Bananas::Green
|
||||
end
|
||||
end
|
||||
|
||||
context "when the report has no subtypes" do
|
||||
let(:arguments) { ["bananas"] }
|
||||
let(:report_subtypes) { [] }
|
||||
|
||||
it "returns base class" do
|
||||
expect(service.report_class).to eq Reports::Bananas::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(Reports::Errors::ReportNotFound)
|
||||
end
|
||||
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(Reports::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
|
||||
100
spec/lib/reports/report_renderer_spec.rb
Normal file
100
spec/lib/reports/report_renderer_spec.rb
Normal file
@@ -0,0 +1,100 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
require 'spec_helper'
|
||||
|
||||
describe Reports::ReportRenderer do
|
||||
let(:report_rows) {
|
||||
[
|
||||
{ id: 1, name: 'carrots', quantity: 3 },
|
||||
{ id: 2, name: 'onions', quantity: 6 }
|
||||
]
|
||||
}
|
||||
let(:report) { OpenStruct.new(report_rows: report_rows) }
|
||||
let(:service) { 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_hashes" do
|
||||
it "returns the report's data as hashes" do
|
||||
expect(service.as_hashes).to eq report_rows
|
||||
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
|
||||
|
||||
context "with summary rows" do
|
||||
let(:report_rows) {
|
||||
[
|
||||
{ id: 1, name: 'carrots', quantity: 3 },
|
||||
{ id: 2, name: 'onions', quantity: 6 },
|
||||
{ id: nil, name: nil, quantity: 9, summary_row_title: "TOTAL" }
|
||||
]
|
||||
}
|
||||
|
||||
it "returns the report's data as arrays" do
|
||||
expect(service.as_arrays).to eq [
|
||||
[:id, :name, :quantity],
|
||||
[1, "carrots", 3],
|
||||
[2, "onions", 6],
|
||||
["TOTAL", nil, 9]
|
||||
]
|
||||
end
|
||||
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
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -6,38 +6,104 @@ describe "Packing Reports", js: true do
|
||||
include AuthenticationHelper
|
||||
include WebHelper
|
||||
|
||||
let(:distributor) { create(:distributor_enterprise) }
|
||||
let(:oc) { create(:simple_order_cycle) }
|
||||
let(:order) { create(:order, completed_at: 1.day.ago, order_cycle: oc, distributor: distributor) }
|
||||
let(:li1) { build(:line_item_with_shipment) }
|
||||
let(:li2) { build(:line_item_with_shipment) }
|
||||
describe "Packing reports" do
|
||||
before do
|
||||
login_as_admin
|
||||
visit spree.admin_reports_path
|
||||
end
|
||||
|
||||
before do
|
||||
order.line_items << li1
|
||||
order.line_items << li2
|
||||
login_as_admin
|
||||
let(:bill_address1) { create(:address, lastname: "Aman") }
|
||||
let(:bill_address2) { create(:address, lastname: "Bman") }
|
||||
let(:distributor_address) {
|
||||
create(:address, address1: "distributor address", city: 'The Shire', zipcode: "1234")
|
||||
}
|
||||
let(:distributor) { create(:distributor_enterprise, address: distributor_address) }
|
||||
let(:order1) { create(:order, distributor: distributor, bill_address: bill_address1) }
|
||||
let(:order2) { create(:order, distributor: distributor, bill_address: bill_address2) }
|
||||
let(:supplier) { create(:supplier_enterprise, name: "Supplier") }
|
||||
let(:product_1) { create(:simple_product, name: "Product 1", supplier: supplier ) }
|
||||
let(:variant_1) { create(:variant, product: product_1, unit_description: "Big") }
|
||||
let(:variant_2) { create(:variant, product: product_1, unit_description: "Small") }
|
||||
let(:product_2) { create(:simple_product, name: "Product 2", supplier: supplier) }
|
||||
|
||||
before do
|
||||
Timecop.travel(Time.zone.local(2013, 4, 25, 14, 0, 0)) { order1.finalize! }
|
||||
Timecop.travel(Time.zone.local(2013, 4, 25, 15, 0, 0)) { order2.finalize! }
|
||||
|
||||
create(:line_item_with_shipment, variant: variant_1, quantity: 1, order: order1)
|
||||
create(:line_item_with_shipment, variant: variant_2, quantity: 3, order: order1)
|
||||
create(:line_item_with_shipment, variant: product_2.master, quantity: 3, order: order2)
|
||||
end
|
||||
|
||||
scenario "Pack By Customer" 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'
|
||||
# select 'Pack By Customer', from: 'report_type'
|
||||
click_button 'Search'
|
||||
|
||||
rows = find("table#listing_orders").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
|
||||
end
|
||||
|
||||
scenario "Pack By Supplier" 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'
|
||||
# select 'Pack By Customer', from: 'report_type'
|
||||
click_button 'Search'
|
||||
|
||||
rows = find("table#listing_orders").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
|
||||
end
|
||||
end
|
||||
|
||||
describe "viewing a report" do
|
||||
context "when an associated variant has been soft-deleted" do
|
||||
it "shows line items" do
|
||||
li1.variant.delete
|
||||
describe "With soft-deleted variants" do
|
||||
let(:distributor) { create(:distributor_enterprise) }
|
||||
let(:oc) { create(:simple_order_cycle) }
|
||||
let(:order) {
|
||||
create(:order, completed_at: 1.day.ago, order_cycle: oc, distributor: distributor)
|
||||
}
|
||||
let(:li1) { build(:line_item_with_shipment) }
|
||||
let(:li2) { build(:line_item_with_shipment) }
|
||||
|
||||
visit spree.admin_reports_path
|
||||
before do
|
||||
order.line_items << li1
|
||||
order.line_items << li2
|
||||
login_as_admin
|
||||
end
|
||||
|
||||
click_on I18n.t("admin.reports.packing.name")
|
||||
select oc.name, from: "q_order_cycle_id_in"
|
||||
describe "viewing a report" do
|
||||
context "when an associated variant has been soft-deleted" do
|
||||
it "shows line items" do
|
||||
li1.variant.delete
|
||||
|
||||
find('#q_completed_at_gt').click
|
||||
select_date(Time.zone.today - 1.day)
|
||||
visit spree.admin_reports_path
|
||||
|
||||
find('#q_completed_at_lt').click
|
||||
select_date(Time.zone.today)
|
||||
click_on I18n.t("admin.reports.packing.name")
|
||||
select oc.name, from: "q_order_cycle_id_in"
|
||||
|
||||
find("button[type='submit']").click
|
||||
find('#q_completed_at_gt').click
|
||||
select_date(Time.zone.today - 1.day)
|
||||
|
||||
expect(page).to have_content li1.product.name
|
||||
expect(page).to have_content li2.product.name
|
||||
find('#q_completed_at_lt').click
|
||||
select_date(Time.zone.today)
|
||||
|
||||
find("button[type='submit']").click
|
||||
|
||||
expect(page).to have_content li1.product.name
|
||||
expect(page).to have_content li2.product.name
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
Reference in New Issue
Block a user