mirror of
https://github.com/openfoodfoundation/openfoodnetwork
synced 2026-02-27 01:43:22 +00:00
Merge pull request #5526 from cillian/drop-blockenspiel
Drop blockenspiel
This commit is contained in:
1
Gemfile
1
Gemfile
@@ -81,7 +81,6 @@ gem "active_model_serializers", "0.8.4"
|
||||
gem 'activerecord-session_store'
|
||||
gem 'acts-as-taggable-on', '~> 4.0'
|
||||
gem 'angularjs-file-upload-rails', '~> 2.4.1'
|
||||
gem 'blockenspiel'
|
||||
gem 'custom_error_message', github: 'jeremydurham/custom-err-msg'
|
||||
gem 'dalli'
|
||||
gem 'diffy'
|
||||
|
||||
@@ -150,7 +150,6 @@ GEM
|
||||
bcrypt (3.1.13)
|
||||
bcrypt-ruby (3.1.5)
|
||||
bcrypt (>= 3.1.3)
|
||||
blockenspiel (0.5.0)
|
||||
bugsnag (6.13.1)
|
||||
concurrent-ruby (~> 1.0)
|
||||
builder (3.1.4)
|
||||
@@ -704,7 +703,6 @@ DEPENDENCIES
|
||||
awesome_nested_set (~> 3.0.0.rc.1)
|
||||
awesome_print
|
||||
aws-sdk (= 1.11.1)
|
||||
blockenspiel
|
||||
bugsnag
|
||||
byebug (~> 11.0.0)
|
||||
cancan (~> 1.6.10)
|
||||
|
||||
@@ -12,7 +12,6 @@ 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/bulk_coop_report'
|
||||
require 'open_food_network/payments_report'
|
||||
require 'open_food_network/orders_and_fulfillments_report'
|
||||
|
||||
@@ -21,6 +20,11 @@ module Spree
|
||||
class ReportsController < Spree::Admin::BaseController
|
||||
include Spree::ReportsHelper
|
||||
|
||||
ORDER_MANAGEMENT_ENGINE_REPORTS = [
|
||||
:bulk_coop,
|
||||
:enterprise_fee_summary
|
||||
].freeze
|
||||
|
||||
helper_method :render_content?
|
||||
|
||||
before_action :cache_search_state
|
||||
@@ -91,19 +95,6 @@ module Spree
|
||||
render_report(@report.header, @report.table, params[:csv], "sales_tax.csv")
|
||||
end
|
||||
|
||||
def bulk_coop
|
||||
# -- Prepare form options
|
||||
@distributors = my_distributors
|
||||
@report_type = params[:report_type]
|
||||
|
||||
# -- Build Report with Order Grouper
|
||||
@report = OpenFoodNetwork::BulkCoopReport.new spree_current_user, params, render_content?
|
||||
@table = order_grouper_table
|
||||
csv_file_name = "bulk_coop_#{params[:report_type]}_#{timestamp}.csv"
|
||||
|
||||
render_report(@report.header, @table, params[:csv], csv_file_name)
|
||||
end
|
||||
|
||||
def payments
|
||||
# -- Prepare Form Options
|
||||
@distributors = my_distributors
|
||||
@@ -262,7 +253,7 @@ module Spree
|
||||
end
|
||||
|
||||
def order_grouper_table
|
||||
order_grouper = OpenFoodNetwork::OrderGrouper.new @report.rules, @report.columns
|
||||
order_grouper = OpenFoodNetwork::OrderGrouper.new @report.rules, @report.columns, @report
|
||||
order_grouper.table(@report.table_items)
|
||||
end
|
||||
|
||||
@@ -311,7 +302,7 @@ module Spree
|
||||
|
||||
# List of reports that have been moved to the Order Management engine
|
||||
def report_in_order_management_engine?(report)
|
||||
report == :enterprise_fee_summary
|
||||
ORDER_MANAGEMENT_ENGINE_REPORTS.include?(report)
|
||||
end
|
||||
|
||||
def timestamp
|
||||
|
||||
@@ -185,9 +185,10 @@ class AbilityDecorator
|
||||
can [:admin, :index, :guide, :import, :save, :save_data, :validate_data, :reset_absent_products], ProductImport::ProductImporter
|
||||
|
||||
# Reports page
|
||||
can [:admin, :index, :customers, :orders_and_distributors, :group_buys, :bulk_coop, :payments,
|
||||
can [:admin, :index, :customers, :orders_and_distributors, :group_buys, :payments,
|
||||
:orders_and_fulfillment, :products_and_inventory, :order_cycle_management, :packing],
|
||||
Spree::Admin::ReportsController
|
||||
add_bulk_coop_abilities
|
||||
add_enterprise_fee_summary_abilities
|
||||
end
|
||||
|
||||
@@ -264,9 +265,10 @@ class AbilityDecorator
|
||||
end
|
||||
|
||||
# Reports page
|
||||
can [:admin, :index, :customers, :group_buys, :bulk_coop, :sales_tax, :payments,
|
||||
can [:admin, :index, :customers, :group_buys, :sales_tax, :payments,
|
||||
:orders_and_distributors, :orders_and_fulfillment, :products_and_inventory,
|
||||
:order_cycle_management, :xero_invoices], Spree::Admin::ReportsController
|
||||
add_bulk_coop_abilities
|
||||
add_enterprise_fee_summary_abilities
|
||||
|
||||
can [:create], Customer
|
||||
@@ -291,6 +293,13 @@ class AbilityDecorator
|
||||
end
|
||||
end
|
||||
|
||||
def add_bulk_coop_abilities
|
||||
# Reveal the report link in spree/admin/reports#index
|
||||
can [:bulk_coop], Spree::Admin::ReportsController
|
||||
# Allow direct access to the report resource
|
||||
can [:admin, :new, :create], :bulk_coop
|
||||
end
|
||||
|
||||
def add_enterprise_fee_summary_abilities
|
||||
# Reveal the report link in spree/admin/reports#index
|
||||
can [:enterprise_fee_summary], Spree::Admin::ReportsController
|
||||
|
||||
@@ -1,19 +0,0 @@
|
||||
= form_for @report.search, :url => spree.bulk_coop_admin_reports_path do |f|
|
||||
= render 'date_range_form', f: f
|
||||
|
||||
.row
|
||||
.four.columns.alpha
|
||||
= label_tag nil, t(:report_distributor)
|
||||
= f.collection_select(:distributor_id_eq, @distributors, :id, :name, {:include_blank => t(:all)}, {:class => "select2 fullwidth"})
|
||||
= label_tag nil, t(:report_type)
|
||||
%br
|
||||
= select_tag(:report_type, options_for_select([:bulk_coop_supplier_report, :bulk_coop_allocation, :bulk_coop_packing_sheets, :bulk_coop_customer_payments].map{ |e| [t(".#{e}"), e] }, @report_type))
|
||||
%br
|
||||
%br
|
||||
= check_box_tag :csv
|
||||
= label_tag :csv, t(:report_customers_csv)
|
||||
%br
|
||||
%br
|
||||
= button t(:search)
|
||||
|
||||
= render "table", id: "listing_orders", msg_option: t(:search)
|
||||
@@ -0,0 +1,66 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module OrderManagement
|
||||
module Reports
|
||||
class BulkCoopController < Spree::Admin::BaseController
|
||||
before_filter :load_report_parameters
|
||||
before_filter :load_permissions
|
||||
|
||||
def new; end
|
||||
|
||||
def create
|
||||
return respond_to_invalid_parameters unless @report_parameters.valid?
|
||||
|
||||
@report_parameters.authorize!(@permissions)
|
||||
|
||||
@report = report_klass::ReportService.new(@permissions, params[:report], spree_current_user)
|
||||
renderer.render(self)
|
||||
rescue ::Reports::Authorizer::ParameterNotAllowedError => e
|
||||
flash[:error] = e.message
|
||||
render_report_form
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def respond_to_invalid_parameters
|
||||
flash[:error] = I18n.t("invalid_filter_parameters", scope: i18n_scope)
|
||||
render_report_form
|
||||
end
|
||||
|
||||
def i18n_scope
|
||||
"order_management.reports.enterprise_fee_summary"
|
||||
end
|
||||
|
||||
def render_report_form
|
||||
render action: :new
|
||||
end
|
||||
|
||||
def report_klass
|
||||
OrderManagement::Reports::BulkCoop
|
||||
end
|
||||
|
||||
def load_report_parameters
|
||||
@report_parameters = report_klass::Parameters.new(params[:report] || {})
|
||||
end
|
||||
|
||||
def load_permissions
|
||||
@permissions = report_klass::Permissions.new(spree_current_user)
|
||||
end
|
||||
|
||||
def report_renderer_klass
|
||||
case params[:report_format]
|
||||
when "csv"
|
||||
report_klass::Renderers::CsvRenderer
|
||||
when nil, "", "html"
|
||||
report_klass::Renderers::HtmlRenderer
|
||||
else
|
||||
raise Reports::UnsupportedReportFormatException
|
||||
end
|
||||
end
|
||||
|
||||
def renderer
|
||||
@renderer ||= report_renderer_klass.new(@report)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,13 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module OrderManagement
|
||||
module Reports
|
||||
module BulkCoop
|
||||
class Authorizer < ::Reports::Authorizer
|
||||
def authorize!
|
||||
require_ids_allowed(parameters.distributor_ids, permissions.allowed_distributors)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,67 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module OrderManagement
|
||||
module Reports
|
||||
module BulkCoop
|
||||
class BulkCoopAllocationReport
|
||||
def header
|
||||
[
|
||||
I18n.t(:report_header_customer),
|
||||
I18n.t(:report_header_product),
|
||||
I18n.t(:report_header_bulk_unit_size),
|
||||
I18n.t(:report_header_variant),
|
||||
I18n.t(:report_header_variant_value),
|
||||
I18n.t(:report_header_variant_unit),
|
||||
I18n.t(:report_header_weight),
|
||||
I18n.t(:report_header_sum_total),
|
||||
I18n.t(:report_header_total_available),
|
||||
I18n.t(:report_header_unallocated),
|
||||
I18n.t(:report_header_max_quantity_excess),
|
||||
]
|
||||
end
|
||||
|
||||
def rules
|
||||
[
|
||||
{
|
||||
group_by: proc { |line_item| line_item.product },
|
||||
sort_by: proc { |product| product.name },
|
||||
summary_columns: [
|
||||
:total_label,
|
||||
:variant_product_name,
|
||||
:variant_product_group_buy_unit_size_f,
|
||||
:empty_cell,
|
||||
:empty_cell,
|
||||
:empty_cell,
|
||||
:empty_cell,
|
||||
:total_amount,
|
||||
:total_available,
|
||||
:remainder,
|
||||
:max_quantity_excess
|
||||
]
|
||||
},
|
||||
{
|
||||
group_by: proc { |line_item| line_item.order },
|
||||
sort_by: proc { |order| order.to_s }
|
||||
}
|
||||
]
|
||||
end
|
||||
|
||||
def columns
|
||||
[
|
||||
:order_billing_address_name,
|
||||
:product_name,
|
||||
:product_group_buy_unit_size,
|
||||
:full_name,
|
||||
:option_value_value,
|
||||
:option_value_unit,
|
||||
:weight_from_unit_value,
|
||||
:total_amount,
|
||||
:empty_cell,
|
||||
:empty_cell,
|
||||
:empty_cell
|
||||
]
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,279 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
require "open_food_network/reports/line_items"
|
||||
|
||||
module OrderManagement
|
||||
module Reports
|
||||
module BulkCoop
|
||||
class BulkCoopReport
|
||||
REPORT_TYPES = [
|
||||
:bulk_coop_supplier_report,
|
||||
:bulk_coop_allocation,
|
||||
:bulk_coop_packing_sheets,
|
||||
:bulk_coop_customer_payments
|
||||
].freeze
|
||||
|
||||
attr_reader :params
|
||||
def initialize(user, params = {}, render_table = false)
|
||||
@params = params
|
||||
@user = user
|
||||
@render_table = render_table
|
||||
|
||||
@supplier_report = BulkCoopSupplierReport.new
|
||||
@allocation_report = BulkCoopAllocationReport.new
|
||||
end
|
||||
|
||||
def header
|
||||
case params[:report_type]
|
||||
when "bulk_coop_supplier_report"
|
||||
@supplier_report.header
|
||||
when "bulk_coop_allocation"
|
||||
@allocation_report.header
|
||||
when "bulk_coop_packing_sheets"
|
||||
[I18n.t(:report_header_customer),
|
||||
I18n.t(:report_header_product),
|
||||
I18n.t(:report_header_variant),
|
||||
I18n.t(:report_header_sum_total)]
|
||||
when "bulk_coop_customer_payments"
|
||||
[I18n.t(:report_header_customer),
|
||||
I18n.t(:report_header_date_of_order),
|
||||
I18n.t(:report_header_total_cost),
|
||||
I18n.t(:report_header_amount_owing),
|
||||
I18n.t(:report_header_amount_paid)]
|
||||
else
|
||||
[I18n.t(:report_header_supplier),
|
||||
I18n.t(:report_header_product),
|
||||
I18n.t(:report_header_product),
|
||||
I18n.t(:report_header_bulk_unit_size),
|
||||
I18n.t(:report_header_variant),
|
||||
I18n.t(:report_header_weight),
|
||||
I18n.t(:report_header_sum_total),
|
||||
I18n.t(:report_header_sum_max_total),
|
||||
I18n.t(:report_header_units_required),
|
||||
I18n.t(:report_header_remainder)]
|
||||
end
|
||||
end
|
||||
|
||||
def search
|
||||
report_line_items.orders
|
||||
end
|
||||
|
||||
def table_items
|
||||
return [] unless @render_table
|
||||
|
||||
report_line_items.list(line_item_includes)
|
||||
end
|
||||
|
||||
def rules
|
||||
case params[:report_type]
|
||||
when "bulk_coop_supplier_report"
|
||||
@supplier_report.rules
|
||||
when "bulk_coop_allocation"
|
||||
@allocation_report.rules
|
||||
when "bulk_coop_packing_sheets"
|
||||
[{ group_by: proc { |li| li.product },
|
||||
sort_by: proc { |product| product.name } },
|
||||
{ group_by: proc { |li| li.full_name },
|
||||
sort_by: proc { |full_name| full_name } },
|
||||
{ group_by: proc { |li| li.order },
|
||||
sort_by: proc { |order| order.to_s } }]
|
||||
when "bulk_coop_customer_payments"
|
||||
[{ group_by: proc { |li| li.order },
|
||||
sort_by: proc { |order| order.completed_at } }]
|
||||
else
|
||||
[{ group_by: proc { |li| li.product.supplier },
|
||||
sort_by: proc { |supplier| supplier.name } },
|
||||
{ group_by: proc { |li| li.product },
|
||||
sort_by: proc { |product| product.name },
|
||||
summary_columns: [proc { |lis| lis.first.product.supplier.name },
|
||||
proc { |lis| lis.first.product.name },
|
||||
proc { |lis| lis.first.product.group_buy_unit_size || 0.0 },
|
||||
proc { |_lis| "" },
|
||||
proc { |_lis| "" },
|
||||
proc { |lis| lis.sum { |li| li.quantity * (li.weight_from_unit_value || 0) } },
|
||||
proc { |lis| lis.sum { |li| (li.max_quantity || 0) * (li.weight_from_unit_value || 0) } },
|
||||
proc { |lis| ( (lis.first.product.group_buy_unit_size || 0).zero? ? 0 : ( lis.sum { |li| [li.max_quantity || 0, li.quantity || 0].max * (li.weight_from_unit_value || 0) } / lis.first.product.group_buy_unit_size ) ).floor },
|
||||
proc { |lis| lis.sum { |li| [li.max_quantity || 0, li.quantity || 0].max * (li.weight_from_unit_value || 0) } - ( ( (lis.first.product.group_buy_unit_size || 0).zero? ? 0 : ( lis.sum { |li| [li.max_quantity || 0, li.quantity || 0].max * (li.weight_from_unit_value || 0) } / lis.first.product.group_buy_unit_size ) ).floor * (lis.first.product.group_buy_unit_size || 0) ) }] },
|
||||
{ group_by: proc { |li| li.full_name },
|
||||
sort_by: proc { |full_name| full_name } }]
|
||||
end
|
||||
end
|
||||
|
||||
def columns
|
||||
case params[:report_type]
|
||||
when "bulk_coop_supplier_report"
|
||||
@supplier_report.columns
|
||||
when "bulk_coop_allocation"
|
||||
@allocation_report.columns
|
||||
when "bulk_coop_packing_sheets"
|
||||
[
|
||||
:order_billing_address_name,
|
||||
:product_name,
|
||||
:full_name,
|
||||
:total_quantity
|
||||
]
|
||||
when "bulk_coop_customer_payments"
|
||||
[
|
||||
:order_billing_address_name,
|
||||
:order_completed_at,
|
||||
:customer_payments_total_cost,
|
||||
:customer_payments_amount_owed,
|
||||
:customer_payments_amount_paid
|
||||
]
|
||||
else
|
||||
[
|
||||
:product_supplier_name,
|
||||
:product_name,
|
||||
:product_group_buy_unit_size,
|
||||
:full_name,
|
||||
:weight_from_unit_value,
|
||||
:total_quantity,
|
||||
:total_max_quantity,
|
||||
:empty_cell,
|
||||
:empty_cell
|
||||
]
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def line_item_includes
|
||||
[{ order: [:bill_address],
|
||||
variant: [{ option_values: :option_type }, { product: :supplier }] }]
|
||||
end
|
||||
|
||||
def order_permissions
|
||||
return @order_permissions unless @order_permissions.nil?
|
||||
|
||||
@order_permissions = ::Permissions::Order.new(@user, @params[:q])
|
||||
end
|
||||
|
||||
def report_line_items
|
||||
@report_line_items ||= OpenFoodNetwork::Reports::LineItems.new(order_permissions, @params)
|
||||
end
|
||||
|
||||
def customer_payments_total_cost(line_items)
|
||||
line_items.map(&:order).uniq.sum(&:total)
|
||||
end
|
||||
|
||||
def customer_payments_amount_owed(line_items)
|
||||
line_items.map(&:order).uniq.sum(&:outstanding_balance)
|
||||
end
|
||||
|
||||
def customer_payments_amount_paid(line_items)
|
||||
line_items.map(&:order).uniq.sum(&:payment_total)
|
||||
end
|
||||
|
||||
def empty_cell(_line_items)
|
||||
""
|
||||
end
|
||||
|
||||
def full_name(line_items)
|
||||
line_items.first.full_name
|
||||
end
|
||||
|
||||
def group_buy_unit_size(line_items)
|
||||
(line_items.first.variant.product.group_buy_unit_size || 0.0) /
|
||||
(line_items.first.product.variant_unit_scale || 1)
|
||||
end
|
||||
|
||||
def max_quantity_excess(line_items)
|
||||
max_quantity_amount(line_items) - total_amount(line_items)
|
||||
end
|
||||
|
||||
def max_quantity_amount(line_items)
|
||||
line_items.sum do |line_item|
|
||||
max_quantity = [line_item.max_quantity || 0, line_item.quantity || 0].max
|
||||
max_quantity * scaled_unit_value(line_item.variant)
|
||||
end
|
||||
end
|
||||
|
||||
def option_value_value(line_items)
|
||||
OpenFoodNetwork::OptionValueNamer.new(line_items.first).value
|
||||
end
|
||||
|
||||
def option_value_unit(line_items)
|
||||
OpenFoodNetwork::OptionValueNamer.new(line_items.first).unit
|
||||
end
|
||||
|
||||
def order_billing_address_name(line_items)
|
||||
billing_address = line_items.first.order.bill_address
|
||||
billing_address.firstname + " " + billing_address.lastname
|
||||
end
|
||||
|
||||
def order_completed_at(line_items)
|
||||
line_items.first.order.completed_at.to_s
|
||||
end
|
||||
|
||||
def product_group_buy_unit_size(line_items)
|
||||
line_items.first.product.group_buy_unit_size || 0.0
|
||||
end
|
||||
|
||||
def product_name(line_items)
|
||||
line_items.first.product.name
|
||||
end
|
||||
|
||||
def product_supplier_name(line_items)
|
||||
line_items.first.product.supplier.name
|
||||
end
|
||||
|
||||
def remainder(line_items)
|
||||
remainder = total_available(line_items) - total_amount(line_items)
|
||||
remainder >= 0 ? remainder : ''
|
||||
end
|
||||
|
||||
def scaled_final_weight_volume(line_item)
|
||||
(line_item.final_weight_volume || 0) / (line_item.product.variant_unit_scale || 1)
|
||||
end
|
||||
|
||||
def scaled_unit_value(variant)
|
||||
(variant.unit_value || 0) / (variant.product.variant_unit_scale || 1)
|
||||
end
|
||||
|
||||
def total_amount(line_items)
|
||||
line_items.sum { |li| scaled_final_weight_volume(li) }
|
||||
end
|
||||
|
||||
def total_available(line_items)
|
||||
units_required(line_items) * group_buy_unit_size(line_items)
|
||||
end
|
||||
|
||||
def total_max_quantity(line_items)
|
||||
line_items.sum { |line_item| line_item.max_quantity || 0 }
|
||||
end
|
||||
|
||||
def total_quantity(line_items)
|
||||
line_items.sum(&:quantity)
|
||||
end
|
||||
|
||||
def total_label(_line_items)
|
||||
I18n.t('admin.reports.total')
|
||||
end
|
||||
|
||||
def units_required(line_items)
|
||||
if group_buy_unit_size(line_items).zero?
|
||||
0
|
||||
else
|
||||
( total_amount(line_items) / group_buy_unit_size(line_items) ).ceil
|
||||
end
|
||||
end
|
||||
|
||||
def variant_product_group_buy_unit_size_f(line_items)
|
||||
group_buy_unit_size(line_items)
|
||||
end
|
||||
|
||||
def variant_product_name(line_items)
|
||||
line_items.first.variant.product.name
|
||||
end
|
||||
|
||||
def variant_product_supplier_name(line_items)
|
||||
line_items.first.variant.product.supplier.name
|
||||
end
|
||||
|
||||
def weight_from_unit_value(line_items)
|
||||
line_items.first.weight_from_unit_value || 0
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,65 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module OrderManagement
|
||||
module Reports
|
||||
module BulkCoop
|
||||
class BulkCoopSupplierReport
|
||||
def header
|
||||
[
|
||||
I18n.t(:report_header_supplier),
|
||||
I18n.t(:report_header_product),
|
||||
I18n.t(:report_header_bulk_unit_size),
|
||||
I18n.t(:report_header_variant),
|
||||
I18n.t(:report_header_variant_value),
|
||||
I18n.t(:report_header_variant_unit),
|
||||
I18n.t(:report_header_weight),
|
||||
I18n.t(:report_header_sum_total),
|
||||
I18n.t(:report_header_units_required),
|
||||
I18n.t(:report_header_unallocated),
|
||||
I18n.t(:report_header_max_quantity_excess),
|
||||
]
|
||||
end
|
||||
|
||||
def rules
|
||||
[
|
||||
{ group_by: proc { |line_item| line_item.product.supplier },
|
||||
sort_by: proc { |supplier| supplier.name } },
|
||||
{ group_by: proc { |line_item| line_item.product },
|
||||
sort_by: proc { |product| product.name },
|
||||
summary_columns: [
|
||||
:variant_product_supplier_name,
|
||||
:variant_product_name,
|
||||
:variant_product_group_buy_unit_size_f,
|
||||
:empty_cell,
|
||||
:empty_cell,
|
||||
:empty_cell,
|
||||
:empty_cell,
|
||||
:total_amount,
|
||||
:units_required,
|
||||
:remainder,
|
||||
:max_quantity_excess
|
||||
] },
|
||||
{ group_by: proc { |line_item| line_item.full_name },
|
||||
sort_by: proc { |full_name| full_name } }
|
||||
]
|
||||
end
|
||||
|
||||
def columns
|
||||
[
|
||||
:variant_product_supplier_name,
|
||||
:variant_product_name,
|
||||
:variant_product_group_buy_unit_size_f,
|
||||
:full_name,
|
||||
:option_value_value,
|
||||
:option_value_unit,
|
||||
:weight_from_unit_value,
|
||||
:total_amount,
|
||||
:empty_cell,
|
||||
:empty_cell,
|
||||
:empty_cell
|
||||
]
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,45 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module OrderManagement
|
||||
module Reports
|
||||
module BulkCoop
|
||||
class Parameters < ::Reports::Parameters::Base
|
||||
extend ActiveModel::Naming
|
||||
extend ActiveModel::Translation
|
||||
include ActiveModel::Validations
|
||||
|
||||
attr_accessor :start_at, :end_at, :distributor_ids, :report_type
|
||||
|
||||
before_validation :cleanup_arrays
|
||||
|
||||
validates :start_at, :end_at, date_time_string: true
|
||||
validates :distributor_ids, integer_array: true
|
||||
validates_inclusion_of :report_type, in: BulkCoopReport::REPORT_TYPES.map(&:to_s)
|
||||
|
||||
validate :require_valid_datetime_range
|
||||
|
||||
def initialize(attributes = {})
|
||||
self.distributor_ids = []
|
||||
|
||||
super(attributes)
|
||||
end
|
||||
|
||||
def authorize!(permissions)
|
||||
authorizer = Authorizer.new(self, permissions)
|
||||
authorizer.authorize!
|
||||
end
|
||||
|
||||
protected
|
||||
|
||||
# Remove the blank strings that Rails multiple selects add by default to
|
||||
# make sure that blank lists are still submitted to the server as arrays
|
||||
# instead of nil.
|
||||
#
|
||||
# https://api.rubyonrails.org/classes/ActionView/Helpers/FormOptionsHelper.html#method-i-select
|
||||
def cleanup_arrays
|
||||
distributor_ids.reject!(&:blank?)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,13 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module OrderManagement
|
||||
module Reports
|
||||
module BulkCoop
|
||||
class Permissions < ::Reports::Permissions
|
||||
def allowed_distributors
|
||||
@allowed_distributors ||= Enterprise.is_distributor.managed_by(user)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,32 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module OrderManagement
|
||||
module Reports
|
||||
module BulkCoop
|
||||
module Renderers
|
||||
class CsvRenderer < ::Reports::Renderers::Base
|
||||
def render(context)
|
||||
context.send_data(generate, filename: filename)
|
||||
end
|
||||
|
||||
def generate
|
||||
CSV.generate do |csv|
|
||||
csv << report_data.header
|
||||
|
||||
report_data.list.each do |data|
|
||||
csv << data
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def filename
|
||||
timestamp = Time.zone.now.strftime("%Y%m%d")
|
||||
"#{report_data.parameters[:report_type]}_#{timestamp}.csv"
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,24 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module OrderManagement
|
||||
module Reports
|
||||
module BulkCoop
|
||||
module Renderers
|
||||
class HtmlRenderer < ::Reports::Renderers::Base
|
||||
def render(context)
|
||||
context.instance_variable_set :@renderer, self
|
||||
context.render(action: :create, renderer: self)
|
||||
end
|
||||
|
||||
def header
|
||||
report_data.header
|
||||
end
|
||||
|
||||
def data_rows
|
||||
report_data.list
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,29 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
require 'open_food_network/order_grouper'
|
||||
|
||||
module OrderManagement
|
||||
module Reports
|
||||
module BulkCoop
|
||||
class ReportService
|
||||
attr_accessor :permissions, :parameters, :user
|
||||
|
||||
def initialize(permissions, parameters, user)
|
||||
@permissions = permissions
|
||||
@parameters = parameters
|
||||
@user = user
|
||||
@report = BulkCoopReport.new(user, parameters, true)
|
||||
end
|
||||
|
||||
def header
|
||||
@report.header
|
||||
end
|
||||
|
||||
def list
|
||||
order_grouper = OpenFoodNetwork::OrderGrouper.new @report.rules, @report.columns, @report
|
||||
order_grouper.table(@report.table_items)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -2,11 +2,6 @@ module OrderManagement
|
||||
module Reports
|
||||
module EnterpriseFeeSummary
|
||||
class Authorizer < ::Reports::Authorizer
|
||||
def self.parameter_not_allowed_error_message
|
||||
i18n_scope = "order_management.reports.enterprise_fee_summary"
|
||||
I18n.t("parameter_not_allowed_error", scope: i18n_scope)
|
||||
end
|
||||
|
||||
def authorize!
|
||||
authorize_by_distribution!
|
||||
authorize_by_fee!
|
||||
@@ -25,14 +20,6 @@ module OrderManagement
|
||||
require_ids_allowed(parameters.shipping_method_ids, permissions.allowed_shipping_methods)
|
||||
require_ids_allowed(parameters.payment_method_ids, permissions.allowed_payment_methods)
|
||||
end
|
||||
|
||||
def require_ids_allowed(array, allowed_objects)
|
||||
error_klass = ::Reports::Authorizer::ParameterNotAllowedError
|
||||
error_message = self.class.parameter_not_allowed_error_message
|
||||
ids_allowed = (array - allowed_objects.map(&:id).map(&:to_s)).blank?
|
||||
|
||||
raise error_klass, error_message unless ids_allowed
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -19,11 +19,6 @@ module OrderManagement
|
||||
|
||||
validate :require_valid_datetime_range
|
||||
|
||||
def self.date_end_before_start_error_message
|
||||
i18n_scope = "order_management.reports.enterprise_fee_summary"
|
||||
I18n.t("date_end_before_start_error", scope: i18n_scope)
|
||||
end
|
||||
|
||||
def initialize(attributes = {})
|
||||
self.distributor_ids = []
|
||||
self.producer_ids = []
|
||||
@@ -42,13 +37,6 @@ module OrderManagement
|
||||
|
||||
protected
|
||||
|
||||
def require_valid_datetime_range
|
||||
return if start_at.blank? || end_at.blank?
|
||||
|
||||
error_message = self.class.date_end_before_start_error_message
|
||||
errors.add(:end_at, error_message) unless start_at < end_at
|
||||
end
|
||||
|
||||
# Remove the blank strings that Rails multiple selects add by default to
|
||||
# make sure that blank lists are still submitted to the server as arrays
|
||||
# instead of nil.
|
||||
|
||||
@@ -8,5 +8,20 @@ module Reports
|
||||
@parameters = parameters
|
||||
@permissions = permissions
|
||||
end
|
||||
|
||||
def self.parameter_not_allowed_error_message
|
||||
i18n_scope = "order_management.reports.enterprise_fee_summary"
|
||||
I18n.t("parameter_not_allowed_error", scope: i18n_scope)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def require_ids_allowed(array, allowed_objects)
|
||||
error_klass = ::Reports::Authorizer::ParameterNotAllowedError
|
||||
error_message = self.class.parameter_not_allowed_error_message
|
||||
ids_allowed = (array - allowed_objects.map(&:id).map(&:to_s)).blank?
|
||||
|
||||
raise error_klass, error_message unless ids_allowed
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -12,8 +12,22 @@ module Reports
|
||||
end
|
||||
end
|
||||
|
||||
def self.date_end_before_start_error_message
|
||||
i18n_scope = "order_management.reports.enterprise_fee_summary"
|
||||
I18n.t("date_end_before_start_error", scope: i18n_scope)
|
||||
end
|
||||
|
||||
# The parameters are never persisted.
|
||||
def to_key; end
|
||||
|
||||
protected
|
||||
|
||||
def require_valid_datetime_range
|
||||
return if start_at.blank? || end_at.blank?
|
||||
|
||||
error_message = self.class.date_end_before_start_error_message
|
||||
errors.add(:end_at, error_message) unless start_at < end_at
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
= form_for @report_parameters, as: :report, url: main_app.order_management_reports_bulk_coop_path, method: :post do |f|
|
||||
.row.date-range-filter
|
||||
.sixteen.columns.alpha
|
||||
= label_tag nil, t(".date_range")
|
||||
%br
|
||||
|
||||
= f.label :start_at, class: "inline"
|
||||
= f.text_field :start_at, class: "datetimepicker datepicker-from"
|
||||
|
||||
%span.range-divider
|
||||
%i.icon-arrow-right
|
||||
|
||||
= f.text_field :end_at, class: "datetimepicker datepicker-to"
|
||||
= f.label :end_at, class: "inline"
|
||||
|
||||
.row
|
||||
.sixteen.columns.alpha
|
||||
= f.label :distributor_ids
|
||||
= f.collection_select(:distributor_ids, @permissions.allowed_distributors, :id, :name, {}, {class: "select2 fullwidth", multiple: true})
|
||||
|
||||
.row
|
||||
.sixteen.columns.alpha
|
||||
= f.label :report_type
|
||||
= f.collection_select(:report_type, OrderManagement::Reports::BulkCoop::BulkCoopReport::REPORT_TYPES.map { |report_type| [t(".#{report_type}"), report_type] }, :last, :first, {}, {class: "select2 fullwidth", multiple: false})
|
||||
|
||||
.row
|
||||
.sixteen.columns.alpha
|
||||
= check_box_tag :report_format, "csv", false, id: "report_format_csv"
|
||||
= label_tag :report_format_csv, t(".report_format_csv")
|
||||
|
||||
= button t(".generate_report")
|
||||
@@ -0,0 +1,20 @@
|
||||
- if @report.present?
|
||||
%table#bulk_coop_report.report__table
|
||||
%thead
|
||||
%tr
|
||||
- @renderer.header.each do |heading|
|
||||
%th= heading
|
||||
|
||||
%tbody
|
||||
- @renderer.data_rows.each do |row|
|
||||
%tr
|
||||
- row.each do |cell_value|
|
||||
%td= cell_value
|
||||
|
||||
- if @renderer.data_rows.empty?
|
||||
%tr
|
||||
%td{colspan: @renderer.header.length}= t('.none')
|
||||
- else
|
||||
%p.report__message
|
||||
= t(".select_and_search")
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
= render "filters"
|
||||
= render "order_management/reports/report"
|
||||
@@ -0,0 +1 @@
|
||||
= render "filters"
|
||||
@@ -1,2 +1,2 @@
|
||||
= render "filters"
|
||||
= render "report"
|
||||
= render "order_management/reports/report"
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
Openfoodnetwork::Application.routes.prepend do
|
||||
namespace :order_management do
|
||||
namespace :reports do
|
||||
resource :bulk_coop, only: [:new, :create], controller: :bulk_coop
|
||||
resource :enterprise_fee_summary, only: [:new, :create]
|
||||
end
|
||||
end
|
||||
|
||||
@@ -0,0 +1,97 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
require "spec_helper"
|
||||
|
||||
describe OrderManagement::Reports::BulkCoopController, type: :controller do
|
||||
let(:report_klass) { OrderManagement::Reports::BulkCoop }
|
||||
|
||||
let!(:distributor) { create(:distributor_enterprise) }
|
||||
|
||||
let(:current_user) { distributor.owner }
|
||||
|
||||
before do
|
||||
allow(controller).to receive(:spree_current_user) { current_user }
|
||||
end
|
||||
|
||||
describe "#new" do
|
||||
it "renders the report form" do
|
||||
get :new
|
||||
|
||||
expect(response).to be_success
|
||||
expect(response).to render_template(new_template_path)
|
||||
end
|
||||
end
|
||||
|
||||
describe "#create" do
|
||||
context "when the parameters are valid" do
|
||||
it "sends the generated report in the correct format" do
|
||||
post :create, report: {
|
||||
start_at: "2018-10-09 07:30:00",
|
||||
report_type: "bulk_coop_supplier_report"
|
||||
}, report_format: "csv"
|
||||
|
||||
expect(response).to be_success
|
||||
expect(response.body).not_to be_blank
|
||||
expect(response.header["Content-Type"]).to eq("text/csv")
|
||||
end
|
||||
end
|
||||
|
||||
context "when the parameters are invalid" do
|
||||
it "renders the report form with an error" do
|
||||
post :create, report: {
|
||||
start_at: "invalid_date",
|
||||
report_type: "bulk_coop_supplier_report"
|
||||
}, report_format: "csv"
|
||||
|
||||
expect(flash[:error]).to eq(I18n.t("invalid_filter_parameters", scope: i18n_scope))
|
||||
expect(response).to render_template(new_template_path)
|
||||
end
|
||||
end
|
||||
|
||||
context "when some parameters are now allowed" do
|
||||
let!(:distributor) { create(:distributor_enterprise) }
|
||||
let!(:other_distributor) { create(:distributor_enterprise) }
|
||||
|
||||
let(:current_user) { distributor.owner }
|
||||
|
||||
it "renders the report form with an error" do
|
||||
post :create, report: {
|
||||
distributor_ids: [other_distributor.id],
|
||||
report_type: "bulk_coop_supplier_report"
|
||||
}, report_format: "csv"
|
||||
|
||||
expect(flash[:error]).to eq(report_klass::Authorizer.parameter_not_allowed_error_message)
|
||||
expect(response).to render_template(new_template_path)
|
||||
end
|
||||
end
|
||||
|
||||
describe "filtering results based on permissions" do
|
||||
let!(:distributor) { create(:distributor_enterprise) }
|
||||
let!(:other_distributor) { create(:distributor_enterprise) }
|
||||
|
||||
let(:current_user) { distributor.owner }
|
||||
|
||||
it "applies permissions to report" do
|
||||
post :create, report: {}, report_format: "csv"
|
||||
|
||||
expect(assigns(:permissions).allowed_distributors.to_a).to eq([distributor])
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def default_report_params
|
||||
{
|
||||
report_type: "bulk_coop_supplier_report"
|
||||
}
|
||||
end
|
||||
|
||||
def i18n_scope
|
||||
"order_management.reports.enterprise_fee_summary"
|
||||
end
|
||||
|
||||
def new_template_path
|
||||
"order_management/reports/bulk_coop/new"
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,17 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
require "spec_helper"
|
||||
|
||||
feature "bulk coop" do
|
||||
include AuthenticationWorkflow
|
||||
include WebHelper
|
||||
|
||||
scenario "bulk co-op report" do
|
||||
quick_login_as_admin
|
||||
visit spree.admin_reports_path
|
||||
click_link 'Bulk Co-Op'
|
||||
click_button 'Generate Report'
|
||||
|
||||
expect(page).to have_content 'Supplier'
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,81 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
require 'spec_helper'
|
||||
|
||||
describe OrderManagement::Reports::BulkCoop::BulkCoopReport do
|
||||
describe "fetching orders" do
|
||||
let(:d1) { create(:distributor_enterprise) }
|
||||
let(:oc1) { create(:simple_order_cycle) }
|
||||
let(:o1) { create(:order, completed_at: 1.day.ago, order_cycle: oc1, distributor: d1) }
|
||||
let(:li1) { build(:line_item_with_shipment) }
|
||||
|
||||
before { o1.line_items << li1 }
|
||||
|
||||
context "as a site admin" do
|
||||
let(:user) { create(:admin_user) }
|
||||
subject { OrderManagement::Reports::BulkCoop::BulkCoopReport.new user, {}, true }
|
||||
|
||||
it "fetches completed orders" do
|
||||
o2 = create(:order)
|
||||
o2.line_items << build(:line_item)
|
||||
expect(subject.table_items).to eq([li1])
|
||||
end
|
||||
|
||||
it "does not show cancelled orders" do
|
||||
o2 = create(:order, state: "canceled", completed_at: 1.day.ago)
|
||||
o2.line_items << build(:line_item_with_shipment)
|
||||
expect(subject.table_items).to eq([li1])
|
||||
end
|
||||
end
|
||||
|
||||
context "as a manager of a supplier" do
|
||||
let!(:user) { create(:user) }
|
||||
subject { OrderManagement::Reports::BulkCoop::BulkCoopReport.new user, {}, true }
|
||||
|
||||
let(:s1) { create(:supplier_enterprise) }
|
||||
|
||||
before do
|
||||
s1.enterprise_roles.create!(user: user)
|
||||
end
|
||||
|
||||
context "that has granted P-OC to the distributor" do
|
||||
let(:o2) do
|
||||
create(:order, distributor: d1, completed_at: 1.day.ago, bill_address: create(:address),
|
||||
ship_address: create(:address))
|
||||
end
|
||||
let(:li2) do
|
||||
build(:line_item_with_shipment, product: create(:simple_product, supplier: s1))
|
||||
end
|
||||
|
||||
before do
|
||||
o2.line_items << li2
|
||||
create(:enterprise_relationship, parent: s1, child: d1,
|
||||
permissions_list: [:add_to_order_cycle])
|
||||
end
|
||||
|
||||
it "shows line items supplied by my producers, with names hidden" do
|
||||
expect(subject.table_items).to eq([li2])
|
||||
expect(subject.table_items.first.order.bill_address.firstname).to eq("HIDDEN")
|
||||
end
|
||||
end
|
||||
|
||||
context "that has not granted P-OC to the distributor" do
|
||||
let(:o2) do
|
||||
create(:order, distributor: d1, completed_at: 1.day.ago, bill_address: create(:address),
|
||||
ship_address: create(:address))
|
||||
end
|
||||
let(:li2) do
|
||||
build(:line_item_with_shipment, product: create(:simple_product, supplier: s1))
|
||||
end
|
||||
|
||||
before do
|
||||
o2.line_items << li2
|
||||
end
|
||||
|
||||
it "does not show line items supplied by my producers" do
|
||||
expect(subject.table_items).to eq([])
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -1,140 +0,0 @@
|
||||
require 'open_food_network/reports/bulk_coop_supplier_report'
|
||||
require 'open_food_network/reports/bulk_coop_allocation_report'
|
||||
require "open_food_network/reports/line_items"
|
||||
|
||||
module OpenFoodNetwork
|
||||
class BulkCoopReport
|
||||
attr_reader :params
|
||||
def initialize(user, params = {}, render_table = false)
|
||||
@params = params
|
||||
@user = user
|
||||
@render_table = render_table
|
||||
|
||||
@supplier_report = Reports::BulkCoopSupplierReport.new
|
||||
@allocation_report = Reports::BulkCoopAllocationReport.new
|
||||
end
|
||||
|
||||
def header
|
||||
case params[:report_type]
|
||||
when "bulk_coop_supplier_report"
|
||||
@supplier_report.header
|
||||
when "bulk_coop_allocation"
|
||||
@allocation_report.header
|
||||
when "bulk_coop_packing_sheets"
|
||||
[I18n.t(:report_header_customer),
|
||||
I18n.t(:report_header_product),
|
||||
I18n.t(:report_header_variant),
|
||||
I18n.t(:report_header_sum_total)]
|
||||
when "bulk_coop_customer_payments"
|
||||
[I18n.t(:report_header_customer),
|
||||
I18n.t(:report_header_date_of_order),
|
||||
I18n.t(:report_header_total_cost),
|
||||
I18n.t(:report_header_amount_owing),
|
||||
I18n.t(:report_header_amount_paid)]
|
||||
else
|
||||
[I18n.t(:report_header_supplier),
|
||||
I18n.t(:report_header_product),
|
||||
I18n.t(:report_header_product),
|
||||
I18n.t(:report_header_bulk_unit_size),
|
||||
I18n.t(:report_header_variant),
|
||||
I18n.t(:report_header_weight),
|
||||
I18n.t(:report_header_sum_total),
|
||||
I18n.t(:report_header_sum_max_total),
|
||||
I18n.t(:report_header_units_required),
|
||||
I18n.t(:report_header_remainder)]
|
||||
end
|
||||
end
|
||||
|
||||
def search
|
||||
report_line_items.orders
|
||||
end
|
||||
|
||||
def table_items
|
||||
return [] unless @render_table
|
||||
|
||||
report_line_items.list(line_item_includes)
|
||||
end
|
||||
|
||||
def rules
|
||||
case params[:report_type]
|
||||
when "bulk_coop_supplier_report"
|
||||
@supplier_report.rules
|
||||
when "bulk_coop_allocation"
|
||||
@allocation_report.rules
|
||||
when "bulk_coop_packing_sheets"
|
||||
[{ group_by: proc { |li| li.product },
|
||||
sort_by: proc { |product| product.name } },
|
||||
{ group_by: proc { |li| li.full_name },
|
||||
sort_by: proc { |full_name| full_name } },
|
||||
{ group_by: proc { |li| li.order },
|
||||
sort_by: proc { |order| order.to_s } }]
|
||||
when "bulk_coop_customer_payments"
|
||||
[{ group_by: proc { |li| li.order },
|
||||
sort_by: proc { |order| order.completed_at } }]
|
||||
else
|
||||
[{ group_by: proc { |li| li.product.supplier },
|
||||
sort_by: proc { |supplier| supplier.name } },
|
||||
{ group_by: proc { |li| li.product },
|
||||
sort_by: proc { |product| product.name },
|
||||
summary_columns: [proc { |lis| lis.first.product.supplier.name },
|
||||
proc { |lis| lis.first.product.name },
|
||||
proc { |lis| lis.first.product.group_buy_unit_size || 0.0 },
|
||||
proc { |_lis| "" },
|
||||
proc { |_lis| "" },
|
||||
proc { |lis| lis.sum { |li| li.quantity * (li.weight_from_unit_value || 0) } },
|
||||
proc { |lis| lis.sum { |li| (li.max_quantity || 0) * (li.weight_from_unit_value || 0) } },
|
||||
proc { |lis| ( (lis.first.product.group_buy_unit_size || 0).zero? ? 0 : ( lis.sum { |li| [li.max_quantity || 0, li.quantity || 0].max * (li.weight_from_unit_value || 0) } / lis.first.product.group_buy_unit_size ) ).floor },
|
||||
proc { |lis| lis.sum { |li| [li.max_quantity || 0, li.quantity || 0].max * (li.weight_from_unit_value || 0) } - ( ( (lis.first.product.group_buy_unit_size || 0).zero? ? 0 : ( lis.sum { |li| [li.max_quantity || 0, li.quantity || 0].max * (li.weight_from_unit_value || 0) } / lis.first.product.group_buy_unit_size ) ).floor * (lis.first.product.group_buy_unit_size || 0) ) }] },
|
||||
{ group_by: proc { |li| li.full_name },
|
||||
sort_by: proc { |full_name| full_name } }]
|
||||
end
|
||||
end
|
||||
|
||||
def columns
|
||||
case params[:report_type]
|
||||
when "bulk_coop_supplier_report"
|
||||
@supplier_report.columns
|
||||
when "bulk_coop_allocation"
|
||||
@allocation_report.columns
|
||||
when "bulk_coop_packing_sheets"
|
||||
[proc { |lis| lis.first.order.bill_address.firstname + " " + lis.first.order.bill_address.lastname },
|
||||
proc { |lis| lis.first.product.name },
|
||||
proc { |lis| lis.first.full_name },
|
||||
proc { |lis| lis.sum(&:quantity) }]
|
||||
when "bulk_coop_customer_payments"
|
||||
[proc { |lis| lis.first.order.bill_address.firstname + " " + lis.first.order.bill_address.lastname },
|
||||
proc { |lis| lis.first.order.completed_at.to_s },
|
||||
proc { |lis| lis.map(&:order).uniq.sum(&:total) },
|
||||
proc { |lis| lis.map(&:order).uniq.sum(&:outstanding_balance) },
|
||||
proc { |lis| lis.map(&:order).uniq.sum(&:payment_total) }]
|
||||
else
|
||||
[proc { |lis| lis.first.product.supplier.name },
|
||||
proc { |lis| lis.first.product.name },
|
||||
proc { |lis| lis.first.product.group_buy_unit_size || 0.0 },
|
||||
proc { |lis| lis.first.full_name },
|
||||
proc { |lis| lis.first.weight_from_unit_value || 0 },
|
||||
proc { |lis| lis.sum(&:quantity) },
|
||||
proc { |lis| lis.sum { |li| li.max_quantity || 0 } },
|
||||
proc { |_lis| "" },
|
||||
proc { |_lis| "" }]
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def line_item_includes
|
||||
[{ order: [:bill_address],
|
||||
variant: [{ option_values: :option_type }, { product: :supplier }] }]
|
||||
end
|
||||
|
||||
def order_permissions
|
||||
return @order_permissions unless @order_permissions.nil?
|
||||
|
||||
@order_permissions = ::Permissions::Order.new(@user, @params[:q])
|
||||
end
|
||||
|
||||
def report_line_items
|
||||
@report_line_items ||= Reports::LineItems.new(order_permissions, @params)
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -1,8 +1,9 @@
|
||||
module OpenFoodNetwork
|
||||
class OrderGrouper
|
||||
def initialize(rules, column_constructors)
|
||||
def initialize(rules, column_constructors, report = nil)
|
||||
@rules = rules
|
||||
@column_constructors = column_constructors
|
||||
@report = report
|
||||
end
|
||||
|
||||
def build_tree(items, remaining_rules)
|
||||
@@ -38,11 +39,11 @@ module OpenFoodNetwork
|
||||
def build_table(groups)
|
||||
rows = []
|
||||
if is_leaf_node(groups)
|
||||
rows << @column_constructors.map { |column_constructor| column_constructor.call(groups) }
|
||||
rows << build_row(groups)
|
||||
else
|
||||
groups.each do |key, group|
|
||||
if key == :summary_row
|
||||
rows << group[:columns].map { |cols| cols.call(group[:items]) }
|
||||
rows << build_summary_row(group[:columns], group[:items])
|
||||
else
|
||||
build_table(group).each { |g| rows << g }
|
||||
end
|
||||
@@ -59,6 +60,26 @@ module OpenFoodNetwork
|
||||
|
||||
private
|
||||
|
||||
def build_cell(column_constructor, items)
|
||||
if column_constructor.is_a?(Symbol)
|
||||
@report.__send__(column_constructor, items)
|
||||
else
|
||||
column_constructor.call(items)
|
||||
end
|
||||
end
|
||||
|
||||
def build_row(groups)
|
||||
@column_constructors.map do |column_constructor|
|
||||
build_cell(column_constructor, groups)
|
||||
end
|
||||
end
|
||||
|
||||
def build_summary_row(summary_row_column_constructors, items)
|
||||
summary_row_column_constructors.map do |summary_row_column_constructor|
|
||||
build_cell(summary_row_column_constructor, items)
|
||||
end
|
||||
end
|
||||
|
||||
def is_leaf_node(node)
|
||||
node.is_a? Array
|
||||
end
|
||||
|
||||
@@ -1,64 +0,0 @@
|
||||
require 'open_food_network/reports/bulk_coop_report'
|
||||
|
||||
module OpenFoodNetwork::Reports
|
||||
class BulkCoopAllocationReport < BulkCoopReport
|
||||
def header
|
||||
[
|
||||
I18n.t(:report_header_customer),
|
||||
I18n.t(:report_header_product),
|
||||
I18n.t(:report_header_bulk_unit_size),
|
||||
I18n.t(:report_header_variant),
|
||||
I18n.t(:report_header_variant_value),
|
||||
I18n.t(:report_header_variant_unit),
|
||||
I18n.t(:report_header_weight),
|
||||
I18n.t(:report_header_sum_total),
|
||||
I18n.t(:report_header_total_available),
|
||||
I18n.t(:report_header_unallocated),
|
||||
I18n.t(:report_header_max_quantity_excess),
|
||||
]
|
||||
end
|
||||
|
||||
organise do
|
||||
group(&:product)
|
||||
sort(&:name)
|
||||
|
||||
summary_row do
|
||||
column { |_lis| I18n.t('admin.reports.total') }
|
||||
column { |lis| product_name(lis) }
|
||||
column { |lis| group_buy_unit_size_f(lis) }
|
||||
column { |_lis| "" }
|
||||
column { |_lis| "" }
|
||||
column { |_lis| "" }
|
||||
column { |_lis| "" }
|
||||
column { |lis| total_amount(lis) }
|
||||
column { |lis| total_available(lis) }
|
||||
column { |lis| remainder(lis) }
|
||||
column { |lis| max_quantity_excess(lis) }
|
||||
end
|
||||
|
||||
organise do
|
||||
group(&:full_name)
|
||||
sort { |full_name| full_name }
|
||||
|
||||
organise do
|
||||
group(&:order)
|
||||
sort(&:to_s)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
columns do
|
||||
column { |lis| lis.first.order.bill_address.firstname + " " + lis.first.order.bill_address.lastname }
|
||||
column { |lis| lis.first.product.name }
|
||||
column { |lis| lis.first.product.group_buy_unit_size || 0.0 }
|
||||
column { |lis| lis.first.full_name }
|
||||
column { |lis| OpenFoodNetwork::OptionValueNamer.new(lis.first).value }
|
||||
column { |lis| OpenFoodNetwork::OptionValueNamer.new(lis.first).unit }
|
||||
column { |lis| lis.first.weight_from_unit_value || 0 }
|
||||
column { |lis| total_amount(lis) }
|
||||
column { |_lis| "" }
|
||||
column { |_lis| "" }
|
||||
column { |_lis| "" }
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -1,66 +0,0 @@
|
||||
require 'open_food_network/reports/report'
|
||||
|
||||
module OpenFoodNetwork::Reports
|
||||
class BulkCoopReport < Report
|
||||
private
|
||||
|
||||
class << self
|
||||
def supplier_name(lis)
|
||||
lis.first.variant.product.supplier.name
|
||||
end
|
||||
|
||||
def product_name(lis)
|
||||
lis.first.variant.product.name
|
||||
end
|
||||
|
||||
def group_buy_unit_size(lis)
|
||||
(lis.first.variant.product.group_buy_unit_size || 0.0) /
|
||||
(lis.first.product.variant_unit_scale || 1)
|
||||
end
|
||||
|
||||
def group_buy_unit_size_f(lis)
|
||||
group_buy_unit_size(lis)
|
||||
end
|
||||
|
||||
def total_amount(lis)
|
||||
lis.sum { |li| scaled_final_weight_volume(li) }
|
||||
end
|
||||
|
||||
def units_required(lis)
|
||||
if group_buy_unit_size(lis).zero?
|
||||
0
|
||||
else
|
||||
( total_amount(lis) / group_buy_unit_size(lis) ).ceil
|
||||
end
|
||||
end
|
||||
|
||||
def total_available(lis)
|
||||
units_required(lis) * group_buy_unit_size(lis)
|
||||
end
|
||||
|
||||
def remainder(lis)
|
||||
remainder = total_available(lis) - total_amount(lis)
|
||||
remainder >= 0 ? remainder : ''
|
||||
end
|
||||
|
||||
def max_quantity_excess(lis)
|
||||
max_quantity_amount(lis) - total_amount(lis)
|
||||
end
|
||||
|
||||
def max_quantity_amount(lis)
|
||||
lis.sum do |li|
|
||||
max_quantity = [li.max_quantity || 0, li.quantity || 0].max
|
||||
max_quantity * scaled_unit_value(li.variant)
|
||||
end
|
||||
end
|
||||
|
||||
def scaled_final_weight_volume(line_item)
|
||||
(line_item.final_weight_volume || 0) / (line_item.product.variant_unit_scale || 1)
|
||||
end
|
||||
|
||||
def scaled_unit_value(variant)
|
||||
(variant.unit_value || 0) / (variant.product.variant_unit_scale || 1)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -1,64 +0,0 @@
|
||||
require 'open_food_network/reports/bulk_coop_report'
|
||||
|
||||
module OpenFoodNetwork::Reports
|
||||
class BulkCoopSupplierReport < BulkCoopReport
|
||||
def header
|
||||
[
|
||||
I18n.t(:report_header_supplier),
|
||||
I18n.t(:report_header_product),
|
||||
I18n.t(:report_header_bulk_unit_size),
|
||||
I18n.t(:report_header_variant),
|
||||
I18n.t(:report_header_variant_value),
|
||||
I18n.t(:report_header_variant_unit),
|
||||
I18n.t(:report_header_weight),
|
||||
I18n.t(:report_header_sum_total),
|
||||
I18n.t(:report_header_units_required),
|
||||
I18n.t(:report_header_unallocated),
|
||||
I18n.t(:report_header_max_quantity_excess),
|
||||
]
|
||||
end
|
||||
|
||||
organise do
|
||||
group { |li| li.product.supplier }
|
||||
sort(&:name)
|
||||
|
||||
organise do
|
||||
group(&:product)
|
||||
sort(&:name)
|
||||
|
||||
summary_row do
|
||||
column { |lis| supplier_name(lis) }
|
||||
column { |lis| product_name(lis) }
|
||||
column { |lis| group_buy_unit_size_f(lis) }
|
||||
column { |_lis| "" }
|
||||
column { |_lis| "" }
|
||||
column { |_lis| "" }
|
||||
column { |_lis| "" }
|
||||
column { |lis| total_amount(lis) }
|
||||
column { |lis| units_required(lis) }
|
||||
column { |lis| remainder(lis) }
|
||||
column { |lis| max_quantity_excess(lis) }
|
||||
end
|
||||
|
||||
organise do
|
||||
group(&:full_name)
|
||||
sort { |full_name| full_name }
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
columns do
|
||||
column { |lis| supplier_name(lis) }
|
||||
column { |lis| product_name(lis) }
|
||||
column { |lis| group_buy_unit_size_f(lis) }
|
||||
column { |lis| lis.first.full_name }
|
||||
column { |lis| OpenFoodNetwork::OptionValueNamer.new(lis.first).value }
|
||||
column { |lis| OpenFoodNetwork::OptionValueNamer.new(lis.first).unit }
|
||||
column { |lis| lis.first.weight_from_unit_value || 0 }
|
||||
column { |lis| total_amount(lis) }
|
||||
column { |_lis| '' }
|
||||
column { |_lis| '' }
|
||||
column { |_lis| '' }
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -31,15 +31,5 @@ module OpenFoodNetwork::Reports
|
||||
def self.header(*columns)
|
||||
self._header = columns
|
||||
end
|
||||
|
||||
def self.columns(&block)
|
||||
self._columns = Row.new
|
||||
Blockenspiel.invoke block, _columns
|
||||
end
|
||||
|
||||
def self.organise(&block)
|
||||
self._rules_head = Rule.new
|
||||
Blockenspiel.invoke block, _rules_head
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
module OpenFoodNetwork::Reports
|
||||
class Row
|
||||
include Blockenspiel::DSL
|
||||
|
||||
def initialize
|
||||
@columns = []
|
||||
end
|
||||
|
||||
@@ -2,7 +2,6 @@ require 'open_food_network/reports/row'
|
||||
|
||||
module OpenFoodNetwork::Reports
|
||||
class Rule
|
||||
include Blockenspiel::DSL
|
||||
attr_reader :next
|
||||
|
||||
def group(&block)
|
||||
@@ -13,16 +12,6 @@ module OpenFoodNetwork::Reports
|
||||
@sort = block
|
||||
end
|
||||
|
||||
def summary_row(&block)
|
||||
@summary_row = Row.new
|
||||
Blockenspiel.invoke block, @summary_row
|
||||
end
|
||||
|
||||
def organise(&block)
|
||||
@next = Rule.new
|
||||
Blockenspiel.invoke block, @next
|
||||
end
|
||||
|
||||
def to_h
|
||||
h = { group_by: @group, sort_by: @sort }
|
||||
h[:summary_columns] = @summary_row.to_a if @summary_row
|
||||
|
||||
@@ -93,18 +93,6 @@ describe Spree::Admin::ReportsController, type: :controller do
|
||||
end
|
||||
end
|
||||
|
||||
describe 'Bulk Coop' do
|
||||
let!(:present_objects) { [orderA1, orderA2, orderB1, orderB2] }
|
||||
|
||||
it "only shows orders that I have access to" do
|
||||
spree_post :bulk_coop, q: {}
|
||||
|
||||
expect(resulting_orders).to include(orderA1, orderB1)
|
||||
expect(resulting_orders).not_to include(orderA2)
|
||||
expect(resulting_orders).not_to include(orderB2)
|
||||
end
|
||||
end
|
||||
|
||||
describe 'Payments' do
|
||||
let!(:present_objects) { [orderA1, orderA2, orderB1, orderB2] }
|
||||
|
||||
@@ -156,31 +144,6 @@ describe Spree::Admin::ReportsController, type: :controller do
|
||||
end
|
||||
end
|
||||
|
||||
describe 'Bulk Coop' do
|
||||
context "where I have granted P-OC to the distributor" do
|
||||
let!(:present_objects) { [orderA1, orderA2] }
|
||||
|
||||
before do
|
||||
create(:enterprise_relationship, parent: supplier1, child: distributor1, permissions_list: [:add_to_order_cycle])
|
||||
end
|
||||
|
||||
it "only shows product line items that I am supplying" do
|
||||
spree_post :bulk_coop, q: {}
|
||||
|
||||
expect(resulting_products).to include product1
|
||||
expect(resulting_products).not_to include product2, product3
|
||||
end
|
||||
end
|
||||
|
||||
context "where I have not granted P-OC to the distributor" do
|
||||
it "shows product line items that I am supplying" do
|
||||
spree_post :bulk_coop
|
||||
|
||||
expect(resulting_products).not_to include product1, product2, product3
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe 'Orders & Fulfillment' do
|
||||
let!(:present_objects) { [orderA1, orderA2] }
|
||||
|
||||
|
||||
@@ -155,15 +155,6 @@ feature '
|
||||
expect(page).to have_content 'Order date'
|
||||
end
|
||||
|
||||
scenario "bulk co-op report" do
|
||||
quick_login_as_admin
|
||||
visit spree.admin_reports_path
|
||||
click_link 'Bulk Co-Op'
|
||||
click_button 'Search'
|
||||
|
||||
expect(page).to have_content 'Supplier'
|
||||
end
|
||||
|
||||
scenario "payments reports" do
|
||||
quick_login_as_admin
|
||||
visit spree.admin_reports_path
|
||||
|
||||
@@ -1,98 +0,0 @@
|
||||
require 'spec_helper'
|
||||
require 'open_food_network/bulk_coop_report'
|
||||
|
||||
include AuthenticationWorkflow
|
||||
|
||||
module OpenFoodNetwork
|
||||
describe BulkCoopReport do
|
||||
describe "fetching orders" do
|
||||
let(:d1) { create(:distributor_enterprise) }
|
||||
let(:oc1) { create(:simple_order_cycle) }
|
||||
let(:o1) { create(:order, completed_at: 1.day.ago, order_cycle: oc1, distributor: d1) }
|
||||
let(:li1) { build(:line_item_with_shipment) }
|
||||
|
||||
before { o1.line_items << li1 }
|
||||
|
||||
context "as a site admin" do
|
||||
let(:user) { create(:admin_user) }
|
||||
subject { BulkCoopReport.new user, {}, true }
|
||||
|
||||
it "fetches completed orders" do
|
||||
o2 = create(:order)
|
||||
o2.line_items << build(:line_item)
|
||||
expect(subject.table_items).to eq([li1])
|
||||
end
|
||||
|
||||
it "does not show cancelled orders" do
|
||||
o2 = create(:order, state: "canceled", completed_at: 1.day.ago)
|
||||
o2.line_items << build(:line_item_with_shipment)
|
||||
expect(subject.table_items).to eq([li1])
|
||||
end
|
||||
end
|
||||
|
||||
context "as a manager of a supplier" do
|
||||
let!(:user) { create(:user) }
|
||||
subject { BulkCoopReport.new user, {}, true }
|
||||
|
||||
let(:s1) { create(:supplier_enterprise) }
|
||||
|
||||
before do
|
||||
s1.enterprise_roles.create!(user: user)
|
||||
end
|
||||
|
||||
context "that has granted P-OC to the distributor" do
|
||||
let(:o2) { create(:order, distributor: d1, completed_at: 1.day.ago, bill_address: create(:address), ship_address: create(:address)) }
|
||||
let(:li2) { build(:line_item_with_shipment, product: create(:simple_product, supplier: s1)) }
|
||||
|
||||
before do
|
||||
o2.line_items << li2
|
||||
create(:enterprise_relationship, parent: s1, child: d1, permissions_list: [:add_to_order_cycle])
|
||||
end
|
||||
|
||||
it "shows line items supplied by my producers, with names hidden" do
|
||||
expect(subject.table_items).to eq([li2])
|
||||
expect(subject.table_items.first.order.bill_address.firstname).to eq("HIDDEN")
|
||||
end
|
||||
end
|
||||
|
||||
context "that has not granted P-OC to the distributor" do
|
||||
let(:o2) { create(:order, distributor: d1, completed_at: 1.day.ago, bill_address: create(:address), ship_address: create(:address)) }
|
||||
let(:li2) { build(:line_item_with_shipment, product: create(:simple_product, supplier: s1)) }
|
||||
|
||||
before do
|
||||
o2.line_items << li2
|
||||
end
|
||||
|
||||
it "does not show line items supplied by my producers" do
|
||||
expect(subject.table_items).to eq([])
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context "as a manager of a distributor" do
|
||||
let!(:user) { create(:user) }
|
||||
subject { PackingReport.new user, {}, true }
|
||||
|
||||
before do
|
||||
d1.enterprise_roles.create!(user: user)
|
||||
end
|
||||
|
||||
it "only shows line items distributed by enterprises managed by the current user" do
|
||||
d2 = create(:distributor_enterprise)
|
||||
d2.enterprise_roles.create!(user: create(:user))
|
||||
o2 = create(:order, distributor: d2, completed_at: 1.day.ago)
|
||||
o2.line_items << build(:line_item_with_shipment)
|
||||
expect(subject.table_items).to eq([li1])
|
||||
end
|
||||
|
||||
it "only shows the selected order cycle" do
|
||||
oc2 = create(:simple_order_cycle)
|
||||
o2 = create(:order, distributor: d1, order_cycle: oc2)
|
||||
o2.line_items << build(:line_item)
|
||||
allow(subject).to receive(:params).and_return(order_cycle_id_in: oc1.id)
|
||||
expect(subject.table_items).to eq([li1])
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -1,98 +1,15 @@
|
||||
require 'open_food_network/reports/report'
|
||||
|
||||
module OpenFoodNetwork::Reports
|
||||
P1 = proc { |o| o[:one] }
|
||||
P2 = proc { |o| o[:two] }
|
||||
P3 = proc { |o| o[:three] }
|
||||
P4 = proc { |o| o[:four] }
|
||||
|
||||
class TestReport < Report
|
||||
header 'One', 'Two', 'Three', 'Four'
|
||||
|
||||
columns do
|
||||
column(&P1)
|
||||
column(&P2)
|
||||
column(&P3)
|
||||
column(&P4)
|
||||
end
|
||||
|
||||
organise do
|
||||
group(&P1)
|
||||
sort(&P2)
|
||||
|
||||
organise do
|
||||
group(&P3)
|
||||
sort(&P4)
|
||||
|
||||
summary_row do
|
||||
column(&P1)
|
||||
column(&P4)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
class HelperReport < Report
|
||||
columns do
|
||||
column { |obj| my_helper(obj) }
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def self.my_helper(obj)
|
||||
obj[:one]
|
||||
end
|
||||
end
|
||||
|
||||
describe Report do
|
||||
let(:report) { TestReport.new }
|
||||
let(:helper_report) { HelperReport.new }
|
||||
let(:rules_head) { TestReport._rules_head }
|
||||
let(:data) { { one: 1, two: 2, three: 3, four: 4 } }
|
||||
|
||||
it "returns the header" do
|
||||
expect(report.header).to eq(%w(One Two Three Four))
|
||||
end
|
||||
|
||||
it "returns columns as an array of procs" do
|
||||
expect(report.columns[0].call(data)).to eq(1)
|
||||
expect(report.columns[1].call(data)).to eq(2)
|
||||
expect(report.columns[2].call(data)).to eq(3)
|
||||
expect(report.columns[3].call(data)).to eq(4)
|
||||
end
|
||||
|
||||
it "supports helpers when outputting columns" do
|
||||
expect(helper_report.columns[0].call(data)).to eq(1)
|
||||
end
|
||||
|
||||
describe "rules" do
|
||||
let(:group_by) { rules_head.to_h[:group_by] }
|
||||
let(:sort_by) { rules_head.to_h[:sort_by] }
|
||||
let(:next_group_by) { rules_head.next.to_h[:group_by] }
|
||||
let(:next_sort_by) { rules_head.next.to_h[:sort_by] }
|
||||
let(:next_summary_columns) { rules_head.next.to_h[:summary_columns] }
|
||||
|
||||
it "constructs the head of the rules list" do
|
||||
expect(group_by.call(data)).to eq(1)
|
||||
expect(sort_by.call(data)).to eq(2)
|
||||
end
|
||||
|
||||
it "constructs nested rules" do
|
||||
expect(next_group_by.call(data)).to eq(3)
|
||||
expect(next_sort_by.call(data)).to eq(4)
|
||||
end
|
||||
|
||||
it "constructs summary columns for rules" do
|
||||
expect(next_summary_columns[0].call(data)).to eq(1)
|
||||
expect(next_summary_columns[1].call(data)).to eq(4)
|
||||
end
|
||||
end
|
||||
|
||||
describe "outputting rules" do
|
||||
it "outputs the rules" do
|
||||
expect(report.rules).to eq([{ group_by: P1, sort_by: P2 },
|
||||
{ group_by: P3, sort_by: P4, summary_columns: [P1, P4] }])
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -17,23 +17,5 @@ module OpenFoodNetwork::Reports
|
||||
rule.sort(&proc)
|
||||
expect(rule.to_h).to eq(group_by: nil, sort_by: proc)
|
||||
end
|
||||
|
||||
it "can define a nested rule" do
|
||||
rule.organise(&proc)
|
||||
expect(rule.next).to be_a Rule
|
||||
end
|
||||
|
||||
it "can define a summary row and return it in a hash" do
|
||||
rule.summary_row do
|
||||
column {}
|
||||
column {}
|
||||
column {}
|
||||
end
|
||||
|
||||
expect(rule.to_h[:summary_columns].count).to eq(3)
|
||||
expect(rule.to_h[:summary_columns][0]).to be_a Proc
|
||||
expect(rule.to_h[:summary_columns][1]).to be_a Proc
|
||||
expect(rule.to_h[:summary_columns][2]).to be_a Proc
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
Reference in New Issue
Block a user