mirror of
https://github.com/openfoodfoundation/openfoodnetwork
synced 2026-02-27 01:43:22 +00:00
Merge pull request #8145 from Matt-Yorkley/reports-query-interface
Reports Query Interface
This commit is contained in:
7
Gemfile
7
Gemfile
@@ -32,8 +32,9 @@ gem 'dfc_provider', path: './engines/dfc_provider'
|
||||
gem "order_management", path: "./engines/order_management"
|
||||
gem 'web', path: './engines/web'
|
||||
|
||||
gem 'activerecord-postgresql-adapter'
|
||||
gem 'pg', '~> 1.2.3'
|
||||
gem "activerecord-postgresql-adapter"
|
||||
gem "arel-helpers", "~> 2.12"
|
||||
gem "pg", "~> 1.2.3"
|
||||
|
||||
gem 'acts_as_list', '1.0.4'
|
||||
gem 'cancancan', '~> 1.15.0'
|
||||
@@ -96,6 +97,7 @@ gem 'wkhtmltopdf-binary'
|
||||
|
||||
gem 'immigrant'
|
||||
gem 'roo', '~> 2.8.3'
|
||||
gem 'spreadsheet_architect'
|
||||
|
||||
gem 'whenever', require: false
|
||||
|
||||
@@ -168,6 +170,7 @@ end
|
||||
group :development do
|
||||
gem 'debugger-linecache'
|
||||
gem 'foreman'
|
||||
gem 'listen'
|
||||
gem 'pry', '~> 0.13.0'
|
||||
gem 'pry-byebug', '~> 3.9.0'
|
||||
gem 'rubocop'
|
||||
|
||||
26
Gemfile.lock
26
Gemfile.lock
@@ -150,6 +150,8 @@ GEM
|
||||
railties (>= 3, < 7)
|
||||
angularjs-file-upload-rails (2.4.1)
|
||||
angularjs-rails (1.8.0)
|
||||
arel-helpers (2.12.1)
|
||||
activerecord (>= 3.1.0, < 7)
|
||||
ast (2.4.2)
|
||||
awesome_nested_set (3.4.0)
|
||||
activerecord (>= 4.0.0, < 7.0)
|
||||
@@ -159,6 +161,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 +189,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 +265,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 +345,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)
|
||||
@@ -361,6 +373,9 @@ GEM
|
||||
letter_opener (1.7.0)
|
||||
launchy (~> 2.2)
|
||||
libv8-node (15.14.0.1)
|
||||
listen (3.7.0)
|
||||
rb-fsevent (~> 0.10, >= 0.10.3)
|
||||
rb-inotify (~> 0.9, >= 0.9.10)
|
||||
loofah (2.12.0)
|
||||
crass (~> 1.0.2)
|
||||
nokogiri (>= 1.5.9)
|
||||
@@ -509,6 +524,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 +620,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)
|
||||
@@ -694,6 +717,7 @@ DEPENDENCIES
|
||||
angular_rails_csrf
|
||||
angularjs-file-upload-rails (~> 2.4.1)
|
||||
angularjs-rails (= 1.8.0)
|
||||
arel-helpers (~> 2.12)
|
||||
awesome_nested_set
|
||||
awesome_print
|
||||
aws-sdk (= 1.67.0)
|
||||
@@ -747,6 +771,7 @@ DEPENDENCIES
|
||||
jwt (~> 2.3)
|
||||
knapsack
|
||||
letter_opener (>= 1.4.1)
|
||||
listen
|
||||
mimemagic (> 0.3.5)
|
||||
mini_racer (= 0.4.0)
|
||||
monetize (~> 1.11)
|
||||
@@ -790,6 +815,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 = 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
|
||||
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 ::Reporting::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: { data: @report.as_json }
|
||||
end
|
||||
|
||||
def render_error(error)
|
||||
render json: { error: error.message }, status: :unprocessable_entity
|
||||
end
|
||||
|
||||
def validate_report
|
||||
raise ::Reporting::Errors::NoReportType if report_type.blank?
|
||||
raise ::Reporting::Errors::ReportNotFound if report_class.blank?
|
||||
end
|
||||
|
||||
def validate_query
|
||||
raise ::Reporting::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 ||= ::Reporting::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 = 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)
|
||||
@@ -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)
|
||||
Reporting::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
|
||||
|
||||
@@ -5,6 +5,9 @@ class ApplicationRecord < ActiveRecord::Base
|
||||
include Spree::Core::Permalinks
|
||||
include Spree::Preferences::Preferable
|
||||
include Searchable
|
||||
include ArelHelpers::ArelTable
|
||||
include ArelHelpers::Aliases
|
||||
include ArelHelpers::JoinAssociation
|
||||
|
||||
self.abstract_class = true
|
||||
end
|
||||
|
||||
@@ -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.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)
|
||||
@@ -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,20 @@ module Openfoodnetwork
|
||||
#{config.root}/app/jobs
|
||||
)
|
||||
|
||||
initializer "ofn.reports" do |app|
|
||||
module ::Reporting; end
|
||||
loader = Zeitwerk::Loader.new
|
||||
loader.push_dir("#{Rails.root}/lib/reporting", namespace: ::Reporting)
|
||||
loader.enable_reloading
|
||||
loader.setup
|
||||
loader.eager_load
|
||||
|
||||
if Rails.env.development?
|
||||
require 'listen'
|
||||
Listen.to("lib/reporting") { loader.reload }.start
|
||||
end
|
||||
end
|
||||
|
||||
config.paths["config/routes.rb"] = %w(
|
||||
config/routes/api.rb
|
||||
config/routes.rb
|
||||
|
||||
@@ -1191,11 +1191,40 @@ 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?"
|
||||
temp_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,9 @@ Openfoodnetwork::Application.routes.draw do
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
get '/reports/:report_type(/:report_subtype)', to: 'reports#show',
|
||||
constraints: lambda { |_| Flipper.enabled?(:api_reports) }
|
||||
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/reporting/errors.rb
Normal file
29
lib/reporting/errors.rb
Normal file
@@ -0,0 +1,29 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module Reporting
|
||||
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/reporting/frontend_data.rb
Normal file
34
lib/reporting/frontend_data.rb
Normal file
@@ -0,0 +1,34 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module Reporting
|
||||
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
|
||||
52
lib/reporting/queries/joins.rb
Normal file
52
lib/reporting/queries/joins.rb
Normal file
@@ -0,0 +1,52 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module Reporting
|
||||
module Queries
|
||||
module Joins
|
||||
def joins_order
|
||||
reflect query.join(association(Spree::LineItem, :order))
|
||||
end
|
||||
|
||||
def joins_order_distributor
|
||||
reflect query.join(association(Spree::Order, :distributor, distributor_alias))
|
||||
end
|
||||
|
||||
def joins_variant
|
||||
reflect query.join(association(Spree::LineItem, :variant))
|
||||
end
|
||||
|
||||
def joins_variant_product
|
||||
reflect query.join(association(Spree::Variant, :product))
|
||||
end
|
||||
|
||||
def joins_product_supplier
|
||||
reflect query.join(association(Spree::Product, :supplier, supplier_alias))
|
||||
end
|
||||
|
||||
def joins_product_shipping_category
|
||||
reflect query.join(association(Spree::Product, :shipping_category))
|
||||
end
|
||||
|
||||
def joins_order_and_distributor
|
||||
reflect query.
|
||||
join(association(Spree::LineItem, :order)).
|
||||
join(association(Spree::Order, :distributor, distributor_alias))
|
||||
end
|
||||
|
||||
def joins_order_customer
|
||||
reflect query.join(association(Spree::Order, :customer))
|
||||
end
|
||||
|
||||
def joins_order_bill_address
|
||||
reflect query.join(association(Spree::Order, :bill_address, bill_address_alias))
|
||||
end
|
||||
|
||||
def join_line_item_option_values
|
||||
reflect query.
|
||||
join(association(Spree::LineItem, :option_values)).
|
||||
join(association(Spree::OptionValuesLineItem, :option_value)
|
||||
)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
89
lib/reporting/queries/query_builder.rb
Normal file
89
lib/reporting/queries/query_builder.rb
Normal file
@@ -0,0 +1,89 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module Reporting
|
||||
module Queries
|
||||
class QueryBuilder < QueryInterface
|
||||
include Joins
|
||||
include Tables
|
||||
|
||||
attr_reader :grouping_fields
|
||||
|
||||
def initialize(model, grouping_fields = [])
|
||||
@grouping_fields = instance_exec(&grouping_fields)
|
||||
|
||||
super model.arel_table
|
||||
end
|
||||
|
||||
def selecting(lambda)
|
||||
fields = instance_exec(&lambda).map{ |key, value| value.public_send(:as, key.to_s) }
|
||||
|
||||
reflect query.project(*fields)
|
||||
end
|
||||
|
||||
def scoped_to_orders(orders_relation)
|
||||
reflect query.where(
|
||||
line_item_table[:order_id].in(Arel.sql(orders_relation.to_sql))
|
||||
)
|
||||
end
|
||||
|
||||
def with_managed_orders(orders_relation)
|
||||
reflect query.
|
||||
outer_join(managed_orders_alias).
|
||||
on(
|
||||
managed_orders_alias[:id].eq(line_item_table[:order_id]).
|
||||
and(managed_orders_alias[:distributor_id].in(Arel.sql(orders_relation.to_sql)))
|
||||
)
|
||||
end
|
||||
|
||||
def grouped_in_sets(group_sets)
|
||||
reflect query.group(*instance_exec(&group_sets))
|
||||
end
|
||||
|
||||
def ordered_by(ordering_fields)
|
||||
reflect query.order(*instance_exec(&ordering_fields))
|
||||
end
|
||||
|
||||
def masked(field, message = nil, mask_rule = nil)
|
||||
Case.new.
|
||||
when(mask_rule || default_mask_rule).
|
||||
then(field).
|
||||
else(quoted(message || I18n.t("hidden_field", scope: i18n_scope)))
|
||||
end
|
||||
|
||||
def distinct_results(fields = nil)
|
||||
return reflect query.distinct if fields.blank?
|
||||
|
||||
reflect query.distinct_on(fields)
|
||||
end
|
||||
|
||||
def variant_full_name
|
||||
display_name = variant_table[:display_name]
|
||||
display_as = variant_table[:display_as]
|
||||
options_text = option_value_table[:presentation]
|
||||
|
||||
unit_to_display = coalesce(nullify_empty_strings(display_as), options_text)
|
||||
combined_description = sql_concat(display_name, raw("' ('"), unit_to_display, raw("')'"))
|
||||
|
||||
Case.new.
|
||||
when(nullify_empty_strings(display_name).eq(nil)).then(unit_to_display).
|
||||
when(nullify_empty_strings(unit_to_display).not_eq(nil)).then(combined_description).
|
||||
else(display_name)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def default_mask_rule
|
||||
line_item_table[:order_id].in(raw("#{managed_orders_alias.name}.id")).
|
||||
or(distributor_alias[:show_customer_names_to_suppliers].eq(true))
|
||||
end
|
||||
|
||||
def summary_row_title
|
||||
I18n.t("total_items", scope: i18n_scope)
|
||||
end
|
||||
|
||||
def i18n_scope
|
||||
"admin.reports"
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
125
lib/reporting/queries/query_interface.rb
Normal file
125
lib/reporting/queries/query_interface.rb
Normal file
@@ -0,0 +1,125 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
require "arel-helpers"
|
||||
|
||||
module Reporting
|
||||
module Queries
|
||||
class QueryInterface < ::ArelHelpers::QueryBuilder
|
||||
include Arel::Nodes
|
||||
|
||||
def coalesce(field, default = 0)
|
||||
NamedFunction.new("COALESCE", [field, default])
|
||||
end
|
||||
|
||||
def sum_values(field, default = 0)
|
||||
NamedFunction.new("SUM", [coalesce(field, default)])
|
||||
end
|
||||
|
||||
def sum_grouped(field, default = 0)
|
||||
Case.new(sql_grouping(grouping_fields)).when(0).then(field.maximum).else(field.sum)
|
||||
end
|
||||
|
||||
def sum_new(field, default = 0)
|
||||
Case.new(sql_grouping(grouping_fields)).when(0).then(field.maximum).else(sum_values(field))
|
||||
end
|
||||
|
||||
def round(field, places: 2)
|
||||
NamedFunction.new("ROUND", [field, places])
|
||||
end
|
||||
|
||||
def association(base_class, association, alias_node = nil, join_type = InnerJoin)
|
||||
options = alias_node.present? ? { aliases: [alias_node] } : {}
|
||||
|
||||
Arel.sql(base_class.join_association(association, join_type, options).first.to_sql)
|
||||
end
|
||||
|
||||
def arel_join(join)
|
||||
Arel.sql(join.first.to_sql)
|
||||
end
|
||||
|
||||
def join_source(join_association)
|
||||
join_association[0].right
|
||||
end
|
||||
|
||||
def default_value(field)
|
||||
field.maximum
|
||||
end
|
||||
|
||||
def default_blank(field)
|
||||
Case.new(sql_grouping(grouping_fields)).when(0).then(field.maximum).else(empty_string)
|
||||
end
|
||||
|
||||
def default_string(field, string)
|
||||
Case.new(sql_grouping(grouping_fields)).when(0).then(field.maximum).else(quoted(string))
|
||||
end
|
||||
|
||||
def default_summary(field)
|
||||
Case.new(sql_grouping(grouping_fields)).when(0).then(field.maximum).else(empty_string)
|
||||
end
|
||||
|
||||
def boolean_blank(field, true_string = I18n.t(:yes), false_string = I18n.t(:no))
|
||||
Case.new(sql_grouping(grouping_fields)).when(0).
|
||||
then(pretty_boolean(field, true_string, false_string).maximum).
|
||||
else(empty_string)
|
||||
end
|
||||
|
||||
def pretty_boolean(field, true_string, false_string)
|
||||
Case.new(field).when(true).
|
||||
then(Arel.sql("'#{true_string}'")).
|
||||
else(Arel.sql("'#{false_string}'"))
|
||||
end
|
||||
|
||||
def cast(field, type)
|
||||
NamedFunction.new("CAST", [field.as(type)])
|
||||
end
|
||||
|
||||
def null_if(field, nullif)
|
||||
NamedFunction.new("NULLIF", [field, nullif])
|
||||
end
|
||||
|
||||
def parenthesise(args)
|
||||
Grouping.new(args)
|
||||
end
|
||||
|
||||
def nullify_empty_strings(field)
|
||||
null_if(field, empty_string)
|
||||
end
|
||||
|
||||
def empty_string
|
||||
raw("''")
|
||||
end
|
||||
|
||||
def sql_concat(*args)
|
||||
NamedFunction.new("CONCAT", args)
|
||||
end
|
||||
|
||||
def raw(string)
|
||||
SqlLiteral.new(string)
|
||||
end
|
||||
|
||||
def quoted(string)
|
||||
Quoted.new(string)
|
||||
end
|
||||
|
||||
def sql_grouping(groupings = grouping_fields)
|
||||
NamedFunction.new("GROUPING", [groupings])
|
||||
end
|
||||
|
||||
def grouping_sets(groupings)
|
||||
GroupingSet.new(groupings)
|
||||
end
|
||||
|
||||
def sql_case(expression)
|
||||
Case.new(expression)
|
||||
end
|
||||
|
||||
def rollup(groupings)
|
||||
RollUp.new(groupings)
|
||||
end
|
||||
|
||||
def raw_result
|
||||
ActiveRecord::Base.connection.exec_query(query.to_sql)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
51
lib/reporting/queries/tables.rb
Normal file
51
lib/reporting/queries/tables.rb
Normal file
@@ -0,0 +1,51 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module Reporting
|
||||
module Queries
|
||||
module Tables
|
||||
def order_table
|
||||
Spree::Order.arel_table
|
||||
end
|
||||
|
||||
def line_item_table
|
||||
Spree::LineItem.arel_table
|
||||
end
|
||||
|
||||
def product_table
|
||||
Spree::Product.arel_table
|
||||
end
|
||||
|
||||
def variant_table
|
||||
Spree::Variant.arel_table
|
||||
end
|
||||
|
||||
def customer_table
|
||||
::Customer.arel_table
|
||||
end
|
||||
|
||||
def distributor_alias
|
||||
Enterprise.arel_table.alias(:order_distributor)
|
||||
end
|
||||
|
||||
def supplier_alias
|
||||
Enterprise.arel_table.alias(:product_supplier)
|
||||
end
|
||||
|
||||
def bill_address_alias
|
||||
Spree::Address.arel_table.alias(:bill_address)
|
||||
end
|
||||
|
||||
def managed_orders_alias
|
||||
Spree::Order.arel_table.alias(:managed_orders)
|
||||
end
|
||||
|
||||
def option_value_table
|
||||
Spree::OptionValue.arel_table
|
||||
end
|
||||
|
||||
def shipping_category_table
|
||||
Spree::ShippingCategory.arel_table
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
40
lib/reporting/report_loader.rb
Normal file
40
lib/reporting/report_loader.rb
Normal file
@@ -0,0 +1,40 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
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
|
||||
rescue NameError
|
||||
raise Reporting::Errors::ReportNotFound
|
||||
end
|
||||
|
||||
def default_report_subtype
|
||||
report_subtypes.first || "base"
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
attr_reader :report_type, :report_subtype
|
||||
|
||||
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
|
||||
39
lib/reporting/report_renderer.rb
Normal file
39
lib/reporting/report_renderer.rb
Normal file
@@ -0,0 +1,39 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
require 'spreadsheet_architect'
|
||||
|
||||
module Reporting
|
||||
class ReportRenderer
|
||||
def initialize(report)
|
||||
@report = report
|
||||
end
|
||||
|
||||
def table_headers
|
||||
@report.report_data.columns
|
||||
end
|
||||
|
||||
def table_rows
|
||||
@report.report_data.rows
|
||||
end
|
||||
|
||||
def as_json
|
||||
@report.report_data.as_json
|
||||
end
|
||||
|
||||
def as_arrays
|
||||
@as_arrays ||= [table_headers] + table_rows
|
||||
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
|
||||
end
|
||||
end
|
||||
52
lib/reporting/report_template.rb
Normal file
52
lib/reporting/report_template.rb
Normal file
@@ -0,0 +1,52 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module Reporting
|
||||
class ReportTemplate
|
||||
delegate :as_json, :as_arrays, :table_headers, :table_rows,
|
||||
:to_csv, :to_xlsx, :to_ods, :to_json, to: :renderer
|
||||
|
||||
attr_reader :options
|
||||
|
||||
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
|
||||
end
|
||||
|
||||
def report_data
|
||||
@report_data ||= report_query.raw_result
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
attr_reader :current_user, :ransack_params
|
||||
|
||||
def renderer
|
||||
@renderer ||= ReportRenderer.new(self)
|
||||
end
|
||||
|
||||
def scoped_orders_relation
|
||||
visible_orders_relation.ransack(ransack_params).result
|
||||
end
|
||||
|
||||
def visible_orders_relation
|
||||
::Permissions::Order.new(current_user).
|
||||
visible_orders.complete.not_state(:canceled).
|
||||
select(:id).distinct
|
||||
end
|
||||
|
||||
def managed_orders_relation
|
||||
::Enterprise.managed_by(current_user).select(:id).distinct
|
||||
end
|
||||
|
||||
def i18n_scope
|
||||
"admin.reports"
|
||||
end
|
||||
end
|
||||
end
|
||||
41
lib/reporting/reports/packing/base.rb
Normal file
41
lib/reporting/reports/packing/base.rb
Normal file
@@ -0,0 +1,41 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module Reporting
|
||||
module Reports
|
||||
module Packing
|
||||
class Base < ReportTemplate
|
||||
SUBTYPES = ["customer", "supplier"]
|
||||
|
||||
def primary_model
|
||||
Spree::LineItem
|
||||
end
|
||||
|
||||
def report_query
|
||||
Queries::QueryBuilder.new(primary_model, grouping_fields).
|
||||
scoped_to_orders(scoped_orders_relation).
|
||||
with_managed_orders(managed_orders_relation).
|
||||
joins_order_and_distributor.
|
||||
joins_order_customer.
|
||||
joins_order_bill_address.
|
||||
joins_variant.
|
||||
joins_variant_product.
|
||||
joins_product_supplier.
|
||||
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
|
||||
lambda do
|
||||
[
|
||||
order_table[:id],
|
||||
line_item_table[:id]
|
||||
]
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
49
lib/reporting/reports/packing/customer.rb
Normal file
49
lib/reporting/reports/packing/customer.rb
Normal file
@@ -0,0 +1,49 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module Reporting
|
||||
module Reports
|
||||
module Packing
|
||||
class Customer < Base
|
||||
def select_fields
|
||||
lambda do
|
||||
{
|
||||
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]),
|
||||
}
|
||||
end
|
||||
end
|
||||
|
||||
def ordering_fields
|
||||
lambda do
|
||||
[
|
||||
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
|
||||
end
|
||||
48
lib/reporting/reports/packing/supplier.rb
Normal file
48
lib/reporting/reports/packing/supplier.rb
Normal file
@@ -0,0 +1,48 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
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
|
||||
end
|
||||
|
||||
def group_sets
|
||||
lambda do
|
||||
[
|
||||
distributor_alias[:name],
|
||||
supplier_alias[:name],
|
||||
grouping_sets([parenthesise(supplier_alias[:name]), parenthesise(grouping_fields)])
|
||||
]
|
||||
end
|
||||
end
|
||||
|
||||
def ordering_fields
|
||||
lambda do
|
||||
[
|
||||
distributor_alias[:name],
|
||||
supplier_alias[:name],
|
||||
sql_grouping(grouping_fields),
|
||||
Arel.sql("product"),
|
||||
Arel.sql("variant"),
|
||||
Arel.sql("last_name")
|
||||
]
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
109
spec/controllers/api/v0/reports/packing_report_spec.rb
Normal file
109
spec/controllers/api/v0/reports/packing_report_spec.rb
Normal file
@@ -0,0 +1,109 @@
|
||||
# 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 }
|
||||
order.finalize!
|
||||
end
|
||||
|
||||
describe "packing report" do
|
||||
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[:data]).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[:data]).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,
|
||||
"temp_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,
|
||||
"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" => "",
|
||||
}
|
||||
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
|
||||
@@ -91,83 +91,6 @@ describe '
|
||||
end
|
||||
end
|
||||
|
||||
describe "Packing reports" do
|
||||
before do
|
||||
login_as_admin_and_visit spree.admin_reports_path
|
||||
end
|
||||
|
||||
let(:bill_address1) { create(:address, lastname: "MULLER") }
|
||||
let(:bill_address2) { create(:address, lastname: "Mistery") }
|
||||
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
|
||||
|
||||
it "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.sort).to eq([
|
||||
["Hub", "Code", "First Name", "Last Name", "Supplier", "Product", "Variant", "Quantity",
|
||||
"TempControlled?"]
|
||||
].sort)
|
||||
expect(page).to have_selector 'table#listing_orders tbody tr', count: 5 # Totals row per order
|
||||
end
|
||||
|
||||
it "Alphabetically Sorted Pack by Customer" do
|
||||
click_link "Pack By Customer"
|
||||
click_button 'Search'
|
||||
|
||||
rows = find("table#listing_orders").all("tr")
|
||||
table = rows.map { |r| r.all("th,td").map { |c| c.text.strip }[3] }
|
||||
expect(table).to eq([
|
||||
"Last Name",
|
||||
order2.bill_address.lastname,
|
||||
"",
|
||||
order1.bill_address.lastname,
|
||||
order1.bill_address.lastname,
|
||||
""
|
||||
])
|
||||
end
|
||||
|
||||
it "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.sort).to eq([
|
||||
["Hub", "Supplier", "Code", "First Name", "Last Name", "Product", "Variant", "Quantity",
|
||||
"TempControlled?"]
|
||||
].sort)
|
||||
expect(all('table#listing_orders tbody tr').count).to eq(4) # Totals row per supplier
|
||||
end
|
||||
end
|
||||
|
||||
it "orders and distributors report" do
|
||||
login_as_admin_and_visit spree.admin_reports_path
|
||||
click_link 'Orders And Distributors'
|
||||
|
||||
@@ -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
|
||||
157
spec/lib/reports/packing/packing_report_spec.rb
Normal file
157
spec/lib/reports/packing/packing_report_spec.rb
Normal file
@@ -0,0 +1,157 @@
|
||||
# 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(:completed_order_with_totals, order_cycle: order_cycle, distributor: distributor,
|
||||
line_items_count: 0)
|
||||
}
|
||||
let(:line_item) { build(:line_item_with_shipment) }
|
||||
let(:user) { create(:admin_user) }
|
||||
let(:params) { {} }
|
||||
|
||||
let(:report_data) { subject.report_data.as_json }
|
||||
let(:report_contents) { subject.report_data.rows.flatten }
|
||||
let(:row_count) { subject.report_data.rows.count }
|
||||
|
||||
subject { Reporting::Reports::Packing::Customer.new user, params }
|
||||
|
||||
before do
|
||||
order.line_items << line_item
|
||||
order.finalize!
|
||||
end
|
||||
|
||||
context "as a site admin" do
|
||||
let(:cancelled_order) { create(:completed_order_with_totals, line_items_count: 0) }
|
||||
let(:line_item2) { build(:line_item_with_shipment) }
|
||||
|
||||
before do
|
||||
cancelled_order.line_items << line_item2
|
||||
cancelled_order.finalize!
|
||||
cancelled_order.cancel!
|
||||
end
|
||||
|
||||
it "fetches line items for completed orders" do
|
||||
expect(report_contents).to include line_item.product.name
|
||||
end
|
||||
|
||||
it "does not fetch line items for cancelled orders" do
|
||||
expect(report_contents).to_not include line_item2.product.name
|
||||
end
|
||||
end
|
||||
|
||||
context "as a manager of a supplier" do
|
||||
let!(:user) { create(:user) }
|
||||
let(:supplier) { create(:supplier_enterprise) }
|
||||
let(:order2) {
|
||||
create(:completed_order_with_totals, distributor: distributor,
|
||||
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
|
||||
order2.finalize!
|
||||
supplier.enterprise_roles.create!(user: user)
|
||||
end
|
||||
|
||||
context "which has not granted P-OC to the distributor" do
|
||||
it "does not show line items supplied by my producers" do
|
||||
expect(row_count).to eq 0
|
||||
end
|
||||
end
|
||||
|
||||
context "which has granted P-OC to the distributor" do
|
||||
before do
|
||||
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(report_contents).to include line_item2.product.name
|
||||
expect(report_data.first["first_name"]).to eq(
|
||||
I18n.t('admin.reports.hidden_field')
|
||||
)
|
||||
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(report_data.first["first_name"]).to eq(order2.bill_address.firstname)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context "as a manager of a distributor" do
|
||||
let!(:user) { create(:user) }
|
||||
let(:distributor2) { create(:distributor_enterprise) }
|
||||
let(:order3) {
|
||||
create(:completed_order_with_totals, distributor: distributor2,
|
||||
line_items_count: 0)
|
||||
}
|
||||
let(:line_item3) { build(:line_item_with_shipment) }
|
||||
|
||||
before do
|
||||
order3.line_items << line_item3
|
||||
order3.finalize!
|
||||
distributor.enterprise_roles.create!(user: user)
|
||||
end
|
||||
|
||||
it "only shows line items distributed by enterprises managed by the current user" do
|
||||
expect(report_contents).to include line_item.product.name
|
||||
expect(report_contents).to_not include line_item3.product.name
|
||||
end
|
||||
|
||||
context "filtering by order cycle" do
|
||||
let(:order_cycle2) { create(:simple_order_cycle) }
|
||||
let(:order4) {
|
||||
create(:completed_order_with_totals, distributor: distributor, order_cycle: order_cycle2,
|
||||
line_items_count: 0)
|
||||
}
|
||||
let(:line_item4) { build(:line_item_with_shipment) }
|
||||
let(:params) { { order_cycle_id_in: order_cycle.id } }
|
||||
|
||||
before do
|
||||
order4.line_items << line_item4
|
||||
order4.finalize!
|
||||
end
|
||||
|
||||
it "only shows results from the selected order cycle" do
|
||||
expect(report_contents).to include line_item.product.name
|
||||
expect(report_contents).to_not include line_item4.product.name
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe "ordering and grouping" do
|
||||
let(:distributor2) { create(:distributor_enterprise) }
|
||||
let(:order2) {
|
||||
create(:completed_order_with_totals, order_cycle: order_cycle, distributor: distributor2,
|
||||
line_items_count: 2)
|
||||
}
|
||||
|
||||
before do
|
||||
order2.finalize!
|
||||
end
|
||||
|
||||
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, ""]
|
||||
)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
108
spec/lib/reports/report_loader_spec.rb
Normal file
108
spec/lib/reports/report_loader_spec.rb
Normal file
@@ -0,0 +1,108 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
require 'spec_helper'
|
||||
|
||||
module Reporting
|
||||
module Reports
|
||||
module Bananas
|
||||
class Base; end
|
||||
class Green; end
|
||||
class Yellow; end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe Reporting::ReportLoader do
|
||||
let(:service) { Reporting::ReportLoader.new(*arguments) }
|
||||
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"] }
|
||||
|
||||
it "returns a report class when given type and subtype" do
|
||||
expect(service.report_class).to eq Reporting::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 Reporting::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 Reporting::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(Reporting::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(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
|
||||
83
spec/lib/reports/report_renderer_spec.rb
Normal file
83
spec/lib/reports/report_renderer_spec.rb
Normal file
@@ -0,0 +1,83 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
require 'spec_helper'
|
||||
|
||||
describe Reporting::ReportRenderer do
|
||||
let(:data) {
|
||||
[
|
||||
{ "id" => 1, "name" => "carrots", "quantity" => 3 },
|
||||
{ "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(: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_json" do
|
||||
it "returns the report's data as hashes" do
|
||||
expect(service.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
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -6,38 +6,130 @@ 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: "MULLER") }
|
||||
let(:bill_address2) { create(:address, lastname: "Mistery") }
|
||||
let(:distributor_address) {
|
||||
create(:address, address1: "distributor address", city: 'The Shire', zipcode: "1234")
|
||||
}
|
||||
let(:distributor) { create(:distributor_enterprise, address: distributor_address) }
|
||||
let(:order1) {
|
||||
create(:completed_order_with_totals, line_items_count: 0, distributor: distributor,
|
||||
bill_address: bill_address1)
|
||||
}
|
||||
let(:order2) {
|
||||
create(:completed_order_with_totals, line_items_count: 0, distributor: distributor,
|
||||
bill_address: bill_address2)
|
||||
}
|
||||
let(:supplier) { create(:supplier_enterprise, name: "Supplier") }
|
||||
let(:product1) { create(:simple_product, name: "Product 1", supplier: supplier ) }
|
||||
let(:variant1) { create(:variant, product: product1, unit_description: "Big") }
|
||||
let(:variant2) { create(:variant, product: product1, unit_description: "Small") }
|
||||
let(:product2) { 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: variant1, quantity: 1, order: order1)
|
||||
create(:line_item_with_shipment, variant: variant2, quantity: 3, order: order1)
|
||||
create(:line_item_with_shipment, variant: product2.master, quantity: 3, order: order2)
|
||||
end
|
||||
|
||||
describe "Pack By Customer" do
|
||||
it "displays the report" 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'
|
||||
|
||||
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
|
||||
|
||||
it "sorts alphabetically" do
|
||||
click_link "Pack By Customer"
|
||||
click_button 'Search'
|
||||
|
||||
rows = find("table#listing_orders").all("tr")
|
||||
table = rows.map { |r| r.all("th,td").map { |c| c.text.strip }[3] }
|
||||
expect(table).to eq([
|
||||
"LAST NAME",
|
||||
order2.bill_address.lastname,
|
||||
"",
|
||||
order1.bill_address.lastname,
|
||||
order1.bill_address.lastname,
|
||||
""
|
||||
])
|
||||
end
|
||||
end
|
||||
|
||||
describe "Pack By Supplier" do
|
||||
it "displays the report" 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'
|
||||
|
||||
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
|
||||
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(:completed_order_with_totals, line_items_count: 0, 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
|
||||
order.finalize!
|
||||
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