mirror of
https://github.com/openfoodfoundation/openfoodnetwork
synced 2026-01-31 21:37:16 +00:00
Merge pull request #3479 from luisramos0/2-0-stable-Feb12
[Spree Upgrade] Merging master into 2-0-stable (2nd run in Feb2019)
This commit is contained in:
61
.github/ISSUE_TEMPLATE/bug_report.md
vendored
Normal file
61
.github/ISSUE_TEMPLATE/bug_report.md
vendored
Normal file
@@ -0,0 +1,61 @@
|
||||
---
|
||||
name: Bug report
|
||||
about: Create a report to help us improve
|
||||
title: ''
|
||||
labels: ''
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
## Description
|
||||
<!-- Provide a more detailed introduction to the issue itself, and why you consider it to be a bug -->
|
||||
|
||||
|
||||
## Expected Behavior
|
||||
<!-- Tell us what should happen -->
|
||||
|
||||
|
||||
## Actual Behaviour
|
||||
<!-- Tell us what happens instead -->
|
||||
|
||||
|
||||
## Steps to Reproduce
|
||||
<!-- Provide an unambiguous set of steps to reproduce this bug -->
|
||||
<!-- Include code to reproduce if relevant -->
|
||||
|
||||
1.
|
||||
2.
|
||||
3.
|
||||
4.
|
||||
|
||||
## Animated Gif/Screenshot
|
||||
<!-- Provide a screenshot or brief animated gif reproducing the bug. Linux users can use
|
||||
[Peek](https://github.com/phw/peek#ubuntu) while Mac users can use [Recordit](http://recordit.co/) -->
|
||||
|
||||
|
||||
## Context
|
||||
<!-- How has this bug affected you? What were you trying to accomplish? -->
|
||||
|
||||
|
||||
## Severity
|
||||
<!-- Assign a label and explain the impact.
|
||||
|
||||
bug-s1: a critical feature is broken: checkout, payments, signup, login
|
||||
bug-s2: a non-critical feature is broken, no workaround
|
||||
bug-s3: a feature is broken but there is a workaround
|
||||
bug-s4: it's annoying, but you can use it
|
||||
bug-s5: we can live with it, only a few users impacted
|
||||
|
||||
https://github.com/openfoodfoundation/openfoodnetwork/wiki/Bug-severity
|
||||
-->
|
||||
|
||||
## Your Environment
|
||||
<!-- Include relevant details about the environment you experienced the bug in -->
|
||||
|
||||
* Version used:
|
||||
* Browser name and version:
|
||||
* Operating System and version (desktop or mobile):
|
||||
* OFN Platform instance where you discovered the bug, and which version of the software they are using.
|
||||
|
||||
## Possible Fix
|
||||
<!-- Not obligatory, but suggest a fix or reason for the bug -->
|
||||
35
.github/ISSUE_TEMPLATE/feature-template.md
vendored
Normal file
35
.github/ISSUE_TEMPLATE/feature-template.md
vendored
Normal file
@@ -0,0 +1,35 @@
|
||||
---
|
||||
name: Feature template
|
||||
about: Create feature epics that detail the larger feature or functionality to be
|
||||
delivered.
|
||||
title: ''
|
||||
labels: ''
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
## What is the problem we are solving
|
||||
<!-- Describe the problem this feature is supposed to solve. Should be described in the icebox item (in Discourse). -->
|
||||
|
||||
## Success factors = expected outcome
|
||||
<!-- Describe what is the expected outcome: when the feature is released, what would it look like? What will make you say "the problem is solved"? Can be metrics like "Platform has less than 1% service interruption", or yes/no statements like "People can checkout on mobiles. -->
|
||||
|
||||
## Useful information for inception
|
||||
<!-- List here any information that can be useful for the inception stage. -->
|
||||
|
||||
## Link to the "Product Development - Backlog" item in Discourse
|
||||
<!-- Put the link here, and put this epic link in the Discourse item as well for cross-referencing. -->
|
||||
Add a custom footer
|
||||
Pages 70
|
||||
Home
|
||||
Development environment setup
|
||||
|
||||
macOS (Sierra, HighSierra and Mojave)
|
||||
OS X (El Capitan)
|
||||
OS X (Mavericks)
|
||||
Ubuntu
|
||||
On Heroku
|
||||
Rubocop
|
||||
General guidelines
|
||||
|
||||
Spree Commerce customisation
|
||||
18
.github/ISSUE_TEMPLATE/story-template.md
vendored
Normal file
18
.github/ISSUE_TEMPLATE/story-template.md
vendored
Normal file
@@ -0,0 +1,18 @@
|
||||
---
|
||||
name: Story template
|
||||
about: Create stories that are small chunks of work that devs will pick up and deliver
|
||||
title: ''
|
||||
labels: ''
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
**## Description**
|
||||
<!-- Describe the story in detail:
|
||||
**- As a:** (enterprise user, super admin, user...)
|
||||
**- On page:** (provide url of the page you want to modify. If not provide where will be created the new url and the name we want to give it)
|
||||
**- I want to be able to do:** (specify the desired behavior)
|
||||
(Link to others issues or resources to provide context > only if really necessary). -->
|
||||
|
||||
**## Acceptance Criteria**
|
||||
<!-- Document the outcomes that need to be achieved before this component can be considered complete. -->
|
||||
@@ -194,6 +194,8 @@ Metrics/BlockNesting:
|
||||
|
||||
Metrics/ClassLength:
|
||||
Max: 100
|
||||
Exclude:
|
||||
- engines/order_management/app/services/order_management/reports/enterprise_fee_summary/scope.rb
|
||||
|
||||
Metrics/ModuleLength:
|
||||
Max: 100
|
||||
@@ -203,6 +205,8 @@ Metrics/CyclomaticComplexity:
|
||||
|
||||
Metrics/MethodLength:
|
||||
Max: 10
|
||||
Exclude:
|
||||
- engines/order_management/app/services/order_management/reports/enterprise_fee_summary/scope.rb
|
||||
|
||||
Metrics/ParameterLists:
|
||||
Max: 5
|
||||
|
||||
5
Gemfile
5
Gemfile
@@ -10,6 +10,7 @@ gem 'i18n-js', '~> 3.1.0'
|
||||
# Patched version. See http://rubysec.com/advisories/CVE-2015-5312/.
|
||||
gem 'nokogiri', '>= 1.6.7.1'
|
||||
|
||||
gem "order_management", path: "./engines/order_management"
|
||||
gem 'web', path: './engines/web'
|
||||
|
||||
gem 'pg'
|
||||
@@ -26,13 +27,13 @@ gem 'spree_auth_devise', github: 'spree/spree_auth_devise', branch: '2-0-stable'
|
||||
# - Pass customer email and phone number to PayPal (merged to upstream master)
|
||||
# - Change type of password from string to password to hide it in the form
|
||||
gem 'spree_paypal_express', github: "openfoodfoundation/better_spree_paypal_express", branch: "2-0-stable"
|
||||
gem 'stripe', '~> 3.3.2'
|
||||
gem 'stripe', '~> 4.5.0'
|
||||
# We need at least this version to have Digicert's root certificate
|
||||
# which is needed for Pin Payments (and possibly others).
|
||||
gem 'activemerchant', '~> 1.78'
|
||||
|
||||
gem 'oauth2', '~> 1.4.1' # Used for Stripe Connect
|
||||
gem 'jwt', '~> 1.5'
|
||||
gem 'jwt', '~> 2.1'
|
||||
|
||||
gem 'delayed_job_active_record'
|
||||
gem 'daemons'
|
||||
|
||||
22
Gemfile.lock
22
Gemfile.lock
@@ -118,6 +118,11 @@ GIT
|
||||
activemodel (>= 3.0)
|
||||
railties (>= 3.0)
|
||||
|
||||
PATH
|
||||
remote: engines/order_management
|
||||
specs:
|
||||
order_management (0.0.1)
|
||||
|
||||
PATH
|
||||
remote: engines/web
|
||||
specs:
|
||||
@@ -240,6 +245,7 @@ GEM
|
||||
compass (~> 1.0.0)
|
||||
sass-rails (< 5.1)
|
||||
sprockets (< 4.0)
|
||||
connection_pool (2.2.2)
|
||||
crack (0.4.3)
|
||||
safe_yaml (~> 1.0.0)
|
||||
css_parser (1.6.0)
|
||||
@@ -501,7 +507,7 @@ GEM
|
||||
json_spec (1.1.5)
|
||||
multi_json (~> 1.0)
|
||||
rspec (>= 2.0, < 4.0)
|
||||
jwt (1.5.6)
|
||||
jwt (2.1.0)
|
||||
kaminari (0.14.1)
|
||||
actionpack (>= 3.0.0)
|
||||
activesupport (>= 3.0.0)
|
||||
@@ -532,6 +538,8 @@ GEM
|
||||
multi_xml (0.6.0)
|
||||
multipart-post (2.0.0)
|
||||
nenv (0.3.0)
|
||||
net-http-persistent (3.0.0)
|
||||
connection_pool (~> 2.2)
|
||||
nokogiri (1.6.8.1)
|
||||
mini_portile2 (~> 2.1.0)
|
||||
notiffany (0.1.1)
|
||||
@@ -623,7 +631,7 @@ GEM
|
||||
trollop (~> 2.1)
|
||||
rdoc (3.12.2)
|
||||
json (~> 1.4)
|
||||
redcarpet (3.2.3)
|
||||
redcarpet (3.4.0)
|
||||
ref (2.0.0)
|
||||
request_store (1.4.1)
|
||||
rack (>= 1.4)
|
||||
@@ -711,8 +719,9 @@ GEM
|
||||
tilt (~> 1.1, != 1.3.0)
|
||||
state_machine (1.2.0)
|
||||
stringex (1.5.1)
|
||||
stripe (3.3.2)
|
||||
faraday (~> 0.9)
|
||||
stripe (4.5.0)
|
||||
faraday (~> 0.13)
|
||||
net-http-persistent (~> 3.0)
|
||||
therubyracer (0.12.0)
|
||||
libv8 (~> 3.16.14.0)
|
||||
ref
|
||||
@@ -808,7 +817,7 @@ DEPENDENCIES
|
||||
jquery-migrate-rails
|
||||
jquery-rails (= 3.0.0)
|
||||
json_spec (~> 1.1.4)
|
||||
jwt (~> 1.5)
|
||||
jwt (~> 2.1)
|
||||
knapsack
|
||||
letter_opener (>= 1.4.1)
|
||||
listen (= 3.0.8)
|
||||
@@ -817,6 +826,7 @@ DEPENDENCIES
|
||||
oauth2 (~> 1.4.1)
|
||||
ofn-qz!
|
||||
oj
|
||||
order_management!
|
||||
paper_trail (~> 5.2.3)
|
||||
paperclip (~> 3.4.1)
|
||||
pg
|
||||
@@ -848,7 +858,7 @@ DEPENDENCIES
|
||||
spree_paypal_express!
|
||||
spring (= 1.7.2)
|
||||
spring-commands-rspec
|
||||
stripe (~> 3.3.2)
|
||||
stripe (~> 4.5.0)
|
||||
therubyracer (= 0.12.0)
|
||||
timecop
|
||||
truncate_html
|
||||
|
||||
@@ -6,7 +6,11 @@ angular.module("admin.subscriptions").controller "SubscriptionController", ($sco
|
||||
$scope.schedules = Schedules.all
|
||||
$scope.paymentMethods = PaymentMethods.all
|
||||
$scope.shippingMethods = ShippingMethods.all
|
||||
$scope.distributor_id = $scope.subscription.shop_id # variant selector requires distributor_id
|
||||
|
||||
# Variant selector requires these
|
||||
$scope.distributor_id = $scope.subscription.shop_id
|
||||
$scope.eligible_for_subscriptions = true
|
||||
|
||||
$scope.view = if $scope.subscription.id? then 'review' else 'details'
|
||||
$scope.nextCallbacks = {}
|
||||
$scope.backCallbacks = {}
|
||||
|
||||
@@ -22,6 +22,7 @@ angular.module("admin.utils").directive "variantAutocomplete", ($timeout) ->
|
||||
q: term
|
||||
distributor_id: scope.distributor_id
|
||||
order_cycle_id: scope.order_cycle_id
|
||||
eligible_for_subscriptions: scope.eligible_for_subscriptions
|
||||
results: (data, page) ->
|
||||
window.variants = data # this is how spree auto complete JS code picks up variants
|
||||
results: data
|
||||
|
||||
@@ -4,7 +4,7 @@ Darkswarm.filter "date_in_words", ->
|
||||
|
||||
Darkswarm.filter "sensible_timeframe", (date_in_wordsFilter)->
|
||||
(date) ->
|
||||
if moment().add('days', 2) < moment(date)
|
||||
if moment().add(2, 'days') < moment(date)
|
||||
t 'orders_open'
|
||||
else
|
||||
t('closing') + date_in_wordsFilter(date)
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
@import '../variables';
|
||||
|
||||
.admin-subscription-form-subscription-line-items {
|
||||
.not-in-open-and-upcoming-order-cycles-warning {
|
||||
color: $warning-red;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
@import '../variables';
|
||||
|
||||
.admin-subscription-review-subscription-line-items {
|
||||
.not-in-open-and-upcoming-order-cycles-warning {
|
||||
color: $warning-red;
|
||||
}
|
||||
}
|
||||
@@ -13,7 +13,8 @@ module Admin
|
||||
def build
|
||||
@subscription_line_item.assign_attributes(params[:subscription_line_item])
|
||||
@subscription_line_item.price_estimate = price_estimate
|
||||
render json: @subscription_line_item, serializer: Api::Admin::SubscriptionLineItemSerializer
|
||||
render json: @subscription_line_item, serializer: Api::Admin::SubscriptionLineItemSerializer,
|
||||
shop: @shop, schedule: @schedule
|
||||
end
|
||||
|
||||
private
|
||||
@@ -26,7 +27,7 @@ module Admin
|
||||
@shop = Enterprise.managed_by(spree_current_user).find_by_id(params[:shop_id])
|
||||
@schedule = permissions.editable_schedules.find_by_id(params[:schedule_id])
|
||||
@order_cycle = @schedule.andand.current_or_next_order_cycle
|
||||
@variant = Spree::Variant.stockable_by(@shop).find_by_id(params[:subscription_line_item][:variant_id])
|
||||
@variant = variant_if_eligible(params[:subscription_line_item][:variant_id]) if @shop.present?
|
||||
end
|
||||
|
||||
def new_actions
|
||||
@@ -50,5 +51,9 @@ module Admin
|
||||
OpenFoodNetwork::ScopeVariantToHub.new(@shop).scope(@variant)
|
||||
@variant.price + fee_calculator.indexed_fees_for(@variant)
|
||||
end
|
||||
|
||||
def variant_if_eligible(variant_id)
|
||||
SubscriptionVariantsService.eligible_variants(@shop).find_by_id(variant_id)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -2,6 +2,7 @@ module Spree
|
||||
module Admin
|
||||
class InvoicesController < Spree::Admin::BaseController
|
||||
respond_to :json
|
||||
authorize_resource class: false
|
||||
|
||||
def create
|
||||
invoice_service = BulkInvoiceService.new
|
||||
|
||||
@@ -0,0 +1,66 @@
|
||||
module Spree
|
||||
module Admin
|
||||
module Reports
|
||||
class EnterpriseFeeSummariesController < 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, @report_parameters)
|
||||
renderer.render(self)
|
||||
rescue ::Reports::Authorizer::ParameterNotAllowedError => e
|
||||
flash[:error] = e.message
|
||||
render_report_form
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def respond_to_invalid_parameters
|
||||
flash[:error] = I18n.t("invalid_filter_parameters", scope: i18n_scope)
|
||||
render_report_form
|
||||
end
|
||||
|
||||
def i18n_scope
|
||||
"order_management.reports.enterprise_fee_summary"
|
||||
end
|
||||
|
||||
def render_report_form
|
||||
render action: :new
|
||||
end
|
||||
|
||||
def report_klass
|
||||
OrderManagement::Reports::EnterpriseFeeSummary
|
||||
end
|
||||
|
||||
def load_report_parameters
|
||||
@report_parameters = report_klass::Parameters.new(params[:report] || {})
|
||||
end
|
||||
|
||||
def load_permissions
|
||||
@permissions = report_klass::Permissions.new(spree_current_user)
|
||||
end
|
||||
|
||||
def report_renderer_klass
|
||||
case params[:report_format]
|
||||
when "csv"
|
||||
report_klass::Renderers::CsvRenderer
|
||||
when nil, "", "html"
|
||||
report_klass::Renderers::HtmlRenderer
|
||||
else
|
||||
raise Reports::UnsupportedReportFormatException
|
||||
end
|
||||
end
|
||||
|
||||
def renderer
|
||||
@renderer ||= report_renderer_klass.new(@report)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -1,4 +1,6 @@
|
||||
require 'csv'
|
||||
|
||||
require 'open_food_network/reports/list'
|
||||
require 'open_food_network/order_and_distributor_report'
|
||||
require 'open_food_network/products_and_inventory_report'
|
||||
require 'open_food_network/lettuce_share_report'
|
||||
@@ -24,35 +26,7 @@ Spree::Admin::ReportsController.class_eval do
|
||||
before_filter :load_data, only: [:customers, :products_and_inventory, :order_cycle_management, :packing]
|
||||
|
||||
def report_types
|
||||
{
|
||||
orders_and_fulfillment: [
|
||||
[I18n.t('admin.reports.supplier_totals'), :order_cycle_supplier_totals],
|
||||
[I18n.t('admin.reports.supplier_totals_by_distributor'), :order_cycle_supplier_totals_by_distributor],
|
||||
[I18n.t('admin.reports.totals_by_supplier'), :order_cycle_distributor_totals_by_supplier],
|
||||
[I18n.t('admin.reports.customer_totals'), :order_cycle_customer_totals]
|
||||
],
|
||||
products_and_inventory: [
|
||||
[I18n.t('admin.reports.all_products'), :all_products],
|
||||
[I18n.t('admin.reports.inventory'), :inventory],
|
||||
[I18n.t('admin.reports.lettuce_share'), :lettuce_share]
|
||||
],
|
||||
customers: [
|
||||
[I18n.t('admin.reports.mailing_list'), :mailing_list],
|
||||
[I18n.t('admin.reports.addresses'), :addresses]
|
||||
],
|
||||
order_cycle_management: [
|
||||
[I18n.t('admin.reports.payment_methods'), :payment_methods],
|
||||
[I18n.t('admin.reports.delivery'), :delivery]
|
||||
],
|
||||
sales_tax: [
|
||||
[I18n.t('admin.reports.tax_types'), :tax_types],
|
||||
[I18n.t('admin.reports.tax_rates'), :tax_rates]
|
||||
],
|
||||
packing: [
|
||||
[I18n.t('admin.reports.pack_by_customer'), :pack_by_customer],
|
||||
[I18n.t('admin.reports.pack_by_supplier'), :pack_by_supplier]
|
||||
]
|
||||
}
|
||||
OpenFoodNetwork::Reports::List.all
|
||||
end
|
||||
|
||||
# Override spree reports list.
|
||||
@@ -275,6 +249,7 @@ Spree::Admin::ReportsController.class_eval do
|
||||
:products_and_inventory,
|
||||
:sales_total,
|
||||
:users_and_enterprises,
|
||||
:enterprise_fee_summary,
|
||||
:order_cycle_management,
|
||||
:sales_tax,
|
||||
:xero_invoices,
|
||||
@@ -295,7 +270,13 @@ Spree::Admin::ReportsController.class_eval do
|
||||
locals: { report_types: report_types[report] }
|
||||
).html_safe
|
||||
end
|
||||
{ name: name, description: description }
|
||||
{ name: name, url: url_for_report(report), description: description }
|
||||
end
|
||||
|
||||
def url_for_report(report)
|
||||
public_send("#{report}_admin_reports_url".to_sym)
|
||||
rescue NoMethodError
|
||||
url_for([:new, :admin, :reports, report.to_s.singularize])
|
||||
end
|
||||
|
||||
def timestamp
|
||||
|
||||
@@ -14,6 +14,13 @@ class FeatureFlags
|
||||
superadmin?
|
||||
end
|
||||
|
||||
# Checks whether the "Enterprise Fee Summary" is enabled for the specified user
|
||||
#
|
||||
# @return [Boolean]
|
||||
def enterprise_fee_summary_enabled?
|
||||
superadmin?
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
attr_reader :user
|
||||
|
||||
@@ -193,7 +193,7 @@ module ProductImport
|
||||
products.flat_map(&:variants).each do |existing_variant|
|
||||
unit_scale = existing_variant.product.variant_unit_scale
|
||||
unscaled_units = entry.unscaled_units || 0
|
||||
entry.unit_value = unscaled_units * unit_scale
|
||||
entry.unit_value = unscaled_units * unit_scale unless unit_scale.nil?
|
||||
|
||||
if entry_matches_existing_variant?(entry, existing_variant)
|
||||
variant_override = create_inventory_item(entry, existing_variant)
|
||||
|
||||
@@ -182,7 +182,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, :orders_and_fulfillment, :products_and_inventory, :order_cycle_management, :packing], :report
|
||||
can [:admin, :index, :customers, :orders_and_distributors, :group_buys, :bulk_coop, :payments,
|
||||
:orders_and_fulfillment, :products_and_inventory, :order_cycle_management, :packing],
|
||||
:report
|
||||
add_enterprise_fee_summary_abilities(user)
|
||||
end
|
||||
|
||||
def add_order_cycle_management_abilities(user)
|
||||
@@ -208,9 +211,10 @@ class AbilityDecorator
|
||||
# during the order creation process from the admin backend
|
||||
order.distributor.nil? || user.enterprises.include?(order.distributor) || order.order_cycle.andand.coordinated_by?(user)
|
||||
end
|
||||
can [:admin, :bulk_management, :managed, :bulk_invoice], Spree::Order do
|
||||
can [:admin, :bulk_management, :managed], Spree::Order do
|
||||
user.admin? || user.enterprises.any?(&:is_distributor)
|
||||
end
|
||||
can [:admin, :create, :show, :poll], :invoice
|
||||
can [:admin, :visible], Enterprise
|
||||
can [:admin, :index, :create, :update, :destroy], :line_item
|
||||
can [:admin, :index, :create], Spree::LineItem
|
||||
@@ -254,7 +258,10 @@ class AbilityDecorator
|
||||
end
|
||||
|
||||
# Reports page
|
||||
can [:admin, :index, :customers, :group_buys, :bulk_coop, :sales_tax, :payments, :orders_and_distributors, :orders_and_fulfillment, :products_and_inventory, :order_cycle_management, :xero_invoices], :report
|
||||
can [:admin, :index, :customers, :group_buys, :bulk_coop, :sales_tax, :payments,
|
||||
:orders_and_distributors, :orders_and_fulfillment, :products_and_inventory,
|
||||
:order_cycle_management, :xero_invoices], :report
|
||||
add_enterprise_fee_summary_abilities(user)
|
||||
|
||||
can [:create], Customer
|
||||
can [:admin, :index, :update, :destroy, :show], Customer, enterprise_id: Enterprise.managed_by(user).pluck(:id)
|
||||
@@ -277,6 +284,16 @@ class AbilityDecorator
|
||||
user.enterprises.include? enterprise_relationship.parent
|
||||
end
|
||||
end
|
||||
|
||||
def add_enterprise_fee_summary_abilities(user)
|
||||
feature_enabled = FeatureFlags.new(user).enterprise_fee_summary_enabled?
|
||||
return unless feature_enabled
|
||||
|
||||
# Reveal the report link in spree/admin/reports#index
|
||||
can [:enterprise_fee_summary], :report
|
||||
# Allow direct access to the report resource
|
||||
can [:admin, :new, :create], :enterprise_fee_summary
|
||||
end
|
||||
end
|
||||
|
||||
Spree::Ability.register_ability(AbilityDecorator)
|
||||
|
||||
@@ -25,6 +25,11 @@ Spree::PaymentMethod.class_eval do
|
||||
end
|
||||
}
|
||||
|
||||
scope :for_distributors, ->(distributors) {
|
||||
non_unique_matches = unscoped.joins(:distributors).where(enterprises: { id: distributors })
|
||||
where(id: non_unique_matches.map(&:id))
|
||||
}
|
||||
|
||||
scope :for_distributor, lambda { |distributor|
|
||||
joins(:distributors).
|
||||
where('enterprises.id = ?', distributor)
|
||||
|
||||
@@ -20,6 +20,10 @@ Spree::ShippingMethod.class_eval do
|
||||
end
|
||||
}
|
||||
|
||||
scope :for_distributors, ->(distributors) {
|
||||
non_unique_matches = unscoped.joins(:distributors).where(enterprises: { id: distributors })
|
||||
where(id: non_unique_matches.map(&:id))
|
||||
}
|
||||
scope :for_distributor, lambda { |distributor|
|
||||
joins(:distributors).
|
||||
where('enterprises.id = ?', distributor)
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
module Api
|
||||
module Admin
|
||||
class SubscriptionLineItemSerializer < ActiveModel::Serializer
|
||||
attributes :id, :variant_id, :quantity, :description, :price_estimate
|
||||
attributes :id, :variant_id, :quantity, :description, :price_estimate,
|
||||
:in_open_and_upcoming_order_cycles
|
||||
|
||||
def description
|
||||
"#{object.variant.product.name} - #{object.variant.full_name}"
|
||||
@@ -10,6 +11,22 @@ module Api
|
||||
def price_estimate
|
||||
object.price_estimate.andand.to_f || "?"
|
||||
end
|
||||
|
||||
def in_open_and_upcoming_order_cycles
|
||||
SubscriptionVariantsService.in_open_and_upcoming_order_cycles?(option_or_assigned_shop,
|
||||
option_or_assigned_schedule,
|
||||
object.variant)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def option_or_assigned_shop
|
||||
@options[:shop] || object.subscription.andand.shop
|
||||
end
|
||||
|
||||
def option_or_assigned_schedule
|
||||
@options[:schedule] || object.subscription.andand.schedule
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -97,15 +97,12 @@ class SubscriptionValidator
|
||||
errors.add(:subscription_line_items, :not_available, name: name)
|
||||
end
|
||||
|
||||
# TODO: Extract this into a separate class
|
||||
def available_variant_ids
|
||||
@available_variant_ids ||=
|
||||
Spree::Variant.joins(exchanges: { order_cycle: :schedules })
|
||||
.where(id: subscription_line_items.map(&:variant_id))
|
||||
.where(schedules: { id: schedule }, exchanges: { incoming: false, receiver_id: shop })
|
||||
.merge(OrderCycle.not_closed)
|
||||
.select('DISTINCT spree_variants.id')
|
||||
.pluck(:id)
|
||||
return @available_variant_ids if @available_variant_ids.present?
|
||||
|
||||
subscription_variant_ids = subscription_line_items.map(&:variant_id)
|
||||
@available_variant_ids = SubscriptionVariantsService.eligible_variants(shop)
|
||||
.where(id: subscription_variant_ids).pluck(:id)
|
||||
end
|
||||
|
||||
def build_msg_from(k, msg)
|
||||
|
||||
39
app/services/subscription_variants_service.rb
Normal file
39
app/services/subscription_variants_service.rb
Normal file
@@ -0,0 +1,39 @@
|
||||
class SubscriptionVariantsService
|
||||
# Includes the following variants:
|
||||
# - Variants of permitted producers
|
||||
# - Variants of hub
|
||||
# - Variants that are in outgoing exchanges where the hub is receiver
|
||||
def self.eligible_variants(distributor)
|
||||
variant_conditions = ["spree_products.supplier_id IN (?)", permitted_producer_ids(distributor)]
|
||||
exchange_variant_ids = outgoing_exchange_variant_ids(distributor)
|
||||
if exchange_variant_ids.present?
|
||||
variant_conditions[0] << " OR spree_variants.id IN (?)"
|
||||
variant_conditions << exchange_variant_ids
|
||||
end
|
||||
|
||||
Spree::Variant.joins(:product).where(is_master: false).where(*variant_conditions)
|
||||
end
|
||||
|
||||
def self.in_open_and_upcoming_order_cycles?(distributor, schedule, variant)
|
||||
scope = ExchangeVariant.joins(exchange: { order_cycle: :schedules })
|
||||
.where(variant_id: variant, exchanges: { incoming: false, receiver_id: distributor })
|
||||
.merge(OrderCycle.not_closed)
|
||||
scope = scope.where(schedules: { id: schedule })
|
||||
scope.any?
|
||||
end
|
||||
|
||||
def self.permitted_producer_ids(distributor)
|
||||
other_permitted_producer_ids = EnterpriseRelationship.joins(:parent)
|
||||
.permitting(distributor).with_permission(:add_to_order_cycle)
|
||||
.merge(Enterprise.is_primary_producer)
|
||||
.pluck(:parent_id)
|
||||
|
||||
other_permitted_producer_ids | [distributor.id]
|
||||
end
|
||||
|
||||
def self.outgoing_exchange_variant_ids(distributor)
|
||||
ExchangeVariant.select("DISTINCT exchange_variants.variant_id").joins(:exchange)
|
||||
.where(exchanges: { incoming: false, receiver_id: distributor.id })
|
||||
.pluck(:variant_id)
|
||||
end
|
||||
end
|
||||
63
app/validators/date_time_string_validator.rb
Normal file
63
app/validators/date_time_string_validator.rb
Normal file
@@ -0,0 +1,63 @@
|
||||
# Validates a datetime string with relaxed rules
|
||||
#
|
||||
# This uses ActiveSupport::TimeZone.parse behind the scenes.
|
||||
#
|
||||
# https://api.rubyonrails.org/classes/ActiveSupport/TimeZone.html#method-i-parse
|
||||
#
|
||||
# === Example
|
||||
#
|
||||
# class Post
|
||||
# include ActiveModel::Validations
|
||||
#
|
||||
# attr_accessor :published_at
|
||||
# validates :published_at, date_time_string: true
|
||||
# end
|
||||
#
|
||||
# post = Post.new
|
||||
#
|
||||
# post.published_at = nil
|
||||
# post.valid? # => true
|
||||
#
|
||||
# post.published_at = ""
|
||||
# post.valid? # => true
|
||||
#
|
||||
# post.published_at = []
|
||||
# post.valid? # => false
|
||||
# post.errors[:published_at] # => ["must be a string"]
|
||||
#
|
||||
# post.published_at = 1
|
||||
# post.valid? # => false
|
||||
# post.errors[:published_at] # => ["must be a string"]
|
||||
#
|
||||
# post.published_at = "2018-09-20 01:02:00 +10:00"
|
||||
# post.valid? # => true
|
||||
#
|
||||
# post.published_at = "Not Valid"
|
||||
# post.valid? # => false
|
||||
# post.errors[:published_at] # => ["must be valid"]
|
||||
class DateTimeStringValidator < ActiveModel::EachValidator
|
||||
NOT_STRING_ERROR = I18n.t("validators.date_time_string_validator.not_string_error")
|
||||
INVALID_FORMAT_ERROR = I18n.t("validators.date_time_string_validator.invalid_format_error")
|
||||
|
||||
def validate_each(record, attribute, value)
|
||||
return if value.nil? || value == ""
|
||||
|
||||
validate_attribute_is_string(record, attribute, value)
|
||||
validate_attribute_is_datetime_string(record, attribute, value)
|
||||
end
|
||||
|
||||
protected
|
||||
|
||||
def validate_attribute_is_string(record, attribute, value)
|
||||
return if value.is_a?(String)
|
||||
|
||||
record.errors.add(attribute, NOT_STRING_ERROR)
|
||||
end
|
||||
|
||||
def validate_attribute_is_datetime_string(record, attribute, value)
|
||||
return unless value.is_a?(String)
|
||||
|
||||
datetime = Time.zone.parse(value)
|
||||
record.errors.add(attribute, INVALID_FORMAT_ERROR) if datetime.blank?
|
||||
end
|
||||
end
|
||||
63
app/validators/integer_array_validator.rb
Normal file
63
app/validators/integer_array_validator.rb
Normal file
@@ -0,0 +1,63 @@
|
||||
# Validates an integer array
|
||||
#
|
||||
# This uses Integer() behind the scenes.
|
||||
#
|
||||
# === Example
|
||||
#
|
||||
# class Post
|
||||
# include ActiveModel::Validations
|
||||
#
|
||||
# attr_accessor :related_post_ids
|
||||
# validates :related_post_ids, integer_array: true
|
||||
# end
|
||||
#
|
||||
# post = Post.new
|
||||
#
|
||||
# post.related_post_ids = nil
|
||||
# post.valid? # => true
|
||||
#
|
||||
# post.related_post_ids = []
|
||||
# post.valid? # => true
|
||||
#
|
||||
# post.related_post_ids = 1
|
||||
# post.valid? # => false
|
||||
# post.errors[:related_post_ids] # => ["must be an array"]
|
||||
#
|
||||
# post.related_post_ids = [1, 2, 3]
|
||||
# post.valid? # => true
|
||||
#
|
||||
# post.related_post_ids = ["1", "2", "3"]
|
||||
# post.valid? # => true
|
||||
#
|
||||
# post.related_post_ids = [1, "2", "Not Integer", 3]
|
||||
# post.valid? # => false
|
||||
# post.errors[:related_post_ids] # => ["must contain only valid integers"]
|
||||
class IntegerArrayValidator < ActiveModel::EachValidator
|
||||
NOT_ARRAY_ERROR = I18n.t("validators.integer_array_validator.not_array_error")
|
||||
INVALID_ELEMENT_ERROR = I18n.t("validators.integer_array_validator.invalid_element_error")
|
||||
|
||||
def validate_each(record, attribute, value)
|
||||
return if value.nil?
|
||||
|
||||
validate_attribute_is_array(record, attribute, value)
|
||||
validate_attribute_elements_are_integer(record, attribute, value)
|
||||
end
|
||||
|
||||
protected
|
||||
|
||||
def validate_attribute_is_array(record, attribute, value)
|
||||
return if value.is_a?(Array)
|
||||
|
||||
record.errors.add(attribute, NOT_ARRAY_ERROR)
|
||||
end
|
||||
|
||||
def validate_attribute_elements_are_integer(record, attribute, array)
|
||||
return unless array.is_a?(Array)
|
||||
|
||||
array.each do |element|
|
||||
Integer(element)
|
||||
end
|
||||
rescue ArgumentError
|
||||
record.errors.add(attribute, INVALID_ELEMENT_ERROR)
|
||||
end
|
||||
end
|
||||
@@ -56,7 +56,7 @@
|
||||
%input#edit-products{ type: "button", value: t(:edit), ng: { click: "setView('products')" } }
|
||||
.row
|
||||
.seven.columns.alpha.omega
|
||||
%table#subscription-line-items
|
||||
%table#subscription-line-items.admin-subscription-review-subscription-line-items
|
||||
%colgroup
|
||||
%col{:style => "width: 62%;"}/
|
||||
%col{:style => "width: 14%;"}/
|
||||
@@ -71,7 +71,10 @@
|
||||
%span= t(:total)
|
||||
%tbody
|
||||
%tr.item{ id: "sli_{{$index}}", ng: { repeat: "item in subscription.subscription_line_items | filter:{ _destroy: '!true' }", class: { even: 'even', odd: 'odd' } } }
|
||||
%td.description {{ item.description }}
|
||||
%td
|
||||
.description {{ item.description }}
|
||||
.not-in-open-and-upcoming-order-cycles-warning{ ng: { if: '!item.in_open_and_upcoming_order_cycles' } }
|
||||
= t(".no_open_or_upcoming_order_cycle")
|
||||
%td.price.align-center {{ item.price_estimate | currency }}
|
||||
%td.quantity {{ item.quantity }}
|
||||
%td.total.align-center {{ (item.price_estimate * item.quantity) | currency }}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
%table#subscription-line-items
|
||||
%table#subscription-line-items.admin-subscription-form-subscription-line-items
|
||||
%colgroup
|
||||
%col{:style => "width: 49%;"}/
|
||||
%col{:style => "width: 14%;"}/
|
||||
@@ -15,7 +15,10 @@
|
||||
%th.orders-actions.actions
|
||||
%tbody
|
||||
%tr.item{ id: "sli_{{$index}}", ng: { repeat: "item in subscription.subscription_line_items | filter:{ _destroy: '!true' }", class: { even: 'even', odd: 'odd' } } }
|
||||
%td.description {{ item.description }}
|
||||
%td
|
||||
.description {{ item.description }}
|
||||
.not-in-open-and-upcoming-order-cycles-warning{ ng: { if: '!item.in_open_and_upcoming_order_cycles' } }
|
||||
= t(".not_in_open_and_upcoming_order_cycles_warning")
|
||||
%td.price.align-center {{ item.price_estimate | currency }}
|
||||
%td.quantity
|
||||
%input{ name: 'quantity', type: 'number', min: 0, ng: { model: 'item.quantity' } }
|
||||
|
||||
@@ -112,7 +112,7 @@
|
||||
|
||||
.margin-bottom-50{ 'ng-hide' => 'RequestMonitor.loading || filteredLineItems.length == 0' }
|
||||
%form{ name: 'bulk_order_form' }
|
||||
%table.index#listing_orders.bulk{ :class => "sixteen columns alpha" }
|
||||
%table.index#listing_orders.bulk{ :class => "sixteen columns alpha", ng: { show: "initialized" } }
|
||||
%thead
|
||||
%tr{ ng: { controller: "ColumnsCtrl" } }
|
||||
%th.bulk
|
||||
@@ -157,7 +157,7 @@
|
||||
= t("admin.orders.bulk_management.ask")
|
||||
%input{ :type => 'checkbox', 'ng-model' => "confirmDelete" }
|
||||
|
||||
%tr.line_item{ 'ng-repeat' => "line_item in filteredLineItems = ( lineItems | filter:quickSearch | selectFilter:supplierFilter:distributorFilter:orderCycleFilter | variantFilter:selectedUnitsProduct:selectedUnitsVariant:sharedResource | orderBy:sorting.predicate:sorting.reverse )", 'ng-class-even' => "'even'", 'ng-class-odd' => "'odd'", :id => "li_{{line_item.id}}" }
|
||||
%tr.line_item{ 'ng-repeat' => "line_item in filteredLineItems = ( lineItems | filter:quickSearch | selectFilter:supplierFilter:distributorFilter:orderCycleFilter | variantFilter:selectedUnitsProduct:selectedUnitsVariant:sharedResource | orderBy:sorting.predicate:sorting.reverse )", 'ng-class-even' => "'even'", 'ng-class-odd' => "'odd'", ng: { attr: { id: "li_{{line_item.id}}" } } }
|
||||
%td.bulk
|
||||
%input{ :type => "checkbox", :name => 'bulk', 'ng-model' => 'line_item.checked', 'ignore-dirty' => true }
|
||||
%td.order_no{ 'ng-show' => 'columns.order_no.visible' } {{ line_item.order.number }}
|
||||
@@ -181,6 +181,6 @@
|
||||
%input.show-dirty{ :type => 'text', :name => 'price', :id => 'price', :ng => { value: 'line_item.price * line_item.quantity | currency:""', readonly: "true", class: '{"update-error": line_item.errors.price}' } }
|
||||
%span.error{ ng: { bind: 'line_item.errors.price' } }
|
||||
%td.actions
|
||||
%a{ href: "/admin/orders/{{line_item.order.number}}/edit", :class => "edit-order icon-edit no-text", 'confirm-link-click' => 'confirmRefresh()' }
|
||||
%a{ ng: { href: "/admin/orders/{{line_item.order.number}}/edit" }, :class => "edit-order icon-edit no-text", 'confirm-link-click' => 'confirmRefresh()' }
|
||||
%td.actions
|
||||
%a{ 'ng-click' => "deleteLineItem(line_item)", :class => "delete-line-item icon-trash no-text" }
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
- if @product_count > 0
|
||||
%div.seven.columns.alpha.list-item
|
||||
%span.six.columns.alpha
|
||||
= "You have #{@product_count} active product#{@product_count > 1 ? "s" : ""}."
|
||||
= t(".active_products", count: @product_count )
|
||||
%span.one.column.omega
|
||||
%span.icon-ok-sign
|
||||
%a.seven.columns.alpha.button.bottom.blue{ href: "#{admin_products_path}" }
|
||||
@@ -21,7 +21,7 @@
|
||||
- else
|
||||
%div.seven.columns.alpha.list-item.red
|
||||
%span.six.columns.alpha
|
||||
= t "spree_admin_enterprises_any_active_products_text"
|
||||
= t(".active_products", count: @product_count )
|
||||
%span.one.column.omega
|
||||
%span.icon-remove-sign
|
||||
%a.seven.columns.alpha.button.bottom.red{ href: "#{new_admin_product_path}" }
|
||||
|
||||
@@ -0,0 +1,50 @@
|
||||
= form_for @report_parameters, as: :report, url: spree.admin_reports_enterprise_fee_summary_path, method: :post do |f|
|
||||
.row.date-range-filter
|
||||
.sixteen.columns.alpha
|
||||
= label_tag nil, t(".date_range")
|
||||
%br
|
||||
|
||||
= f.label :start_at, class: "inline"
|
||||
= f.text_field :start_at, class: "datetimepicker datepicker-from"
|
||||
|
||||
%span.range-divider
|
||||
%i.icon-arrow-right
|
||||
|
||||
= f.text_field :end_at, class: "datetimepicker datepicker-to"
|
||||
= f.label :end_at, class: "inline"
|
||||
|
||||
.row
|
||||
.sixteen.columns.alpha
|
||||
= f.label :distributor_ids
|
||||
= f.collection_select(:distributor_ids, @permissions.allowed_distributors, :id, :name, {}, {class: "select2 fullwidth", multiple: true})
|
||||
|
||||
.row
|
||||
.sixteen.columns.alpha
|
||||
= f.label :producer_ids
|
||||
= f.collection_select(:producer_ids, @permissions.allowed_producers, :id, :name, {}, {class: "select2 fullwidth", multiple: true})
|
||||
|
||||
.row
|
||||
.sixteen.columns.alpha
|
||||
= f.label :order_cycle_ids
|
||||
= f.collection_select(:order_cycle_ids, @permissions.allowed_order_cycles, :id, :name, {}, {class: "select2 fullwidth", multiple: true})
|
||||
|
||||
.row
|
||||
.eight.columns.alpha
|
||||
= f.label :enterprise_fee_ids
|
||||
= f.collection_select(:enterprise_fee_ids, @permissions.allowed_enterprise_fees, :id, :name, {}, {class: "select2 fullwidth", multiple: true})
|
||||
.eight.columns.omega
|
||||
= f.label :shipping_method_ids
|
||||
= f.collection_select(:shipping_method_ids, @permissions.allowed_shipping_methods, :id, :name, {}, {class: "select2 fullwidth", multiple: true})
|
||||
|
||||
.row
|
||||
.eight.columns.alpha
|
||||
.eight.columns.omega
|
||||
= f.label :payment_method_ids
|
||||
= f.collection_select(:payment_method_ids, @permissions.allowed_payment_methods, :id, :name, {}, {class: "select2 fullwidth", multiple: true})
|
||||
|
||||
.row
|
||||
.sixteen.columns.alpha
|
||||
= check_box_tag :report_format, "csv", false, id: "report_format_csv"
|
||||
= label_tag :report_format_csv, t(".report_format_csv")
|
||||
|
||||
= button t(".generate_report")
|
||||
@@ -0,0 +1,19 @@
|
||||
- if @report.present?
|
||||
%table#enterprise_fee_summary_report.report__table
|
||||
%thead
|
||||
%tr
|
||||
- @renderer.header.each do |heading|
|
||||
%th= heading
|
||||
|
||||
%tbody
|
||||
- @renderer.data_rows.each do |row|
|
||||
%tr
|
||||
- row.each do |cell_value|
|
||||
%td= cell_value
|
||||
|
||||
- if @renderer.data_rows.empty?
|
||||
%tr
|
||||
%td{colspan: @renderer.header.length}= t(:none)
|
||||
- else
|
||||
%p.report__message
|
||||
= t(".select_and_search")
|
||||
@@ -0,0 +1,2 @@
|
||||
= render "filters"
|
||||
= render "report"
|
||||
@@ -0,0 +1 @@
|
||||
= render "filters"
|
||||
21
app/views/spree/admin/reports/index.html.erb
Normal file
21
app/views/spree/admin/reports/index.html.erb
Normal file
@@ -0,0 +1,21 @@
|
||||
<% content_for :page_title do %>
|
||||
<%= t(:listing_reports) %>
|
||||
<% end %>
|
||||
|
||||
<table class="index">
|
||||
<thead>
|
||||
<tr data-hook="reports_header">
|
||||
<th><%= t(:name) %></th>
|
||||
<th><%= t(:description) %></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<% @reports.each do |key, value| %>
|
||||
<tr data-hook="reports_row">
|
||||
<td><%= link_to value[:name], value[:url] %></td>
|
||||
<td><%= value[:description] %></td>
|
||||
</tr>
|
||||
<% end %>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
2649
config/locales/ca.yml
Normal file
2649
config/locales/ca.yml
Normal file
File diff suppressed because it is too large
Load Diff
@@ -54,6 +54,16 @@ en:
|
||||
on_demand_but_count_on_hand_set: "must be blank if on demand"
|
||||
limited_stock_but_no_count_on_hand: "must be specified because forcing limited stock"
|
||||
activemodel:
|
||||
attributes:
|
||||
order_management/reports/enterprise_fee_summary/parameters:
|
||||
start_at: "Start"
|
||||
end_at: "End"
|
||||
distributor_ids: "Hubs"
|
||||
producer_ids: "Producers"
|
||||
order_cycle_ids: "Order Cycles"
|
||||
enterprise_fee_ids: "Fees Names"
|
||||
shipping_method_ids: "Shipping Methods"
|
||||
payment_method_ids: "Payment Methods"
|
||||
errors:
|
||||
models:
|
||||
subscription_validator:
|
||||
@@ -100,6 +110,14 @@ en:
|
||||
order_cycle:
|
||||
cloned_order_cycle_name: "COPY OF %{order_cycle}"
|
||||
|
||||
validators:
|
||||
date_time_string_validator:
|
||||
not_string_error: "must be a string"
|
||||
invalid_format_error: "must be valid"
|
||||
integer_array_validator:
|
||||
not_array_error: "must be an array"
|
||||
invalid_element_error: "must contain only valid integers"
|
||||
|
||||
enterprise_mailer:
|
||||
confirmation_instructions:
|
||||
subject: "Please confirm the email address for %{enterprise}"
|
||||
@@ -262,6 +280,7 @@ en:
|
||||
|
||||
actions:
|
||||
create_and_add_another: "Create and Add Another"
|
||||
create: "Create"
|
||||
admin:
|
||||
# Common properties / models
|
||||
begins_at: Begins At
|
||||
@@ -1055,7 +1074,9 @@ en:
|
||||
description: Invoices for import into Xero
|
||||
packing:
|
||||
name: Packing Reports
|
||||
|
||||
enterprise_fee_summary:
|
||||
name: "Enterprise Fee Summary"
|
||||
description: "Summary of Enterprise Fees collected"
|
||||
subscriptions:
|
||||
subscriptions: Subscriptions
|
||||
new: New Subscription
|
||||
@@ -1089,6 +1110,7 @@ en:
|
||||
this_is_an_estimate: |
|
||||
The displayed prices are only an estimate and calculated at the time the subscription is changed.
|
||||
If you change prices or fees, orders will be updated, but the subscription will still display the old values.
|
||||
not_in_open_and_upcoming_order_cycles_warning: "There are no open or upcoming order cycles for this product."
|
||||
details:
|
||||
details: Details
|
||||
invalid_error: Oops! Please fill in all of the required fields...
|
||||
@@ -1103,6 +1125,7 @@ en:
|
||||
details: Details
|
||||
address: Address
|
||||
products: Products
|
||||
no_open_or_upcoming_order_cycle: "No Upcoming Order Cycle"
|
||||
product_already_in_order: This product has already been added to the order. Please edit the quantity directly.
|
||||
orders:
|
||||
number: Number
|
||||
@@ -1348,7 +1371,6 @@ en:
|
||||
ie_warning_firefox: Download Firefox
|
||||
ie_warning_ie: Upgrade Internet Explorer
|
||||
ie_warning_other: "Can't upgrade your browser? Try Open Food Network on your smartphone :-)"
|
||||
|
||||
legal:
|
||||
cookies_policy:
|
||||
header: "How We Use Cookies"
|
||||
@@ -2156,7 +2178,6 @@ See the %{link} to find out more about %{sitename}'s features and to start using
|
||||
spree_admin_enterprises_none_text: "You don't have any enterprises yet"
|
||||
spree_admin_enterprises_tabs_hubs: "HUBS"
|
||||
spree_admin_enterprises_producers_manage_products: "MANAGE PRODUCTS"
|
||||
spree_admin_enterprises_any_active_products_text: "You don't have any active products."
|
||||
spree_admin_enterprises_create_new_product: "CREATE A NEW PRODUCT"
|
||||
spree_admin_single_enterprise_alert_mail_confirmation: "Please confirm the email address for"
|
||||
spree_admin_single_enterprise_alert_mail_sent: "We've sent an email to"
|
||||
@@ -2684,6 +2705,44 @@ See the %{link} to find out more about %{sitename}'s features and to start using
|
||||
signup:
|
||||
start_free_profile: "Start with a free profile, and expand when you're ready!"
|
||||
|
||||
order_management:
|
||||
reports:
|
||||
enterprise_fee_summary:
|
||||
date_end_before_start_error: "must be after start"
|
||||
parameter_not_allowed_error: "You are not authorized to use one or more selected filters for this report."
|
||||
fee_calculated_on_transfer_through_all: "All"
|
||||
fee_type:
|
||||
payment_method: "Payment Transaction"
|
||||
shipping_method: "Shipment"
|
||||
fee_placements:
|
||||
supplier: "Incoming"
|
||||
distributor: "Outgoing"
|
||||
coordinator: "Coordinator"
|
||||
tax_category_name:
|
||||
shipping_instance_rate: "Platform Rate"
|
||||
formats:
|
||||
csv:
|
||||
header:
|
||||
fee_type: "Fee Type"
|
||||
enterprise_name: "Enterprise Owner"
|
||||
fee_name: "Fee Name"
|
||||
customer_name: "Customer"
|
||||
fee_placement: "Fee Placement"
|
||||
fee_calculated_on_transfer_through_name: "Fee Calc on Transfer Through"
|
||||
tax_category_name: "Tax Category"
|
||||
total_amount: "$$ SUM"
|
||||
html:
|
||||
header:
|
||||
fee_type: "Fee Type"
|
||||
enterprise_name: "Enterprise Owner"
|
||||
fee_name: "Fee Name"
|
||||
customer_name: "Customer"
|
||||
fee_placement: "Fee Placement"
|
||||
fee_calculated_on_transfer_through_name: "Fee Calc on Transfer Through"
|
||||
tax_category_name: "Tax Category"
|
||||
total_amount: "$$ SUM"
|
||||
invalid_filter_parameters: "The filters you selected for this report are invalid."
|
||||
|
||||
spree:
|
||||
# TODO: remove `email` key once we get to Spree 2.0
|
||||
email: Email
|
||||
@@ -2752,6 +2811,11 @@ See the %{link} to find out more about %{sitename}'s features and to start using
|
||||
distributor: "Distributor:"
|
||||
order_cycle: "Order cycle:"
|
||||
overview:
|
||||
products:
|
||||
active_products:
|
||||
zero: "You don't have any active products."
|
||||
one: "You have one active product"
|
||||
other: "You have %{count} active products"
|
||||
order_cycles:
|
||||
order_cycles: "Order Cycles"
|
||||
order_cycles_tip: "Order cycles determine when and where your products are available to customers."
|
||||
@@ -2825,6 +2889,14 @@ See the %{link} to find out more about %{sitename}'s features and to start using
|
||||
bulk_coop_allocation: 'Bulk Co-op - Allocation'
|
||||
bulk_coop_packing_sheets: 'Bulk Co-op - Packing Sheets'
|
||||
bulk_coop_customer_payments: 'Bulk Co-op - Customer Payments'
|
||||
enterprise_fee_summaries:
|
||||
filters:
|
||||
date_range: "Date Range"
|
||||
report_format_csv: "Download as CSV"
|
||||
generate_report: "Generate Report"
|
||||
report:
|
||||
none: "None"
|
||||
select_and_search: "Select filters and click on GENERATE REPORT to access your data."
|
||||
users:
|
||||
index:
|
||||
listing_users: "Listing Users"
|
||||
|
||||
@@ -1105,7 +1105,7 @@ fr_BE:
|
||||
footer_legal_call: "Lire nos"
|
||||
footer_legal_tos: "Termes et conditions"
|
||||
footer_legal_visit: "Nous trouver sur"
|
||||
footer_legal_text_html: "Open Food Network est une plateforme logicielle open source, libre et gratuite. Nos données sont protégées sous licence %{content_license} et notre code sous %{code_license}."
|
||||
footer_legal_text_html: "Open Food Network est une plateforme logicielle open source et libre. Nos données sont protégées sous licence %{content_license} et notre code sous %{code_license}."
|
||||
footer_data_text_with_privacy_policy_html: "Nous prenons soin de vos données. Voir notre %{privacy_policy} et %{cookies_policy}."
|
||||
footer_data_text_without_privacy_policy_html: "Nous prenons soin de vos données. Voir notre %{cookies_policy}."
|
||||
footer_data_privacy_policy: "politique de confidentialité"
|
||||
|
||||
@@ -602,6 +602,7 @@ it:
|
||||
desc_long_placeholder: Racconta di te ai consumatori. Questa informazione comparirà nel tuo profilo pubblico.
|
||||
business_details:
|
||||
abn: ABN
|
||||
abn_placeholder: es. 99 123 456 789
|
||||
acn: ACN
|
||||
acn_placeholder: es. 123 456 789
|
||||
display_invoice_logo: Mostra logo nelle fatture
|
||||
@@ -1131,6 +1132,7 @@ it:
|
||||
total_excl_tax: "Totale (Tasse escl.):"
|
||||
total_incl_tax: "Totale (Tasse incl.):"
|
||||
abn: "CF:"
|
||||
acn: "ACN:"
|
||||
invoice_issued_on: "Fattura emessa il"
|
||||
order_number: "Numero fattura:"
|
||||
date_of_transaction: "Data della transazione:"
|
||||
@@ -1149,7 +1151,9 @@ it:
|
||||
menu_5_title: "About"
|
||||
menu_5_url: "http://www.openfoodnetwork.org/"
|
||||
menu_6_title: "Connetti"
|
||||
menu_6_url: "https://openfoodnetwork.org/au/connect/"
|
||||
menu_7_title: "Impara"
|
||||
menu_7_url: "https://openfoodnetwork.org/au/learn/"
|
||||
logo: "Logo (640x130)"
|
||||
logo_mobile: "Mobile logo (75x26)"
|
||||
logo_mobile_svg: "Mobile logo (SVG)"
|
||||
@@ -2216,6 +2220,9 @@ it:
|
||||
validation_msg_tax: "^Categoria d'imposta richiesta"
|
||||
validation_msg_tax_category_cant_be_blank: "^Categoria di tassa non può essere vuoto"
|
||||
validation_msg_is_associated_with_an_exising_customer: "è associato con un cliente esistente"
|
||||
content_configuration_pricing_table: "(TODO: tabella dei prezzi)"
|
||||
content_configuration_case_studies: "(TODO: Casi studio)"
|
||||
content_configuration_detail: "(TODO: Dettaglio)"
|
||||
enterprise_name_error: "è già stato utilizzato. Se questo è il nome della tua azienda e vorresti reclamarne la proprietà, o se vuoi contattare questa azienda, puoi contattare l'attuale referente di questo profilo a %{email}."
|
||||
enterprise_owner_error: "^%{email}non può gestire altre aziende (il limite è %{enterprise_limit})."
|
||||
enterprise_role_uniqueness_error: "^Questo ruolo è già presente."
|
||||
@@ -2253,6 +2260,7 @@ it:
|
||||
back_to_orders_list: "Indietro alla lista delle gentili richieste"
|
||||
no_orders_found: "Nessuna gentile richiesta trovata"
|
||||
order_information: "Informazioni Gentile Richiesta"
|
||||
date_completed: "Data di completamento"
|
||||
amount: "Quantità"
|
||||
state_names:
|
||||
ready: Pronto
|
||||
@@ -2275,6 +2283,7 @@ it:
|
||||
choose: Scegli
|
||||
resolve_errors: 'Per favore risolvi i seguenti errori:'
|
||||
more_items: "+ %{count} ancora"
|
||||
default_card_updated: Carta predefinita aggiornata
|
||||
admin:
|
||||
enterprise_limit_reached: "Hai raggiunto il limite standard di aziende per account. Scrivi a %{contact_email} se hai bisogno di aumentarlo."
|
||||
modals:
|
||||
@@ -2368,14 +2377,18 @@ it:
|
||||
non_producer_example: es. Botteghe, Food Coop, GAS
|
||||
enterprise_status:
|
||||
status_title: "%{name} è impostato e pronto a partire!"
|
||||
severity: Gravità
|
||||
description: Descrizione
|
||||
resolve: Risolvi
|
||||
new_tag_rule_dialog:
|
||||
select_rule_type: "Seleziona un tipo di regola:"
|
||||
orders:
|
||||
index:
|
||||
per_page: "%{results} per pagina"
|
||||
view_file: Vedi file
|
||||
compiling_invoices: Compilazione fatture
|
||||
bulk_invoice_created: Fattura all'ingrosso creata
|
||||
bulk_invoice_failed: Creazione fattura all'ingrosso fallita
|
||||
please_wait: Si prega di attendere che il PDF sia pronto prima di chiudere questo modale
|
||||
resend_user_email_confirmation:
|
||||
resend: "Invia nuovamente"
|
||||
@@ -2394,6 +2407,7 @@ it:
|
||||
'yes': "A richiesta"
|
||||
variant_overrides:
|
||||
on_demand:
|
||||
use_producer_settings: "Usa le impostazioni dello stock del produttore"
|
||||
'yes': "Sì"
|
||||
'no': "No"
|
||||
inventory_products: "Inventario Prodotti"
|
||||
@@ -2401,6 +2415,9 @@ it:
|
||||
new_products: "Nuovi Prodotti"
|
||||
reset_stock_levels: Resetta le quantità disponibili alla quantità predefinita
|
||||
changes_to: Cambia in
|
||||
one_override: una si è sovrascritta
|
||||
overrides: sovrascrive
|
||||
remain_unsaved: restano da memorizzare
|
||||
no_changes_to_save: Nessuna modifica da salvare.
|
||||
no_authorisation: "Non abbiamo l'autorizzazione per salvare queste modifiche. "
|
||||
some_trouble: "Abbiamo avuto problemi durante il salvataggio: %{errors}"
|
||||
@@ -2449,16 +2466,23 @@ it:
|
||||
admin:
|
||||
orders:
|
||||
index:
|
||||
listing_orders: "Listino Ordini"
|
||||
new_order: "Nuovo ordine"
|
||||
capture: "Cattura"
|
||||
ship: "Spedizione"
|
||||
edit: "Modifica"
|
||||
note: "Nota"
|
||||
first: "Primo"
|
||||
last: "Ultimo"
|
||||
previous: "Precedente"
|
||||
next: "Prossimo"
|
||||
loading: "Caricamento"
|
||||
no_orders_found: "Nessuna gentile richiesta trovata"
|
||||
results_found: "%{number} Risultati trovati."
|
||||
viewing: "Guardando da %{start} a %{end}"
|
||||
print_invoices: "Stampa fatture"
|
||||
invoice:
|
||||
issued_on: Emesso il
|
||||
tax_invoice: FATTURA DELLE TASSE
|
||||
code: Codice
|
||||
from: Da
|
||||
@@ -2490,12 +2514,18 @@ it:
|
||||
stripe_disabled_msg: I pagamenti Stripe sono stati disabilitati dall'amministratore di sistema.
|
||||
request_failed_msg: Spiacenti, qualcosa è andato storto mentre cercavamo di verificare i dettagli dell'account con Stripe...
|
||||
account_missing_msg: Non esiste un account Stripe per questa azienda.
|
||||
connect_one: Connetti One
|
||||
access_revoked_msg: L'accesso a questo account Stripe è stato revocato, per favore ricollega il tuo account.
|
||||
status: Stato
|
||||
connected: Connesso
|
||||
account_id: Account ID
|
||||
business_name: Ragione sociale
|
||||
charges_enabled: Cambi Consentiti
|
||||
payments:
|
||||
source_forms:
|
||||
stripe:
|
||||
error_saving_payment: Errore memorizzando il pagamento
|
||||
submitting_payment: Eseguendo il pagamento
|
||||
products:
|
||||
new:
|
||||
title: 'Nuovo prodotto'
|
||||
@@ -2514,7 +2544,9 @@ it:
|
||||
inherits_properties?: Eredita proprietà?
|
||||
available_on: Disponibile il
|
||||
av_on: "Disp. il"
|
||||
import_date: "Data di importazione"
|
||||
products_variant:
|
||||
variant_has_n_overrides: "Questa variante ha %{n} sostituzione/i"
|
||||
new_variant: "Nuova variante"
|
||||
product_name: Nome Prodotto
|
||||
primary_taxon_form:
|
||||
@@ -2523,14 +2555,19 @@ it:
|
||||
group_buy: "Acquisto di gruppo?"
|
||||
display_as:
|
||||
display_as: Visualizza come
|
||||
reports:
|
||||
table:
|
||||
select_and_search: "Seleziona filtri e clicca su %{option} per accedere al tuo dato"
|
||||
users:
|
||||
index:
|
||||
listing_users: "Elenco Utenti"
|
||||
new_user: "Nuovo utente"
|
||||
user: "Utente"
|
||||
enterprise_limit: "Limite azienda"
|
||||
search: "Cerca"
|
||||
email: "Email"
|
||||
edit:
|
||||
editing_user: "Modificando Utente"
|
||||
back_to_users_list: "Torna all'elenco degli utenti"
|
||||
general_settings: "Impostazioni generali"
|
||||
form:
|
||||
@@ -2548,6 +2585,7 @@ it:
|
||||
edit:
|
||||
legal_settings: "Impostazioni Legali"
|
||||
cookies_consent_banner_toggle: "Mostra banner di consenso per i cookie"
|
||||
privacy_policy_url: "Privacy Policy URL"
|
||||
enterprises_require_tos: "Le aziende devono accettare i Termini di Servizio"
|
||||
cookies_policy_matomo_section: "Visualizza la sezione di Matomo nella pagina della cookie policy"
|
||||
cookies_policy_ga_section: "Visualizza la sezione di Google Analytics nella pagina della cookie policy"
|
||||
@@ -2556,7 +2594,12 @@ it:
|
||||
payment:
|
||||
stripe:
|
||||
choose_one: Scegli uno
|
||||
enter_new_card: Inserire dettagli per una nuova carta
|
||||
used_saved_card: "usare la carta salvata"
|
||||
or_enter_new_card: "Oppure, inserire dettagli di una nuova carta"
|
||||
remember_this_card: Ricordare questa Carta?
|
||||
date_picker:
|
||||
format: '%Y-%m-%d'
|
||||
js_format: 'aa-mm-gg'
|
||||
inventory: Inventario
|
||||
orders:
|
||||
@@ -2567,6 +2610,7 @@ it:
|
||||
order_mailer:
|
||||
invoice_email:
|
||||
hi: "Ciao %{name}"
|
||||
invoice_attached_text: 'Aggiunge una fattura per il tuo recente ordine di '
|
||||
order_state:
|
||||
address: indirizzo
|
||||
adjustments: aggiustamenti
|
||||
@@ -2659,5 +2703,6 @@ it:
|
||||
authorised_shops: Negozi autorizzati
|
||||
authorised_shops:
|
||||
shop_name: "Nome del negozio"
|
||||
allow_charges?: "Consentire ricarichi"
|
||||
localized_number:
|
||||
invalid_format: 'Formato non valido: inserire un numero.'
|
||||
|
||||
@@ -1027,6 +1027,7 @@ nb:
|
||||
this_is_an_estimate: |
|
||||
De viste prisene er bare et estimat og beregnet på det tidspunktet abonnementet endres.
|
||||
Hvis du endrer priser eller avgifter, vil ordrer bli oppdatert, men abonnementet vil fortsatt vise de gamle verdiene.
|
||||
not_in_open_and_upcoming_order_cycles_warning: "Det er ingen åpne eller kommende bestillingsrunder for dette produktet."
|
||||
details:
|
||||
details: Detaljer
|
||||
invalid_error: Oops! Vennligst fyll inn alle obligatoriske felter...
|
||||
@@ -1041,6 +1042,7 @@ nb:
|
||||
details: Detaljer
|
||||
address: Adresse
|
||||
products: Produkter
|
||||
no_open_or_upcoming_order_cycle: "Ingen kommende bestillingsrunde"
|
||||
product_already_in_order: Dette produktet er allerede lagt til i bestillingen. Vennligst rediger mengden direkte.
|
||||
orders:
|
||||
number: Antall
|
||||
|
||||
@@ -118,8 +118,9 @@ Openfoodnetwork::Application.routes.draw do
|
||||
|
||||
get 'sitemap.xml', to: 'sitemap#index', defaults: { format: 'xml' }
|
||||
|
||||
# Mount Web engine routes
|
||||
# Mount engine routes
|
||||
mount Web::Engine, :at => '/'
|
||||
mount OrderManagement::Engine, :at => '/'
|
||||
|
||||
# Mount Spree's routes
|
||||
mount Spree::Core::Engine, :at => '/'
|
||||
|
||||
@@ -31,7 +31,6 @@ Spree::Core::Engine.routes.prepend do
|
||||
|
||||
resources :credit_cards
|
||||
|
||||
|
||||
namespace :api, :defaults => { :format => 'json' } do
|
||||
resources :users do
|
||||
get :authorise_api, on: :collection
|
||||
|
||||
5
engines/order_management/README.md
Normal file
5
engines/order_management/README.md
Normal file
@@ -0,0 +1,5 @@
|
||||
# Order Management
|
||||
|
||||
This is the rails engine for the Order Management domain.
|
||||
|
||||
See our wiki for [more info about domains and engines in OFN](https://github.com/openfoodfoundation/openfoodnetwork/wiki/Tech-Doc:-How-OFN-is-organized-in-Domains-using-Rails-Engines).
|
||||
@@ -0,0 +1 @@
|
||||
//= require_tree .
|
||||
@@ -0,0 +1,5 @@
|
||||
module OrderManagement
|
||||
class ApplicationController < ActionController::Base
|
||||
protect_from_forgery with: :exception
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,39 @@
|
||||
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!
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def authorize_by_distribution!
|
||||
require_ids_allowed(parameters.order_cycle_ids, permissions.allowed_order_cycles)
|
||||
require_ids_allowed(parameters.distributor_ids, permissions.allowed_distributors)
|
||||
require_ids_allowed(parameters.producer_ids, permissions.allowed_producers)
|
||||
end
|
||||
|
||||
def authorize_by_fee!
|
||||
require_ids_allowed(parameters.enterprise_fee_ids, permissions.allowed_enterprise_fees)
|
||||
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
|
||||
end
|
||||
@@ -0,0 +1,87 @@
|
||||
module OrderManagement
|
||||
module Reports
|
||||
module EnterpriseFeeSummary
|
||||
class EnterpriseFeeTypeTotalSummarizer
|
||||
attr_accessor :data
|
||||
|
||||
def initialize(data)
|
||||
@data = data
|
||||
end
|
||||
|
||||
def fee_type
|
||||
if for_payment_method?
|
||||
i18n_translate("fee_type.payment_method")
|
||||
elsif for_shipping_method?
|
||||
i18n_translate("fee_type.shipping_method")
|
||||
else
|
||||
data["fee_type"].try(:capitalize)
|
||||
end
|
||||
end
|
||||
|
||||
def enterprise_name
|
||||
if for_payment_method?
|
||||
data["hub_name"]
|
||||
elsif for_shipping_method?
|
||||
data["hub_name"]
|
||||
else
|
||||
data["enterprise_name"]
|
||||
end
|
||||
end
|
||||
|
||||
def fee_name
|
||||
if for_payment_method?
|
||||
data["payment_method_name"]
|
||||
elsif for_shipping_method?
|
||||
data["shipping_method_name"]
|
||||
else
|
||||
data["fee_name"]
|
||||
end
|
||||
end
|
||||
|
||||
def customer_name
|
||||
data["customer_name"]
|
||||
end
|
||||
|
||||
def fee_placement
|
||||
return if for_payment_method? || for_shipping_method?
|
||||
|
||||
i18n_translate("fee_placements.#{data['placement_enterprise_role']}")
|
||||
end
|
||||
|
||||
def fee_calculated_on_transfer_through_name
|
||||
return if for_payment_method? || for_shipping_method?
|
||||
|
||||
transfer_through_all_string = i18n_translate("fee_calculated_on_transfer_through_all")
|
||||
|
||||
data["incoming_exchange_enterprise_name"] || data["outgoing_exchange_enterprise_name"] ||
|
||||
(transfer_through_all_string if data["placement_enterprise_role"] == "coordinator")
|
||||
end
|
||||
|
||||
def tax_category_name
|
||||
return if for_payment_method?
|
||||
return i18n_translate("tax_category_name.shipping_instance_rate") if for_shipping_method?
|
||||
|
||||
data["tax_category_name"] || data["product_tax_category_name"]
|
||||
end
|
||||
|
||||
def total_amount
|
||||
data["total_amount"]
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def for_payment_method?
|
||||
data["payment_method_name"].present?
|
||||
end
|
||||
|
||||
def for_shipping_method?
|
||||
data["shipping_method_name"].present?
|
||||
end
|
||||
|
||||
def i18n_translate(translation_key)
|
||||
I18n.t("order_management.reports.enterprise_fee_summary.#{translation_key}")
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,68 @@
|
||||
module OrderManagement
|
||||
module Reports
|
||||
module EnterpriseFeeSummary
|
||||
class Parameters < ::Reports::Parameters::Base
|
||||
extend ActiveModel::Naming
|
||||
extend ActiveModel::Translation
|
||||
include ActiveModel::Validations
|
||||
|
||||
attr_accessor :start_at, :end_at, :distributor_ids, :producer_ids, :order_cycle_ids,
|
||||
:enterprise_fee_ids, :shipping_method_ids, :payment_method_ids
|
||||
|
||||
before_validation :cleanup_arrays
|
||||
|
||||
validates :start_at, :end_at, date_time_string: true
|
||||
validates :distributor_ids, :producer_ids, integer_array: true
|
||||
validates :order_cycle_ids, integer_array: true
|
||||
validates :enterprise_fee_ids, integer_array: true
|
||||
validates :shipping_method_ids, :payment_method_ids, integer_array: true
|
||||
|
||||
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 = []
|
||||
self.order_cycle_ids = []
|
||||
self.enterprise_fee_ids = []
|
||||
self.shipping_method_ids = []
|
||||
self.payment_method_ids = []
|
||||
|
||||
super(attributes)
|
||||
end
|
||||
|
||||
def authorize!(permissions)
|
||||
authorizer = Authorizer.new(self, permissions)
|
||||
authorizer.authorize!
|
||||
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
|
||||
|
||||
# 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?)
|
||||
producer_ids.reject!(&:blank?)
|
||||
order_cycle_ids.reject!(&:blank?)
|
||||
enterprise_fee_ids.reject!(&:blank?)
|
||||
shipping_method_ids.reject!(&:blank?)
|
||||
payment_method_ids.reject!(&:blank?)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,45 @@
|
||||
module OrderManagement
|
||||
module Reports
|
||||
module EnterpriseFeeSummary
|
||||
class Permissions < ::Reports::Permissions
|
||||
def allowed_order_cycles
|
||||
@allowed_order_cycles ||= OrderCycle.accessible_by(user)
|
||||
end
|
||||
|
||||
def allowed_distributors
|
||||
outgoing_exchanges = Exchange.outgoing.where(order_cycle_id: allowed_order_cycle_ids)
|
||||
@allowed_distributors ||= Enterprise.where(id: outgoing_exchanges.pluck(:receiver_id))
|
||||
end
|
||||
|
||||
def allowed_producers
|
||||
incoming_exchanges = Exchange.incoming.where(order_cycle_id: allowed_order_cycle_ids)
|
||||
@allowed_producers ||= Enterprise.where(id: incoming_exchanges.pluck(:sender_id))
|
||||
end
|
||||
|
||||
def allowed_enterprise_fees
|
||||
return EnterpriseFee.where("1=0") if allowed_order_cycles.blank?
|
||||
|
||||
coordinator_enterprise_fees = EnterpriseFee.joins(:coordinator_fees)
|
||||
.where(coordinator_fees: { order_cycle_id: allowed_order_cycle_ids })
|
||||
exchange_enterprise_fees = EnterpriseFee.joins(exchange_fees: :exchange)
|
||||
.where(exchanges: { order_cycle_id: allowed_order_cycle_ids })
|
||||
@allowed_enterprise_fees ||= (coordinator_enterprise_fees | exchange_enterprise_fees).uniq
|
||||
end
|
||||
|
||||
def allowed_shipping_methods
|
||||
@allowed_shipping_methods ||= Spree::ShippingMethod.for_distributors(allowed_distributors)
|
||||
end
|
||||
|
||||
def allowed_payment_methods
|
||||
@allowed_payment_methods ||= Spree::PaymentMethod.for_distributors(allowed_distributors)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def allowed_order_cycle_ids
|
||||
@allowed_order_cycle_ids ||= allowed_order_cycles.map(&:id)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,64 @@
|
||||
module OrderManagement
|
||||
module Reports
|
||||
module EnterpriseFeeSummary
|
||||
module Renderers
|
||||
class CsvRenderer < ::Reports::Renderers::Base
|
||||
def render(context)
|
||||
context.send_data(generate, filename: filename)
|
||||
end
|
||||
|
||||
def generate
|
||||
CSV.generate do |csv|
|
||||
render_header(csv)
|
||||
|
||||
report_data.list.each do |data|
|
||||
render_data_row(csv, data)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def filename
|
||||
timestamp = Time.zone.now.strftime("%Y%m%d")
|
||||
"enterprise_fee_summary_#{timestamp}.csv"
|
||||
end
|
||||
|
||||
def render_header(csv)
|
||||
csv << [
|
||||
header_label(:fee_type),
|
||||
header_label(:enterprise_name),
|
||||
header_label(:fee_name),
|
||||
header_label(:customer_name),
|
||||
header_label(:fee_placement),
|
||||
header_label(:fee_calculated_on_transfer_through_name),
|
||||
header_label(:tax_category_name),
|
||||
header_label(:total_amount)
|
||||
]
|
||||
end
|
||||
|
||||
def render_data_row(csv, data)
|
||||
csv << [
|
||||
data.fee_type,
|
||||
data.enterprise_name,
|
||||
data.fee_name,
|
||||
data.customer_name,
|
||||
data.fee_placement,
|
||||
data.fee_calculated_on_transfer_through_name,
|
||||
data.tax_category_name,
|
||||
data.total_amount
|
||||
]
|
||||
end
|
||||
|
||||
def header_label(attribute)
|
||||
I18n.t("header.#{attribute}", scope: i18n_scope)
|
||||
end
|
||||
|
||||
def i18n_scope
|
||||
"order_management.reports.enterprise_fee_summary.formats.csv"
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,51 @@
|
||||
module OrderManagement
|
||||
module Reports
|
||||
module EnterpriseFeeSummary
|
||||
module Renderers
|
||||
class HtmlRenderer < ::Reports::Renderers::Base
|
||||
def render(context)
|
||||
context.instance_variable_set :@renderer, self
|
||||
context.render(action: :create, renderer: self)
|
||||
end
|
||||
|
||||
def header
|
||||
data_row_attributes.map do |attribute|
|
||||
header_label(attribute)
|
||||
end
|
||||
end
|
||||
|
||||
def data_rows
|
||||
report_data.list.map do |data|
|
||||
data_row_attributes.map do |attribute|
|
||||
data.public_send(attribute)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def data_row_attributes
|
||||
[
|
||||
:fee_type,
|
||||
:enterprise_name,
|
||||
:fee_name,
|
||||
:customer_name,
|
||||
:fee_placement,
|
||||
:fee_calculated_on_transfer_through_name,
|
||||
:tax_category_name,
|
||||
:total_amount
|
||||
]
|
||||
end
|
||||
|
||||
def header_label(attribute)
|
||||
I18n.t("header.#{attribute}", scope: i18n_scope)
|
||||
end
|
||||
|
||||
def i18n_scope
|
||||
"order_management.reports.enterprise_fee_summary.formats.csv"
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,29 @@
|
||||
module OrderManagement
|
||||
module Reports
|
||||
module EnterpriseFeeSummary
|
||||
module ReportData
|
||||
class EnterpriseFeeTypeTotal < ::Reports::ReportData::Base
|
||||
attr_accessor :fee_type, :enterprise_name, :fee_name, :customer_name, :fee_placement,
|
||||
:fee_calculated_on_transfer_through_name, :tax_category_name, :total_amount
|
||||
|
||||
def <=>(other)
|
||||
sortable_data <=> other.sortable_data
|
||||
end
|
||||
|
||||
def sortable_data
|
||||
[
|
||||
fee_type,
|
||||
enterprise_name,
|
||||
fee_name,
|
||||
customer_name,
|
||||
fee_placement,
|
||||
fee_calculated_on_transfer_through_name,
|
||||
tax_category_name,
|
||||
total_amount
|
||||
].map { |attribute| attribute || "" }
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,47 @@
|
||||
module OrderManagement
|
||||
module Reports
|
||||
module EnterpriseFeeSummary
|
||||
class ReportService
|
||||
attr_accessor :permissions, :parameters
|
||||
|
||||
def initialize(permissions, parameters)
|
||||
@permissions = permissions
|
||||
@parameters = parameters
|
||||
end
|
||||
|
||||
def enterprise_fees_by_customer
|
||||
Scope.new.apply_filters(permission_filters).apply_filters(parameters).result
|
||||
end
|
||||
|
||||
def list
|
||||
enterprise_fee_type_total_list.sort
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def permission_filters
|
||||
Parameters.new(order_cycle_ids: permissions.allowed_order_cycles.map(&:id))
|
||||
end
|
||||
|
||||
def enterprise_fee_type_total_list
|
||||
enterprise_fees_by_customer.map do |total_data|
|
||||
summarizer = EnterpriseFeeTypeTotalSummarizer.new(total_data)
|
||||
|
||||
ReportData::EnterpriseFeeTypeTotal.new.tap do |total|
|
||||
enterprise_fee_type_summarizer_to_total_attributes.each do |attribute|
|
||||
total.public_send("#{attribute}=", summarizer.public_send(attribute))
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def enterprise_fee_type_summarizer_to_total_attributes
|
||||
[
|
||||
:fee_type, :enterprise_name, :fee_name, :customer_name, :fee_placement,
|
||||
:fee_calculated_on_transfer_through_name, :tax_category_name, :total_amount
|
||||
]
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,362 @@
|
||||
module OrderManagement
|
||||
module Reports
|
||||
module EnterpriseFeeSummary
|
||||
class Scope
|
||||
attr_accessor :parameters
|
||||
|
||||
def initialize
|
||||
setup_default_scope
|
||||
end
|
||||
|
||||
def apply_filters(params)
|
||||
filter_by_date(params)
|
||||
filter_by_distribution(params)
|
||||
filter_by_fee(params)
|
||||
|
||||
self
|
||||
end
|
||||
|
||||
def result
|
||||
group_data.select_attributes
|
||||
@scope.all
|
||||
end
|
||||
|
||||
protected
|
||||
|
||||
def setup_default_scope
|
||||
find_supported_adjustments
|
||||
|
||||
include_adjustment_metadata
|
||||
include_order_details
|
||||
include_payment_fee_details
|
||||
include_shipping_fee_details
|
||||
include_enterprise_fee_details
|
||||
include_line_item_details
|
||||
include_incoming_exchange_details
|
||||
include_outgoing_exchange_details
|
||||
|
||||
group_data
|
||||
select_attributes
|
||||
end
|
||||
|
||||
def find_supported_adjustments
|
||||
find_adjustments.for_orders.for_supported_adjustments
|
||||
end
|
||||
|
||||
def find_adjustments
|
||||
chain_to_scope do
|
||||
Spree::Adjustment
|
||||
end
|
||||
end
|
||||
|
||||
def for_orders
|
||||
chain_to_scope do
|
||||
where(adjustable_type: "Spree::Order")
|
||||
end
|
||||
end
|
||||
|
||||
def for_supported_adjustments
|
||||
chain_to_scope do
|
||||
where(originator_type: ["EnterpriseFee", "Spree::PaymentMethod",
|
||||
"Spree::ShippingMethod"])
|
||||
end
|
||||
end
|
||||
|
||||
def include_adjustment_metadata
|
||||
join_scope(
|
||||
<<-JOIN_STRING.strip_heredoc
|
||||
LEFT OUTER JOIN adjustment_metadata
|
||||
ON (adjustment_metadata.adjustment_id = spree_adjustments.id)
|
||||
JOIN_STRING
|
||||
)
|
||||
end
|
||||
|
||||
# Includes:
|
||||
# * Order
|
||||
# * Customer
|
||||
# * Hub
|
||||
def include_order_details
|
||||
join_scope(
|
||||
<<-JOIN_STRING.strip_heredoc
|
||||
LEFT OUTER JOIN spree_orders
|
||||
ON (
|
||||
spree_adjustments.adjustable_type = 'Spree::Order'
|
||||
AND spree_orders.id = spree_adjustments.adjustable_id
|
||||
AND spree_orders.completed_at IS NOT NULL
|
||||
)
|
||||
JOIN_STRING
|
||||
)
|
||||
|
||||
join_scope("LEFT OUTER JOIN customers ON (customers.id = spree_orders.customer_id)")
|
||||
|
||||
join_scope(
|
||||
<<-JOIN_STRING.strip_heredoc
|
||||
LEFT OUTER JOIN enterprises AS hubs
|
||||
ON (hubs.id = spree_orders.distributor_id)
|
||||
JOIN_STRING
|
||||
)
|
||||
end
|
||||
|
||||
# If for payment fee
|
||||
#
|
||||
# Includes:
|
||||
# * Payment method
|
||||
def include_payment_fee_details
|
||||
join_scope(
|
||||
<<-JOIN_STRING.strip_heredoc
|
||||
LEFT OUTER JOIN spree_payment_methods
|
||||
ON (
|
||||
spree_adjustments.originator_type = 'Spree::PaymentMethod'
|
||||
AND spree_payment_methods.id = spree_adjustments.originator_id
|
||||
)
|
||||
JOIN_STRING
|
||||
)
|
||||
|
||||
join_scope(
|
||||
<<-JOIN_STRING.strip_heredoc
|
||||
LEFT OUTER JOIN enterprises AS payment_hubs
|
||||
ON (
|
||||
spree_payment_methods.id IS NOT NULL
|
||||
AND payment_hubs.id = spree_orders.distributor_id
|
||||
)
|
||||
JOIN_STRING
|
||||
)
|
||||
end
|
||||
|
||||
# If for shipping fee
|
||||
#
|
||||
# Includes:
|
||||
# * Shipping method
|
||||
def include_shipping_fee_details
|
||||
join_scope(
|
||||
<<-JOIN_STRING.strip_heredoc
|
||||
LEFT OUTER JOIN spree_shipping_methods
|
||||
ON (
|
||||
spree_adjustments.originator_type = 'Spree::ShippingMethod'
|
||||
AND spree_shipping_methods.id = spree_adjustments.originator_id
|
||||
)
|
||||
JOIN_STRING
|
||||
)
|
||||
end
|
||||
|
||||
# Includes:
|
||||
# * Enterprise fee
|
||||
# * Enterprise
|
||||
# * Enterprise fee tax category
|
||||
def include_enterprise_fee_details
|
||||
join_scope(
|
||||
<<-JOIN_STRING.strip_heredoc
|
||||
LEFT OUTER JOIN enterprise_fees
|
||||
ON (
|
||||
spree_adjustments.originator_type = 'EnterpriseFee'
|
||||
AND enterprise_fees.id = spree_adjustments.originator_id
|
||||
)
|
||||
JOIN_STRING
|
||||
)
|
||||
|
||||
join_scope(
|
||||
<<-JOIN_STRING.strip_heredoc
|
||||
LEFT OUTER JOIN enterprises
|
||||
ON (enterprises.id = enterprise_fees.enterprise_id)
|
||||
JOIN_STRING
|
||||
)
|
||||
|
||||
join_scope(
|
||||
<<-JOIN_STRING.strip_heredoc
|
||||
LEFT OUTER JOIN spree_tax_categories
|
||||
ON (spree_tax_categories.id = enterprise_fees.tax_category_id)
|
||||
JOIN_STRING
|
||||
)
|
||||
end
|
||||
|
||||
# If for line item - Use data only if spree_line_items.id is present
|
||||
#
|
||||
# Includes:
|
||||
# * Line item
|
||||
# * Variant
|
||||
# * Product
|
||||
# * Tax category of product, if enterprise fee tells to inherit
|
||||
def include_line_item_details
|
||||
join_scope(
|
||||
<<-JOIN_STRING.strip_heredoc
|
||||
LEFT OUTER JOIN spree_line_items
|
||||
ON (
|
||||
spree_adjustments.source_type = 'Spree::LineItem'
|
||||
AND spree_line_items.id = spree_adjustments.source_id
|
||||
)
|
||||
JOIN_STRING
|
||||
)
|
||||
|
||||
join_scope(
|
||||
<<-JOIN_STRING.strip_heredoc
|
||||
LEFT OUTER JOIN spree_variants
|
||||
ON (
|
||||
spree_adjustments.source_type = 'Spree::LineItem'
|
||||
AND spree_variants.id = spree_line_items.variant_id
|
||||
)
|
||||
JOIN_STRING
|
||||
)
|
||||
|
||||
join_scope(
|
||||
<<-JOIN_STRING.strip_heredoc
|
||||
LEFT OUTER JOIN spree_products
|
||||
ON (spree_products.id = spree_variants.product_id)
|
||||
JOIN_STRING
|
||||
)
|
||||
|
||||
join_scope(
|
||||
<<-JOIN_STRING.strip_heredoc
|
||||
LEFT OUTER JOIN spree_tax_categories AS product_tax_categories
|
||||
ON (
|
||||
enterprise_fees.inherits_tax_category IS TRUE
|
||||
AND product_tax_categories.id = spree_products.tax_category_id
|
||||
)
|
||||
JOIN_STRING
|
||||
)
|
||||
end
|
||||
|
||||
# If incoming exchange - Use data only if incoming_exchange_variants.id is present
|
||||
#
|
||||
# Includes:
|
||||
# * Incoming exchange
|
||||
# * Incoming exchange enterprise
|
||||
# * Incoming exchange variant
|
||||
def include_incoming_exchange_details
|
||||
join_scope(
|
||||
<<-JOIN_STRING.strip_heredoc
|
||||
LEFT OUTER JOIN
|
||||
(
|
||||
exchange_variants AS incoming_exchange_variants
|
||||
LEFT OUTER JOIN exchanges AS incoming_exchanges
|
||||
ON (
|
||||
incoming_exchanges.incoming IS TRUE
|
||||
AND incoming_exchange_variants.exchange_id = incoming_exchanges.id
|
||||
)
|
||||
)
|
||||
ON (
|
||||
spree_adjustments.source_type = 'Spree::LineItem'
|
||||
AND adjustment_metadata.enterprise_role = 'supplier'
|
||||
AND incoming_exchanges.order_cycle_id = spree_orders.order_cycle_id
|
||||
AND incoming_exchange_variants.id IS NOT NULL
|
||||
AND incoming_exchange_variants.variant_id = spree_line_items.variant_id
|
||||
)
|
||||
JOIN_STRING
|
||||
)
|
||||
|
||||
join_scope(
|
||||
<<-JOIN_STRING.strip_heredoc
|
||||
LEFT OUTER JOIN enterprises AS incoming_exchange_enterprises
|
||||
ON (incoming_exchange_enterprises.id = incoming_exchanges.sender_id)
|
||||
JOIN_STRING
|
||||
)
|
||||
end
|
||||
|
||||
# If outgoing exchange - Use data only if outgoing_exchange_variants.id is present
|
||||
#
|
||||
# Includes:
|
||||
# * Outgoing exchange
|
||||
# * Outgoing exchange enterprise
|
||||
# * Outgoing exchange variant
|
||||
def include_outgoing_exchange_details
|
||||
join_scope(
|
||||
<<-JOIN_STRING.strip_heredoc
|
||||
LEFT OUTER JOIN
|
||||
(
|
||||
exchange_variants AS outgoing_exchange_variants
|
||||
LEFT OUTER JOIN exchanges AS outgoing_exchanges
|
||||
ON (
|
||||
outgoing_exchanges.incoming IS NOT TRUE
|
||||
AND outgoing_exchange_variants.exchange_id = outgoing_exchanges.id
|
||||
)
|
||||
)
|
||||
ON (
|
||||
spree_adjustments.source_type = 'Spree::LineItem'
|
||||
AND adjustment_metadata.enterprise_role = 'distributor'
|
||||
AND outgoing_exchanges.order_cycle_id = spree_orders.order_cycle_id
|
||||
AND outgoing_exchange_variants.id IS NOT NULL
|
||||
AND outgoing_exchange_variants.variant_id = spree_line_items.variant_id
|
||||
)
|
||||
JOIN_STRING
|
||||
)
|
||||
|
||||
join_scope(
|
||||
<<-JOIN_STRING.strip_heredoc
|
||||
LEFT OUTER JOIN enterprises AS outgoing_exchange_enterprises
|
||||
ON (outgoing_exchange_enterprises.id = outgoing_exchanges.receiver_id)
|
||||
JOIN_STRING
|
||||
)
|
||||
end
|
||||
|
||||
def filter_by_date(params)
|
||||
filter_scope("spree_orders.completed_at >= ?", params.start_at) \
|
||||
if params.start_at.present?
|
||||
filter_scope("spree_orders.completed_at <= ?", params.end_at) if params.end_at.present?
|
||||
end
|
||||
|
||||
def filter_by_distribution(params)
|
||||
filter_scope(spree_orders: { distributor_id: params.distributor_ids }) \
|
||||
if params.distributor_ids.present?
|
||||
filter_scope(spree_products: { supplier_id: params.producer_ids }) \
|
||||
if params.producer_ids.present?
|
||||
filter_scope(spree_orders: { order_cycle_id: params.order_cycle_ids }) \
|
||||
if params.order_cycle_ids.present?
|
||||
end
|
||||
|
||||
def filter_by_fee(params)
|
||||
filter_scope(enterprise_fees: { id: params.enterprise_fee_ids }) \
|
||||
if params.enterprise_fee_ids.present?
|
||||
filter_scope(spree_shipping_methods: { id: params.shipping_method_ids }) \
|
||||
if params.shipping_method_ids.present?
|
||||
filter_scope(spree_payment_methods: { id: params.payment_method_ids }) \
|
||||
if params.payment_method_ids.present?
|
||||
end
|
||||
|
||||
def group_data
|
||||
chain_to_scope do
|
||||
group("enterprise_fees.id", "enterprises.id", "customers.id", "hubs.id",
|
||||
"spree_payment_methods.id", "spree_shipping_methods.id",
|
||||
"adjustment_metadata.enterprise_role", "spree_tax_categories.id",
|
||||
"product_tax_categories.id", "incoming_exchange_enterprises.id",
|
||||
"outgoing_exchange_enterprises.id")
|
||||
end
|
||||
end
|
||||
|
||||
def select_attributes
|
||||
chain_to_scope do
|
||||
select(
|
||||
<<-JOIN_STRING.strip_heredoc
|
||||
SUM(spree_adjustments.amount) AS total_amount, spree_payment_methods.name AS
|
||||
payment_method_name, spree_shipping_methods.name AS shipping_method_name,
|
||||
hubs.name AS hub_name, enterprises.name AS enterprise_name,
|
||||
enterprise_fees.fee_type AS fee_type, customers.name AS customer_name,
|
||||
customers.email AS customer_email, enterprise_fees.fee_type AS fee_type,
|
||||
enterprise_fees.name AS fee_name, spree_tax_categories.name AS tax_category_name,
|
||||
product_tax_categories.name AS product_tax_category_name,
|
||||
adjustment_metadata.enterprise_role AS placement_enterprise_role,
|
||||
incoming_exchange_enterprises.name AS incoming_exchange_enterprise_name,
|
||||
outgoing_exchange_enterprises.name AS outgoing_exchange_enterprise_name
|
||||
JOIN_STRING
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
def chain_to_scope(&block)
|
||||
@scope = @scope.instance_eval(&block)
|
||||
self
|
||||
end
|
||||
|
||||
def join_scope(join_string)
|
||||
chain_to_scope do
|
||||
joins(join_string)
|
||||
end
|
||||
end
|
||||
|
||||
def filter_scope(*args)
|
||||
chain_to_scope do
|
||||
where(*args)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
3
engines/order_management/app/services/reports.rb
Normal file
3
engines/order_management/app/services/reports.rb
Normal file
@@ -0,0 +1,3 @@
|
||||
module Reports
|
||||
class UnsupportedReportFormatException < StandardError; end
|
||||
end
|
||||
12
engines/order_management/app/services/reports/authorizer.rb
Normal file
12
engines/order_management/app/services/reports/authorizer.rb
Normal file
@@ -0,0 +1,12 @@
|
||||
module Reports
|
||||
class Authorizer
|
||||
class ParameterNotAllowedError < StandardError; end
|
||||
|
||||
attr_accessor :parameters, :permissions
|
||||
|
||||
def initialize(parameters, permissions)
|
||||
@parameters = parameters
|
||||
@permissions = permissions
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,19 @@
|
||||
module Reports
|
||||
module Parameters
|
||||
class Base
|
||||
extend ActiveModel::Naming
|
||||
extend ActiveModel::Translation
|
||||
include ActiveModel::Validations
|
||||
include ActiveModel::Validations::Callbacks
|
||||
|
||||
def initialize(attributes = {})
|
||||
attributes.each do |key, value|
|
||||
public_send("#{key}=", value)
|
||||
end
|
||||
end
|
||||
|
||||
# The parameters are never persisted.
|
||||
def to_key; end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,9 @@
|
||||
module Reports
|
||||
class Permissions
|
||||
attr_accessor :user
|
||||
|
||||
def initialize(user)
|
||||
@user = user
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,11 @@
|
||||
module Reports
|
||||
module Renderers
|
||||
class Base
|
||||
attr_reader :report_data
|
||||
|
||||
def initialize(report_data)
|
||||
@report_data = report_data
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,11 @@
|
||||
module Reports
|
||||
module ReportData
|
||||
class Base
|
||||
def initialize(attributes = {})
|
||||
attributes.each do |key, value|
|
||||
public_send("#{key}=", value)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
7
engines/order_management/config/routes.rb
Normal file
7
engines/order_management/config/routes.rb
Normal file
@@ -0,0 +1,7 @@
|
||||
Spree::Core::Engine.routes.prepend do
|
||||
namespace :admin do
|
||||
namespace :reports do
|
||||
resource :enterprise_fee_summary, only: [:new, :create]
|
||||
end
|
||||
end
|
||||
end
|
||||
4
engines/order_management/lib/order_management.rb
Normal file
4
engines/order_management/lib/order_management.rb
Normal file
@@ -0,0 +1,4 @@
|
||||
require "order_management/engine"
|
||||
|
||||
module OrderManagement
|
||||
end
|
||||
5
engines/order_management/lib/order_management/engine.rb
Normal file
5
engines/order_management/lib/order_management/engine.rb
Normal file
@@ -0,0 +1,5 @@
|
||||
module OrderManagement
|
||||
class Engine < ::Rails::Engine
|
||||
isolate_namespace OrderManagement
|
||||
end
|
||||
end
|
||||
3
engines/order_management/lib/order_management/version.rb
Normal file
3
engines/order_management/lib/order_management/version.rb
Normal file
@@ -0,0 +1,3 @@
|
||||
module OrderManagement
|
||||
VERSION = "0.0.1".freeze
|
||||
end
|
||||
13
engines/order_management/order_management.gemspec
Normal file
13
engines/order_management/order_management.gemspec
Normal file
@@ -0,0 +1,13 @@
|
||||
$LOAD_PATH.push File.expand_path('lib', __dir__)
|
||||
|
||||
require "order_management/version"
|
||||
|
||||
Gem::Specification.new do |s|
|
||||
s.name = "order_management"
|
||||
s.version = OrderManagement::VERSION
|
||||
s.authors = ["developers@ofn"]
|
||||
s.summary = "Order Management domain of the OFN solution."
|
||||
|
||||
s.files = Dir["{app,config,db,lib}/**/*"] + ["LICENSE.txt", "Rakefile", "README.rdoc"]
|
||||
s.test_files = Dir["spec/**/*"]
|
||||
end
|
||||
@@ -0,0 +1,172 @@
|
||||
require "spec_helper"
|
||||
|
||||
describe OrderManagement::Reports::EnterpriseFeeSummary::Authorizer do
|
||||
let(:report_klass) { OrderManagement::Reports::EnterpriseFeeSummary }
|
||||
let(:user) { create(:user) }
|
||||
|
||||
let(:parameters) { report_klass::Parameters.new(params) }
|
||||
let(:permissions) { report_klass::Permissions.new(user) }
|
||||
let(:authorizer) { described_class.new(parameters, permissions) }
|
||||
|
||||
context "for distributors" do
|
||||
before do
|
||||
allow(permissions).to receive(:allowed_distributors) do
|
||||
stub_model_collection(Enterprise, :id, ["1", "2", "3"])
|
||||
end
|
||||
end
|
||||
|
||||
context "when distributors are allowed" do
|
||||
let(:params) { { distributor_ids: ["1", "3"] } }
|
||||
|
||||
it "does not raise error" do
|
||||
expect { authorizer.authorize! }.not_to raise_error
|
||||
end
|
||||
end
|
||||
|
||||
context "when a distributor is not allowed" do
|
||||
let(:params) { { distributor_ids: ["1", "4"] } }
|
||||
|
||||
it "raises ParameterNotAllowedError" do
|
||||
expect { authorizer.authorize! }
|
||||
.to raise_error(Reports::Authorizer::ParameterNotAllowedError)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context "for producers" do
|
||||
before do
|
||||
allow(permissions).to receive(:allowed_producers) do
|
||||
stub_model_collection(Enterprise, :id, ["1", "2", "3"])
|
||||
end
|
||||
end
|
||||
|
||||
context "when producers are allowed" do
|
||||
let(:params) { { producer_ids: ["1", "3"] } }
|
||||
|
||||
it "does not raise error" do
|
||||
expect { authorizer.authorize! }.not_to raise_error
|
||||
end
|
||||
end
|
||||
|
||||
context "when a producer is not allowed" do
|
||||
let(:params) { { producer_ids: ["1", "4"] } }
|
||||
|
||||
it "raises ParameterNotAllowedError" do
|
||||
expect { authorizer.authorize! }
|
||||
.to raise_error(Reports::Authorizer::ParameterNotAllowedError)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context "for order cycles" do
|
||||
before do
|
||||
allow(permissions).to receive(:allowed_order_cycles) do
|
||||
stub_model_collection(OrderCycle, :id, ["1", "2", "3"])
|
||||
end
|
||||
end
|
||||
|
||||
context "when order cycles are allowed" do
|
||||
let(:params) { { order_cycle_ids: ["1", "3"] } }
|
||||
|
||||
it "does not raise error" do
|
||||
expect { authorizer.authorize! }.not_to raise_error
|
||||
end
|
||||
end
|
||||
|
||||
context "when an order cycle is not allowed" do
|
||||
let(:params) { { order_cycle_ids: ["1", "4"] } }
|
||||
|
||||
it "raises ParameterNotAllowedError" do
|
||||
expect { authorizer.authorize! }
|
||||
.to raise_error(Reports::Authorizer::ParameterNotAllowedError)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context "for enterprise fees" do
|
||||
before do
|
||||
allow(permissions).to receive(:allowed_enterprise_fees) do
|
||||
stub_model_collection(EnterpriseFee, :id, ["1", "2", "3"])
|
||||
end
|
||||
end
|
||||
|
||||
context "when enterprise fees are allowed" do
|
||||
let(:params) { { enterprise_fee_ids: ["1", "3"] } }
|
||||
|
||||
it "does not raise error" do
|
||||
expect { authorizer.authorize! }.not_to raise_error
|
||||
end
|
||||
end
|
||||
|
||||
context "when an enterprise fee is not allowed" do
|
||||
let(:params) { { enterprise_fee_ids: ["1", "4"] } }
|
||||
|
||||
it "raises ParameterNotAllowedError" do
|
||||
expect { authorizer.authorize! }
|
||||
.to raise_error(Reports::Authorizer::ParameterNotAllowedError)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context "for shipping methods" do
|
||||
before do
|
||||
allow(permissions).to receive(:allowed_shipping_methods) do
|
||||
stub_model_collection(Spree::ShippingMethod, :id, ["1", "2", "3"])
|
||||
end
|
||||
end
|
||||
|
||||
context "when shipping methods are allowed" do
|
||||
let(:params) { { shipping_method_ids: ["1", "3"] } }
|
||||
|
||||
it "does not raise error" do
|
||||
expect { authorizer.authorize! }.not_to raise_error
|
||||
end
|
||||
end
|
||||
|
||||
context "when a shipping method is not allowed" do
|
||||
let(:params) { { shipping_method_ids: ["1", "4"] } }
|
||||
|
||||
it "raises ParameterNotAllowedError" do
|
||||
expect { authorizer.authorize! }
|
||||
.to raise_error(Reports::Authorizer::ParameterNotAllowedError)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context "for payment methods" do
|
||||
before do
|
||||
allow(permissions).to receive(:allowed_payment_methods) do
|
||||
stub_model_collection(Spree::PaymentMethod, :id, ["1", "2", "3"])
|
||||
end
|
||||
end
|
||||
|
||||
context "when payment methods are allowed" do
|
||||
let(:params) { { payment_method_ids: ["1", "3"] } }
|
||||
|
||||
it "does not raise error" do
|
||||
expect { authorizer.authorize! }.not_to raise_error
|
||||
end
|
||||
end
|
||||
|
||||
context "when a payment method is not allowed" do
|
||||
let(:params) { { payment_method_ids: ["1", "4"] } }
|
||||
|
||||
it "raises ParameterNotAllowedError" do
|
||||
expect { authorizer.authorize! }
|
||||
.to raise_error(Reports::Authorizer::ParameterNotAllowedError)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def stub_model_collection(model, attribute_name, attribute_list)
|
||||
attribute_list.map do |attribute_value|
|
||||
stub_model(model, attribute_name => attribute_value)
|
||||
end
|
||||
end
|
||||
|
||||
def stub_model(model, params)
|
||||
model.new.tap do |instance|
|
||||
instance.stub(params)
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,88 @@
|
||||
require "spec_helper"
|
||||
|
||||
require "date_time_string_validator"
|
||||
|
||||
describe OrderManagement::Reports::EnterpriseFeeSummary::Parameters do
|
||||
describe "validation" do
|
||||
let(:parameters) { described_class.new }
|
||||
|
||||
it "allows all parameters to be blank" do
|
||||
expect(parameters).to be_valid
|
||||
end
|
||||
|
||||
context "for type of parameters" do
|
||||
it { is_expected.to validate_date_time_format_of(:start_at) }
|
||||
it { is_expected.to validate_date_time_format_of(:end_at) }
|
||||
it { is_expected.to validate_integer_array(:distributor_ids) }
|
||||
it { is_expected.to validate_integer_array(:producer_ids) }
|
||||
it { is_expected.to validate_integer_array(:order_cycle_ids) }
|
||||
it { is_expected.to validate_integer_array(:enterprise_fee_ids) }
|
||||
it { is_expected.to validate_integer_array(:shipping_method_ids) }
|
||||
it { is_expected.to validate_integer_array(:payment_method_ids) }
|
||||
|
||||
it "allows integer arrays to include blank string and cleans it up" do
|
||||
subject.distributor_ids = ["", "1"]
|
||||
subject.producer_ids = ["", "1"]
|
||||
subject.order_cycle_ids = ["", "1"]
|
||||
subject.enterprise_fee_ids = ["", "1"]
|
||||
subject.shipping_method_ids = ["", "1"]
|
||||
subject.payment_method_ids = ["", "1"]
|
||||
|
||||
expect(subject).to be_valid
|
||||
|
||||
expect(subject.distributor_ids).to eq(["1"])
|
||||
expect(subject.producer_ids).to eq(["1"])
|
||||
expect(subject.order_cycle_ids).to eq(["1"])
|
||||
expect(subject.enterprise_fee_ids).to eq(["1"])
|
||||
expect(subject.shipping_method_ids).to eq(["1"])
|
||||
expect(subject.payment_method_ids).to eq(["1"])
|
||||
end
|
||||
|
||||
describe "requiring start_at to be before end_at" do
|
||||
let(:now) { Time.zone.now }
|
||||
|
||||
it "adds error when start_at is after end_at" do
|
||||
allow(subject).to receive(:start_at) { now.to_s }
|
||||
allow(subject).to receive(:end_at) { (now - 1.hour).to_s }
|
||||
|
||||
expect(subject).not_to be_valid
|
||||
error_message = described_class.date_end_before_start_error_message
|
||||
expect(subject.errors[:end_at]).to eq([error_message])
|
||||
end
|
||||
|
||||
it "does not add error when start_at is before end_at" do
|
||||
allow(subject).to receive(:start_at) { now.to_s }
|
||||
allow(subject).to receive(:end_at) { (now + 1.hour).to_s }
|
||||
|
||||
expect(subject).to be_valid
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe "smoke authorization" do
|
||||
let!(:order_cycle) { create(:order_cycle) }
|
||||
let!(:user) { create(:user) }
|
||||
|
||||
let(:permissions) do
|
||||
report_klass::Permissions.new(nil).tap do |instance|
|
||||
instance.stub(allowed_order_cycles: [order_cycle])
|
||||
end
|
||||
end
|
||||
|
||||
it "does not raise error when the parameters are allowed" do
|
||||
parameters = described_class.new(order_cycle_ids: [order_cycle.id.to_s])
|
||||
expect { parameters.authorize!(permissions) }.not_to raise_error
|
||||
end
|
||||
|
||||
it "raises error when the parameters are not allowed" do
|
||||
parameters = described_class.new(order_cycle_ids: [(order_cycle.id + 1).to_s])
|
||||
expect { parameters.authorize!(permissions) }
|
||||
.to raise_error(Reports::Authorizer::ParameterNotAllowedError)
|
||||
end
|
||||
end
|
||||
|
||||
def report_klass
|
||||
OrderManagement::Reports::EnterpriseFeeSummary
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,206 @@
|
||||
require "spec_helper"
|
||||
|
||||
describe OrderManagement::Reports::EnterpriseFeeSummary::Permissions do
|
||||
let!(:order_cycle) { create(:simple_order_cycle) }
|
||||
let!(:incoming_exchange) { create(:exchange, incoming: true, order_cycle: order_cycle) }
|
||||
let!(:outgoing_exchange) { create(:exchange, incoming: false, order_cycle: order_cycle) }
|
||||
|
||||
# The factory for order cycle uses the first distributor it finds in the database, if it exists.
|
||||
# However, for this example group, we need to make sure that the coordinator for the second order
|
||||
# cycle is not the same as the one in the first.
|
||||
let!(:another_coordinator) { create(:distributor_enterprise) }
|
||||
|
||||
let!(:another_order_cycle) { create(:simple_order_cycle, coordinator: another_coordinator) }
|
||||
let!(:another_incoming_exchange) do
|
||||
create(:exchange, incoming: true, order_cycle: another_order_cycle)
|
||||
end
|
||||
let!(:another_outgoing_exchange) do
|
||||
create(:exchange, incoming: false, order_cycle: another_order_cycle)
|
||||
end
|
||||
|
||||
describe "permissions for order cycles" do
|
||||
it "allows admin" do
|
||||
user = create(:admin_user)
|
||||
authorizer = described_class.new(user)
|
||||
expect(authorizer.allowed_order_cycles).to include(order_cycle)
|
||||
end
|
||||
|
||||
it "allows coordinator of the order cycle" do
|
||||
user = order_cycle.coordinator.owner
|
||||
authorizer = described_class.new(user)
|
||||
expect(authorizer.allowed_order_cycles).to include(order_cycle)
|
||||
end
|
||||
|
||||
it "allows sender of incoming exchange" do
|
||||
user = incoming_exchange.sender.owner
|
||||
authorizer = described_class.new(user)
|
||||
expect(authorizer.allowed_order_cycles).to include(order_cycle)
|
||||
end
|
||||
|
||||
it "allows receiver of outgoing exchange" do
|
||||
user = outgoing_exchange.receiver.owner
|
||||
authorizer = described_class.new(user)
|
||||
expect(authorizer.allowed_order_cycles).to include(order_cycle)
|
||||
end
|
||||
|
||||
it "does not allow coordinator of another order cycle" do
|
||||
user = another_order_cycle.coordinator.owner
|
||||
authorizer = described_class.new(user)
|
||||
expect(authorizer.allowed_order_cycles).not_to include(order_cycle)
|
||||
end
|
||||
|
||||
it "does not allow sender of incoming exchange of another order cycle" do
|
||||
user = another_incoming_exchange.sender.owner
|
||||
authorizer = described_class.new(user)
|
||||
expect(authorizer.allowed_order_cycles).not_to include(order_cycle)
|
||||
end
|
||||
|
||||
it "does not allow receiver of outgoing exchange of another order cycle" do
|
||||
user = another_outgoing_exchange.receiver.owner
|
||||
authorizer = described_class.new(user)
|
||||
expect(authorizer.allowed_order_cycles).not_to include(order_cycle)
|
||||
end
|
||||
end
|
||||
|
||||
describe "permissions for properties related to the order cycle" do
|
||||
let(:user) { create(:user) }
|
||||
let(:authorizer) do
|
||||
described_class.new(user).tap do |instance|
|
||||
allow(instance).to receive(:allowed_order_cycles) { [order_cycle] }
|
||||
end
|
||||
end
|
||||
|
||||
describe "allowed distributors" do
|
||||
it "includes distributor of allowed order cycle" do
|
||||
expect(authorizer.allowed_distributors).to include(outgoing_exchange.receiver)
|
||||
end
|
||||
|
||||
it "does not include distributor of order cycle that is not allowed" do
|
||||
expect(authorizer.allowed_distributors).not_to include(another_outgoing_exchange.receiver)
|
||||
end
|
||||
end
|
||||
|
||||
describe "allowed producers" do
|
||||
it "includes supplier of allowed order cycle" do
|
||||
expect(authorizer.allowed_producers).to include(incoming_exchange.sender)
|
||||
end
|
||||
|
||||
it "does not include supplier of order cycle that is not allowed" do
|
||||
expect(authorizer.allowed_producers).not_to include(another_incoming_exchange.sender)
|
||||
end
|
||||
end
|
||||
|
||||
describe "allowed enterprise fees" do
|
||||
context "when coordinator fee for order cycle" do
|
||||
let!(:coordinator_fee) do
|
||||
create(:enterprise_fee, enterprise: order_cycle.coordinator).tap do |fee|
|
||||
order_cycle.coordinator_fees << fee
|
||||
end
|
||||
end
|
||||
|
||||
let!(:another_coordinator_fee) do
|
||||
create(:enterprise_fee, enterprise: another_order_cycle.coordinator).tap do |fee|
|
||||
another_order_cycle.coordinator_fees << fee
|
||||
end
|
||||
end
|
||||
|
||||
it "includes enterprise fee in allowed order cycle" do
|
||||
expect(authorizer.allowed_enterprise_fees).to include(coordinator_fee)
|
||||
end
|
||||
|
||||
it "does not include enterprise fee in order cycle that is not allowed" do
|
||||
expect(authorizer.allowed_enterprise_fees).not_to include(another_coordinator_fee)
|
||||
end
|
||||
end
|
||||
|
||||
context "when enterprise fee for incoming exchange" do
|
||||
let!(:exchange_fee) do
|
||||
create(:enterprise_fee, enterprise: incoming_exchange.sender).tap do |fee|
|
||||
incoming_exchange.enterprise_fees << fee
|
||||
end
|
||||
end
|
||||
|
||||
let!(:another_exchange_fee) do
|
||||
create(:enterprise_fee, enterprise: another_incoming_exchange.sender).tap do |fee|
|
||||
another_incoming_exchange.enterprise_fees << fee
|
||||
end
|
||||
end
|
||||
|
||||
it "includes enterprise fee in allowed order cycle" do
|
||||
expect(authorizer.allowed_enterprise_fees).to include(exchange_fee)
|
||||
end
|
||||
|
||||
it "does not include enterprise fee in order cycle that is not allowed" do
|
||||
expect(authorizer.allowed_enterprise_fees).not_to include(another_exchange_fee)
|
||||
end
|
||||
end
|
||||
|
||||
context "when enterprise fee for outgoing exchange" do
|
||||
let!(:exchange_fee) do
|
||||
create(:enterprise_fee, enterprise: outgoing_exchange.receiver).tap do |fee|
|
||||
outgoing_exchange.enterprise_fees << fee
|
||||
end
|
||||
end
|
||||
|
||||
let!(:another_exchange_fee) do
|
||||
create(:enterprise_fee, enterprise: another_outgoing_exchange.receiver).tap do |fee|
|
||||
another_outgoing_exchange.enterprise_fees << fee
|
||||
end
|
||||
end
|
||||
|
||||
it "includes enterprise fee in allowed order cycle" do
|
||||
expect(authorizer.allowed_enterprise_fees).to include(exchange_fee)
|
||||
end
|
||||
|
||||
it "does not include enterprise fee in order cycle that is not allowed" do
|
||||
expect(authorizer.allowed_enterprise_fees).not_to include(another_exchange_fee)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe "allowed shipping methods" do
|
||||
it "includes shipping methods of distributors in allowed order cycle" do
|
||||
shipping_method = create(:shipping_method, distributors: [outgoing_exchange.receiver])
|
||||
expect(authorizer.allowed_shipping_methods).to include(shipping_method)
|
||||
end
|
||||
|
||||
it "does not include shipping methods of suppliers in allowed order cycle" do
|
||||
shipping_method = create(:shipping_method, distributors: [incoming_exchange.sender])
|
||||
expect(authorizer.allowed_shipping_methods).not_to include(shipping_method)
|
||||
end
|
||||
|
||||
it "does not include shipping methods of coordinator of allowed order cycle" do
|
||||
shipping_method = create(:shipping_method, distributors: [order_cycle.coordinator])
|
||||
expect(authorizer.allowed_shipping_methods).not_to include(shipping_method)
|
||||
end
|
||||
|
||||
it "does not include shipping methods of distributors in order cycle that is not allowed" do
|
||||
shipping_method = create(:shipping_method,
|
||||
distributors: [another_outgoing_exchange.receiver])
|
||||
expect(authorizer.allowed_shipping_methods).not_to include(shipping_method)
|
||||
end
|
||||
end
|
||||
|
||||
describe "allowed payment methods" do
|
||||
it "includes payment methods of distributors in allowed order cycle" do
|
||||
payment_method = create(:payment_method, distributors: [outgoing_exchange.receiver])
|
||||
expect(authorizer.allowed_payment_methods).to include(payment_method)
|
||||
end
|
||||
|
||||
it "does not include payment methods of suppliers in allowed order cycle" do
|
||||
payment_method = create(:payment_method, distributors: [incoming_exchange.sender])
|
||||
expect(authorizer.allowed_payment_methods).not_to include(payment_method)
|
||||
end
|
||||
|
||||
it "does not include payment methods of coordinator of allowed order cycle" do
|
||||
payment_method = create(:payment_method, distributors: [order_cycle.coordinator])
|
||||
expect(authorizer.allowed_payment_methods).not_to include(payment_method)
|
||||
end
|
||||
|
||||
it "does not include payment methods of distributors in order cycle that is not allowed" do
|
||||
payment_method = create(:payment_method, distributors: [another_outgoing_exchange.receiver])
|
||||
expect(authorizer.allowed_payment_methods).not_to include(payment_method)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,89 @@
|
||||
require "spec_helper"
|
||||
|
||||
describe OrderManagement::Reports::EnterpriseFeeSummary::Renderers::CsvRenderer do
|
||||
let(:report_klass) { OrderManagement::Reports::EnterpriseFeeSummary }
|
||||
|
||||
let!(:permissions) { report_klass::Permissions.new(current_user) }
|
||||
let!(:parameters) { report_klass::Parameters.new }
|
||||
let!(:service) { report_klass::ReportService.new(permissions, parameters) }
|
||||
let!(:renderer) { described_class.new(service) }
|
||||
|
||||
# Context which will be passed to the renderer. The response object is not automatically prepared,
|
||||
# so this has to be assigned explicitly.
|
||||
let!(:response) { ActionController::TestResponse.new }
|
||||
let!(:controller) do
|
||||
ActionController::Base.new.tap do |controller_mock|
|
||||
controller_mock.instance_variable_set(:@_response, response)
|
||||
end
|
||||
end
|
||||
|
||||
let!(:enterprise_fee_type_totals) do
|
||||
[
|
||||
report_klass::ReportData::EnterpriseFeeTypeTotal.new(
|
||||
fee_type: "Fee Type A",
|
||||
enterprise_name: "Enterprise A",
|
||||
fee_name: "Fee A",
|
||||
customer_name: "Custoemr A",
|
||||
fee_placement: "Fee Placement A",
|
||||
fee_calculated_on_transfer_through_name: "Transfer Enterprise A",
|
||||
tax_category_name: "Tax Category A",
|
||||
total_amount: "1.00"
|
||||
),
|
||||
report_klass::ReportData::EnterpriseFeeTypeTotal.new(
|
||||
fee_type: "Fee Type B",
|
||||
enterprise_name: "Enterprise B",
|
||||
fee_name: "Fee C",
|
||||
customer_name: "Custoemr D",
|
||||
fee_placement: "Fee Placement E",
|
||||
fee_calculated_on_transfer_through_name: "Transfer Enterprise F",
|
||||
tax_category_name: "Tax Category G",
|
||||
total_amount: "2.00"
|
||||
)
|
||||
]
|
||||
end
|
||||
|
||||
let(:current_user) { nil }
|
||||
|
||||
before do
|
||||
allow(service).to receive(:list) { enterprise_fee_type_totals }
|
||||
end
|
||||
|
||||
it "generates CSV header" do
|
||||
renderer.render(controller)
|
||||
result = response.body
|
||||
csv = CSV.parse(result)
|
||||
header_row = csv[0]
|
||||
|
||||
# Test all header cells have values
|
||||
expect(header_row.length).to eq(8)
|
||||
expect(header_row.all?(&:present?)).to be_truthy
|
||||
end
|
||||
|
||||
it "generates CSV data rows" do
|
||||
renderer.render(controller)
|
||||
result = response.body
|
||||
csv = CSV.parse(result, headers: true)
|
||||
|
||||
expect(csv.length).to eq(2)
|
||||
|
||||
# Test random cells
|
||||
expect(csv[0][i18n_translate("header.fee_type")]).to eq("Fee Type A")
|
||||
expect(csv[0][i18n_translate("header.total_amount")]).to eq("1.00")
|
||||
expect(csv[1][i18n_translate("header.total_amount")]).to eq("2.00")
|
||||
end
|
||||
|
||||
it "generates filename correctly" do
|
||||
Timecop.freeze(Time.zone.local(2018, 10, 9, 7, 30, 0)) do
|
||||
filename = renderer.__send__(:filename)
|
||||
expect(filename).to eq("enterprise_fee_summary_20181009.csv")
|
||||
end
|
||||
end
|
||||
|
||||
def i18n_translate(key)
|
||||
I18n.t(key, scope: i18n_scope)
|
||||
end
|
||||
|
||||
def i18n_scope
|
||||
"order_management.reports.enterprise_fee_summary.formats.csv"
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,70 @@
|
||||
require "spec_helper"
|
||||
|
||||
describe OrderManagement::Reports::EnterpriseFeeSummary::Renderers::HtmlRenderer do
|
||||
let(:report_klass) { OrderManagement::Reports::EnterpriseFeeSummary }
|
||||
|
||||
let!(:permissions) { report_klass::Permissions.new(current_user) }
|
||||
let!(:parameters) { report_klass::Parameters.new }
|
||||
let!(:controller) { Spree::Admin::Reports::EnterpriseFeeSummariesController.new }
|
||||
let!(:service) { report_klass::ReportService.new(permissions, parameters) }
|
||||
let!(:renderer) { described_class.new(service) }
|
||||
|
||||
let!(:enterprise_fee_type_totals) do
|
||||
[
|
||||
report_klass::ReportData::EnterpriseFeeTypeTotal.new(
|
||||
fee_type: "Fee Type A",
|
||||
enterprise_name: "Enterprise A",
|
||||
fee_name: "Fee A",
|
||||
customer_name: "Custoemr A",
|
||||
fee_placement: "Fee Placement A",
|
||||
fee_calculated_on_transfer_through_name: "Transfer Enterprise A",
|
||||
tax_category_name: "Tax Category A",
|
||||
total_amount: "1.00"
|
||||
),
|
||||
report_klass::ReportData::EnterpriseFeeTypeTotal.new(
|
||||
fee_type: "Fee Type B",
|
||||
enterprise_name: "Enterprise B",
|
||||
fee_name: "Fee C",
|
||||
customer_name: "Custoemr D",
|
||||
fee_placement: "Fee Placement E",
|
||||
fee_calculated_on_transfer_through_name: "Transfer Enterprise F",
|
||||
tax_category_name: "Tax Category G",
|
||||
total_amount: "2.00"
|
||||
)
|
||||
]
|
||||
end
|
||||
|
||||
let(:current_user) { nil }
|
||||
|
||||
before do
|
||||
allow(service).to receive(:list) { enterprise_fee_type_totals }
|
||||
end
|
||||
|
||||
it "generates header values" do
|
||||
header_row = renderer.header
|
||||
|
||||
# Test all header cells have values
|
||||
expect(header_row.length).to eq(8)
|
||||
expect(header_row.all?(&:present?)).to be_truthy
|
||||
end
|
||||
|
||||
it "generates data rows" do
|
||||
header_row = renderer.header
|
||||
result = renderer.data_rows
|
||||
|
||||
expect(result.length).to eq(2)
|
||||
|
||||
# Test random cells
|
||||
expect(result[0][header_row.index(i18n_translate("header.fee_type"))]).to eq("Fee Type A")
|
||||
expect(result[0][header_row.index(i18n_translate("header.total_amount"))]).to eq("1.00")
|
||||
expect(result[1][header_row.index(i18n_translate("header.total_amount"))]).to eq("2.00")
|
||||
end
|
||||
|
||||
def i18n_translate(key)
|
||||
I18n.t(key, scope: i18n_scope)
|
||||
end
|
||||
|
||||
def i18n_scope
|
||||
"order_management.reports.enterprise_fee_summary.formats.csv"
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,46 @@
|
||||
require "spec_helper"
|
||||
|
||||
describe OrderManagement::Reports::EnterpriseFeeSummary::ReportData::EnterpriseFeeTypeTotal do
|
||||
it "sorts instances according to their attributes" do
|
||||
instance_a = described_class.new(
|
||||
fee_type: "sales",
|
||||
enterprise_name: "Enterprise A",
|
||||
fee_name: "A Sales",
|
||||
customer_name: "Customer A",
|
||||
fee_placement: "Incoming",
|
||||
fee_calculated_on_transfer_through_name: "Transfer Enterprise B",
|
||||
tax_category_name: "Sales 4%",
|
||||
total_amount: "12.00"
|
||||
)
|
||||
|
||||
instance_b = described_class.new(
|
||||
fee_type: "sales",
|
||||
enterprise_name: "Enterprise A",
|
||||
fee_name: "B Sales",
|
||||
customer_name: "Customer A",
|
||||
fee_placement: "Incoming",
|
||||
fee_calculated_on_transfer_through_name: "Transfer Enterprise B",
|
||||
tax_category_name: "Sales 4%",
|
||||
total_amount: "12.00"
|
||||
)
|
||||
|
||||
instance_c = described_class.new(
|
||||
fee_type: "admin",
|
||||
enterprise_name: "Enterprise A",
|
||||
fee_name: "C Admin",
|
||||
customer_name: "Customer B",
|
||||
fee_placement: "Incoming",
|
||||
fee_calculated_on_transfer_through_name: nil,
|
||||
tax_category_name: "Sales 6%",
|
||||
total_amount: "12.00"
|
||||
)
|
||||
|
||||
list = [
|
||||
instance_a,
|
||||
instance_b,
|
||||
instance_c
|
||||
]
|
||||
|
||||
expect(list.sort).to eq([instance_c, instance_a, instance_b])
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,536 @@
|
||||
require "spec_helper"
|
||||
|
||||
describe OrderManagement::Reports::EnterpriseFeeSummary::ReportService do
|
||||
let(:report_klass) { OrderManagement::Reports::EnterpriseFeeSummary }
|
||||
|
||||
# Basic data.
|
||||
let!(:shipping_method) do
|
||||
create(:shipping_method, :per_item, amount: 1, name: "Sample Shipping Method")
|
||||
end
|
||||
|
||||
let!(:payment_method) do
|
||||
create(:payment_method, :per_item, amount: 2, name: "Sample Payment Method")
|
||||
end
|
||||
|
||||
# Create enterprises.
|
||||
let!(:distributor) do
|
||||
create(:distributor_enterprise, name: "Sample Distributor").tap do |enterprise|
|
||||
payment_method.distributors << enterprise
|
||||
shipping_method.distributors << enterprise
|
||||
end
|
||||
end
|
||||
let!(:producer) { create(:supplier_enterprise, name: "Sample Producer") }
|
||||
let!(:coordinator) { create(:enterprise, name: "Sample Coordinator") }
|
||||
|
||||
# Add some fee noise.
|
||||
let!(:other_distributor_fee) { create(:enterprise_fee, :per_item, enterprise: distributor) }
|
||||
let!(:other_producer_fee) { create(:enterprise_fee, :per_item, enterprise: producer) }
|
||||
let!(:other_coordinator_fee) { create(:enterprise_fee, :per_item, enterprise: coordinator) }
|
||||
|
||||
# Set up other requirements for ordering.
|
||||
let!(:order_cycle) { create(:simple_order_cycle, coordinator: coordinator) }
|
||||
let!(:product) { create(:product, tax_category: product_tax_category) }
|
||||
let!(:product_tax_category) { create(:tax_category, name: "Sample Product Tax") }
|
||||
let!(:variant) { prepare_variant }
|
||||
|
||||
# Create customers.
|
||||
let!(:customer) { create(:customer, name: "Sample Customer") }
|
||||
let!(:another_customer) { create(:customer, name: "Another Customer") }
|
||||
|
||||
# Setup up permissions and report.
|
||||
let!(:current_user) { create(:admin_user) }
|
||||
|
||||
let(:permissions) { report_klass::Permissions.new(current_user) }
|
||||
let(:parameters) { report_klass::Parameters.new }
|
||||
let(:service) { described_class.new(permissions, parameters) }
|
||||
|
||||
describe "grouping and sorting of entries" do
|
||||
let!(:order_cycle) do
|
||||
create(:simple_order_cycle, coordinator: coordinator, coordinator_fees: order_cycle_fees)
|
||||
end
|
||||
|
||||
let!(:variant) do
|
||||
prepare_variant(incoming_exchange_fees: variant_incoming_exchange_fees,
|
||||
outgoing_exchange_fees: variant_outgoing_exchange_fees)
|
||||
end
|
||||
|
||||
let!(:order_cycle_fees) do
|
||||
[
|
||||
create(:enterprise_fee, :per_item, name: "Coordinator Fee 1", enterprise: coordinator,
|
||||
fee_type: "admin", amount: 512.0,
|
||||
tax_category: coordinator_tax_category),
|
||||
create(:enterprise_fee, :per_item, name: "Coordinator Fee 2", enterprise: coordinator,
|
||||
fee_type: "sales", amount: 1024.0,
|
||||
inherits_tax_category: true)
|
||||
]
|
||||
end
|
||||
let!(:coordinator_tax_category) { create(:tax_category, name: "Sample Coordinator Tax") }
|
||||
|
||||
let!(:variant_incoming_exchange_fees) do
|
||||
[
|
||||
create(:enterprise_fee, :per_item, name: "Producer Fee 1", enterprise: producer,
|
||||
fee_type: "sales", amount: 64.0,
|
||||
tax_category: producer_tax_category),
|
||||
create(:enterprise_fee, :per_item, name: "Producer Fee 2", enterprise: producer,
|
||||
fee_type: "sales", amount: 128.0,
|
||||
inherits_tax_category: true)
|
||||
]
|
||||
end
|
||||
let!(:producer_tax_category) { create(:tax_category, name: "Sample Producer Tax") }
|
||||
|
||||
let!(:variant_outgoing_exchange_fees) do
|
||||
[
|
||||
create(:enterprise_fee, :per_item, name: "Distributor Fee 1", enterprise: distributor,
|
||||
fee_type: "admin", amount: 4.0,
|
||||
tax_category: distributor_tax_category),
|
||||
create(:enterprise_fee, :per_item, name: "Distributor Fee 2", enterprise: distributor,
|
||||
fee_type: "sales", amount: 8.0,
|
||||
inherits_tax_category: true)
|
||||
]
|
||||
end
|
||||
let!(:distributor_tax_category) { create(:tax_category, name: "Sample Distributor Tax") }
|
||||
|
||||
let!(:customer_order) { prepare_order(customer: customer) }
|
||||
let!(:customer_incomplete_order) { prepare_incomplete_order(customer: customer) }
|
||||
let!(:second_customer_order) { prepare_order(customer: customer) }
|
||||
let!(:other_customer_order) { prepare_order(customer: another_customer) }
|
||||
|
||||
it "groups and sorts entries correctly" do
|
||||
totals = service.list
|
||||
|
||||
expect(totals.length).to eq(16)
|
||||
|
||||
# Data is sorted by the following, in order:
|
||||
# * fee_type
|
||||
# * enterprise_name
|
||||
# * fee_name
|
||||
# * customer_name
|
||||
# * fee_placement
|
||||
# * fee_calculated_on_transfer_through_name
|
||||
# * tax_category_name
|
||||
# * total_amount
|
||||
|
||||
expected_result = [
|
||||
["Admin", "Sample Coordinator", "Coordinator Fee 1", "Another Customer",
|
||||
"Coordinator", "All", "Sample Coordinator Tax", "512.00"],
|
||||
["Admin", "Sample Coordinator", "Coordinator Fee 1", "Sample Customer",
|
||||
"Coordinator", "All", "Sample Coordinator Tax", "1024.00"],
|
||||
["Admin", "Sample Distributor", "Distributor Fee 1", "Another Customer",
|
||||
"Outgoing", "Sample Distributor", "Sample Distributor Tax", "4.00"],
|
||||
["Admin", "Sample Distributor", "Distributor Fee 1", "Sample Customer",
|
||||
"Outgoing", "Sample Distributor", "Sample Distributor Tax", "8.00"],
|
||||
["Payment Transaction", "Sample Distributor", "Sample Payment Method", "Another Customer",
|
||||
nil, nil, nil, "2.00"],
|
||||
["Payment Transaction", "Sample Distributor", "Sample Payment Method", "Sample Customer",
|
||||
nil, nil, nil, "4.00"],
|
||||
["Sales", "Sample Coordinator", "Coordinator Fee 2", "Another Customer",
|
||||
"Coordinator", "All", "Sample Product Tax", "1024.00"],
|
||||
["Sales", "Sample Coordinator", "Coordinator Fee 2", "Sample Customer",
|
||||
"Coordinator", "All", "Sample Product Tax", "2048.00"],
|
||||
["Sales", "Sample Distributor", "Distributor Fee 2", "Another Customer",
|
||||
"Outgoing", "Sample Distributor", "Sample Product Tax", "8.00"],
|
||||
["Sales", "Sample Distributor", "Distributor Fee 2", "Sample Customer",
|
||||
"Outgoing", "Sample Distributor", "Sample Product Tax", "16.00"],
|
||||
["Sales", "Sample Producer", "Producer Fee 1", "Another Customer",
|
||||
"Incoming", "Sample Producer", "Sample Producer Tax", "64.00"],
|
||||
["Sales", "Sample Producer", "Producer Fee 1", "Sample Customer",
|
||||
"Incoming", "Sample Producer", "Sample Producer Tax", "128.00"],
|
||||
["Sales", "Sample Producer", "Producer Fee 2", "Another Customer",
|
||||
"Incoming", "Sample Producer", "Sample Product Tax", "128.00"],
|
||||
["Sales", "Sample Producer", "Producer Fee 2", "Sample Customer",
|
||||
"Incoming", "Sample Producer", "Sample Product Tax", "256.00"],
|
||||
["Shipment", "Sample Distributor", "Sample Shipping Method", "Another Customer",
|
||||
nil, nil, "Platform Rate", "1.00"],
|
||||
["Shipment", "Sample Distributor", "Sample Shipping Method", "Sample Customer",
|
||||
nil, nil, "Platform Rate", "2.00"]
|
||||
]
|
||||
|
||||
expected_result.each_with_index do |expected_attributes, row_index|
|
||||
expect_total_attributes(totals[row_index], expected_attributes)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe "handling of more complex cases" do
|
||||
context "with non-sender fee for incoming exchange and non-receiver fee for outgoing" do
|
||||
let!(:variant) do
|
||||
prepare_variant(incoming_exchange_fees: variant_incoming_exchange_fees,
|
||||
outgoing_exchange_fees: variant_outgoing_exchange_fees)
|
||||
end
|
||||
let!(:variant_incoming_exchange_fees) { [coordinator_fee, distributor_fee] }
|
||||
let!(:variant_outgoing_exchange_fees) { [producer_fee, coordinator_fee] }
|
||||
|
||||
let!(:producer_fee) do
|
||||
tax_category = create(:tax_category, name: "Sample Producer Tax")
|
||||
create(:enterprise_fee, :per_item, name: "Sample Producer Fee", enterprise: producer,
|
||||
fee_type: "sales", amount: 64.0,
|
||||
tax_category: tax_category)
|
||||
end
|
||||
let!(:coordinator_fee) do
|
||||
tax_category = create(:tax_category, name: "Sample Coordinator Tax")
|
||||
create(:enterprise_fee, :per_item, name: "Sample Coordinator Fee", enterprise: coordinator,
|
||||
fee_type: "admin", amount: 512.0,
|
||||
tax_category: tax_category)
|
||||
end
|
||||
let!(:distributor_fee) do
|
||||
tax_category = create(:tax_category, name: "Sample Distributor Tax")
|
||||
create(:enterprise_fee, :per_item, name: "Sample Distributor Fee", enterprise: distributor,
|
||||
fee_type: "admin", amount: 4.0,
|
||||
tax_category: tax_category)
|
||||
end
|
||||
|
||||
let!(:customer_order) { prepare_order(customer: customer) }
|
||||
|
||||
it "fetches data correctly" do
|
||||
totals = service.list
|
||||
|
||||
expect(totals.length).to eq(6)
|
||||
|
||||
expected_result = [
|
||||
["Admin", "Sample Coordinator", "Sample Coordinator Fee", "Sample Customer",
|
||||
"Incoming", "Sample Producer", "Sample Coordinator Tax", "512.00"],
|
||||
["Admin", "Sample Coordinator", "Sample Coordinator Fee", "Sample Customer",
|
||||
"Outgoing", "Sample Distributor", "Sample Coordinator Tax", "512.00"],
|
||||
["Admin", "Sample Distributor", "Sample Distributor Fee", "Sample Customer",
|
||||
"Incoming", "Sample Producer", "Sample Distributor Tax", "4.00"],
|
||||
["Payment Transaction", "Sample Distributor", "Sample Payment Method", "Sample Customer",
|
||||
nil, nil, nil, "2.00"],
|
||||
["Sales", "Sample Producer", "Sample Producer Fee", "Sample Customer",
|
||||
"Outgoing", "Sample Distributor", "Sample Producer Tax", "64.00"],
|
||||
["Shipment", "Sample Distributor", "Sample Shipping Method", "Sample Customer",
|
||||
nil, nil, "Platform Rate", "1.00"]
|
||||
]
|
||||
|
||||
expected_result.each_with_index do |expected_attributes, row_index|
|
||||
expect_total_attributes(totals[row_index], expected_attributes)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe "filtering results based on permissions" do
|
||||
let!(:distributor_a) do
|
||||
create(:distributor_enterprise, name: "Distributor A", payment_methods: [payment_method],
|
||||
shipping_methods: [shipping_method])
|
||||
end
|
||||
let!(:distributor_b) do
|
||||
create(:distributor_enterprise, name: "Distributor B", payment_methods: [payment_method],
|
||||
shipping_methods: [shipping_method])
|
||||
end
|
||||
|
||||
let!(:order_cycle_a) { create(:simple_order_cycle, coordinator: coordinator) }
|
||||
let!(:order_cycle_b) { create(:simple_order_cycle, coordinator: coordinator) }
|
||||
|
||||
let!(:variant_a) { prepare_variant(distributor: distributor_a, order_cycle: order_cycle_a) }
|
||||
let!(:variant_b) { prepare_variant(distributor: distributor_b, order_cycle: order_cycle_b) }
|
||||
|
||||
let!(:order_a) { prepare_order(order_cycle: order_cycle_a, distributor: distributor_a) }
|
||||
let!(:order_b) { prepare_order(order_cycle: order_cycle_b, distributor: distributor_b) }
|
||||
|
||||
context "when admin" do
|
||||
let!(:current_user) { create(:admin_user) }
|
||||
|
||||
it "includes all order cycles" do
|
||||
totals = service.list
|
||||
|
||||
expect_total_matches(totals, 2, fee_type: "Shipment")
|
||||
expect_total_matches(totals, 1, fee_type: "Shipment", enterprise_name: "Distributor A")
|
||||
expect_total_matches(totals, 1, fee_type: "Shipment", enterprise_name: "Distributor B")
|
||||
end
|
||||
end
|
||||
|
||||
context "when enterprise owner for distributor" do
|
||||
let!(:current_user) { distributor_a.owner }
|
||||
|
||||
it "does not include unrelated order cycles" do
|
||||
totals = service.list
|
||||
|
||||
expect_total_matches(totals, 1, fee_type: "Shipment")
|
||||
expect_total_matches(totals, 1, fee_type: "Shipment", enterprise_name: "Distributor A")
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe "filters entries correctly" do
|
||||
let(:parameters) { report_klass::Parameters.new(parameters_attributes) }
|
||||
|
||||
context "filtering by completion date" do
|
||||
let(:timestamp) { Time.zone.local(2018, 1, 5, 14, 30, 5) }
|
||||
|
||||
let!(:customer_a) { create(:customer, name: "Customer A") }
|
||||
let!(:customer_b) { create(:customer, name: "Customer B") }
|
||||
let!(:customer_c) { create(:customer, name: "Customer C") }
|
||||
|
||||
let!(:order_placed_before_timestamp) do
|
||||
prepare_order(customer: customer_a).tap do |order|
|
||||
order.update_column(:completed_at, timestamp - 1.second)
|
||||
end
|
||||
end
|
||||
|
||||
let!(:order_placed_during_timestamp) do
|
||||
prepare_order(customer: customer_b).tap do |order|
|
||||
order.update_column(:completed_at, timestamp)
|
||||
end
|
||||
end
|
||||
|
||||
let!(:order_placed_after_timestamp) do
|
||||
prepare_order(customer: customer_c).tap do |order|
|
||||
order.update_column(:completed_at, timestamp + 1.second)
|
||||
end
|
||||
end
|
||||
|
||||
context "on or after start_at" do
|
||||
let(:parameters_attributes) { { start_at: timestamp } }
|
||||
|
||||
it "filters entries" do
|
||||
totals = service.list
|
||||
|
||||
expect_total_matches(totals, 0, fee_type: "Shipment", customer_name: "Customer A")
|
||||
expect_total_matches(totals, 1, fee_type: "Shipment", customer_name: "Customer B")
|
||||
expect_total_matches(totals, 1, fee_type: "Shipment", customer_name: "Customer C")
|
||||
end
|
||||
end
|
||||
|
||||
context "on or before end_at" do
|
||||
let(:parameters_attributes) { { end_at: timestamp } }
|
||||
|
||||
it "filters entries" do
|
||||
totals = service.list
|
||||
|
||||
expect_total_matches(totals, 1, fee_type: "Shipment", customer_name: "Customer A")
|
||||
expect_total_matches(totals, 1, fee_type: "Shipment", customer_name: "Customer B")
|
||||
expect_total_matches(totals, 0, fee_type: "Shipment", customer_name: "Customer C")
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe "for specified shops" do
|
||||
let!(:distributor_a) do
|
||||
create(:distributor_enterprise, name: "Distributor A", payment_methods: [payment_method],
|
||||
shipping_methods: [shipping_method])
|
||||
end
|
||||
let!(:distributor_b) do
|
||||
create(:distributor_enterprise, name: "Distributor B", payment_methods: [payment_method],
|
||||
shipping_methods: [shipping_method])
|
||||
end
|
||||
let!(:distributor_c) do
|
||||
create(:distributor_enterprise, name: "Distributor C", payment_methods: [payment_method],
|
||||
shipping_methods: [shipping_method])
|
||||
end
|
||||
|
||||
let!(:order_a) { prepare_order(distributor: distributor_a) }
|
||||
let!(:order_b) { prepare_order(distributor: distributor_b) }
|
||||
let!(:order_c) { prepare_order(distributor: distributor_c) }
|
||||
|
||||
let(:parameters_attributes) { { distributor_ids: [distributor_a.id, distributor_b.id] } }
|
||||
|
||||
it "filters entries" do
|
||||
totals = service.list
|
||||
|
||||
expect_total_matches(totals, 1, fee_type: "Shipment", enterprise_name: "Distributor A")
|
||||
expect_total_matches(totals, 1, fee_type: "Shipment", enterprise_name: "Distributor B")
|
||||
expect_total_matches(totals, 0, fee_type: "Shipment", enterprise_name: "Distributor C")
|
||||
end
|
||||
end
|
||||
|
||||
describe "for specified suppliers" do
|
||||
let!(:producer_a) { create(:supplier_enterprise, name: "Producer A") }
|
||||
let!(:producer_b) { create(:supplier_enterprise, name: "Producer B") }
|
||||
let!(:producer_c) { create(:supplier_enterprise, name: "Producer C") }
|
||||
|
||||
let!(:fee_a) { create(:enterprise_fee, name: "Fee A", enterprise: producer_a) }
|
||||
let!(:fee_b) { create(:enterprise_fee, name: "Fee B", enterprise: producer_b) }
|
||||
let!(:fee_c) { create(:enterprise_fee, name: "Fee C", enterprise: producer_c) }
|
||||
|
||||
let!(:product_a) { create(:product, supplier: producer_a) }
|
||||
let!(:product_b) { create(:product, supplier: producer_b) }
|
||||
let!(:product_c) { create(:product, supplier: producer_c) }
|
||||
|
||||
let!(:variant_a) do
|
||||
prepare_variant(product: product_a, producer: producer_a, incoming_exchange_fees: [fee_a])
|
||||
end
|
||||
let!(:variant_b) do
|
||||
prepare_variant(product: product_b, producer: producer_b, incoming_exchange_fees: [fee_b])
|
||||
end
|
||||
let!(:variant_c) do
|
||||
prepare_variant(product: product_c, producer: producer_c, incoming_exchange_fees: [fee_c])
|
||||
end
|
||||
|
||||
let!(:order_a) { prepare_order(variant: variant_a) }
|
||||
let!(:order_b) { prepare_order(variant: variant_b) }
|
||||
let!(:order_c) { prepare_order(variant: variant_c) }
|
||||
|
||||
let(:parameters_attributes) { { producer_ids: [producer_a.id, producer_b.id] } }
|
||||
|
||||
it "filters entries" do
|
||||
totals = service.list
|
||||
|
||||
expect_total_matches(totals, 1, fee_name: "Fee A", enterprise_name: "Producer A")
|
||||
expect_total_matches(totals, 1, fee_name: "Fee B", enterprise_name: "Producer B")
|
||||
expect_total_matches(totals, 0, fee_name: "Fee C", enterprise_name: "Producer C")
|
||||
end
|
||||
end
|
||||
|
||||
describe "for specified order cycles" do
|
||||
let!(:distributor_a) do
|
||||
create(:distributor_enterprise, name: "Distributor A", payment_methods: [payment_method],
|
||||
shipping_methods: [shipping_method])
|
||||
end
|
||||
let!(:distributor_b) do
|
||||
create(:distributor_enterprise, name: "Distributor B", payment_methods: [payment_method],
|
||||
shipping_methods: [shipping_method])
|
||||
end
|
||||
let!(:distributor_c) do
|
||||
create(:distributor_enterprise, name: "Distributor C", payment_methods: [payment_method],
|
||||
shipping_methods: [shipping_method])
|
||||
end
|
||||
|
||||
let!(:order_cycle_a) { create(:simple_order_cycle, coordinator: coordinator) }
|
||||
let!(:order_cycle_b) { create(:simple_order_cycle, coordinator: coordinator) }
|
||||
let!(:order_cycle_c) { create(:simple_order_cycle, coordinator: coordinator) }
|
||||
|
||||
let!(:variant_a) { prepare_variant(distributor: distributor_a, order_cycle: order_cycle_a) }
|
||||
let!(:variant_b) { prepare_variant(distributor: distributor_b, order_cycle: order_cycle_b) }
|
||||
let!(:variant_c) { prepare_variant(distributor: distributor_c, order_cycle: order_cycle_c) }
|
||||
|
||||
let!(:order_a) { prepare_order(order_cycle: order_cycle_a, distributor: distributor_a) }
|
||||
let!(:order_b) { prepare_order(order_cycle: order_cycle_b, distributor: distributor_b) }
|
||||
let!(:order_c) { prepare_order(order_cycle: order_cycle_c, distributor: distributor_c) }
|
||||
|
||||
let(:parameters_attributes) { { order_cycle_ids: [order_cycle_a.id, order_cycle_b.id] } }
|
||||
|
||||
it "filters entries" do
|
||||
totals = service.list
|
||||
|
||||
expect_total_matches(totals, 1, fee_type: "Shipment", enterprise_name: "Distributor A")
|
||||
expect_total_matches(totals, 1, fee_type: "Shipment", enterprise_name: "Distributor B")
|
||||
expect_total_matches(totals, 0, fee_type: "Shipment", enterprise_name: "Distributor C")
|
||||
end
|
||||
end
|
||||
|
||||
describe "for specified enterprise fees" do
|
||||
let!(:fee_a) { create(:enterprise_fee, name: "Fee A", enterprise: distributor) }
|
||||
let!(:fee_b) { create(:enterprise_fee, name: "Fee B", enterprise: distributor) }
|
||||
let!(:fee_c) { create(:enterprise_fee, name: "Fee C", enterprise: distributor) }
|
||||
|
||||
let!(:variant) { prepare_variant(outgoing_exchange_fees: variant_outgoing_exchange_fees) }
|
||||
let!(:variant_outgoing_exchange_fees) { [fee_a, fee_b, fee_c] }
|
||||
|
||||
let!(:order) { prepare_order(variant: variant) }
|
||||
|
||||
let(:parameters_attributes) { { enterprise_fee_ids: [fee_a.id, fee_b.id] } }
|
||||
|
||||
it "filters entries" do
|
||||
totals = service.list
|
||||
|
||||
expect_total_matches(totals, 1, fee_name: "Fee A")
|
||||
expect_total_matches(totals, 1, fee_name: "Fee B")
|
||||
expect_total_matches(totals, 0, fee_name: "Fee C")
|
||||
end
|
||||
end
|
||||
|
||||
describe "for specified shipping methods" do
|
||||
let!(:shipping_method_a) do
|
||||
create(:shipping_method, name: "Shipping A", distributors: [distributor])
|
||||
end
|
||||
let!(:shipping_method_b) do
|
||||
create(:shipping_method, name: "Shipping B", distributors: [distributor])
|
||||
end
|
||||
let!(:shipping_method_c) do
|
||||
create(:shipping_method, name: "Shipping C", distributors: [distributor])
|
||||
end
|
||||
|
||||
let!(:order_a) { prepare_order(shipping_method: shipping_method_a) }
|
||||
let!(:order_b) { prepare_order(shipping_method: shipping_method_b) }
|
||||
let!(:order_c) { prepare_order(shipping_method: shipping_method_c) }
|
||||
|
||||
let(:parameters_attributes) do
|
||||
{ shipping_method_ids: [shipping_method_a.id, shipping_method_b.id] }
|
||||
end
|
||||
|
||||
it "filters entries" do
|
||||
totals = service.list
|
||||
|
||||
expect_total_matches(totals, 1, fee_name: "Shipping A")
|
||||
expect_total_matches(totals, 1, fee_name: "Shipping B")
|
||||
expect_total_matches(totals, 0, fee_name: "Shipping C")
|
||||
end
|
||||
end
|
||||
|
||||
describe "for specified payment methods" do
|
||||
let!(:payment_method_a) do
|
||||
create(:payment_method, name: "Payment A", distributors: [distributor])
|
||||
end
|
||||
let!(:payment_method_b) do
|
||||
create(:payment_method, name: "Payment B", distributors: [distributor])
|
||||
end
|
||||
let!(:payment_method_c) do
|
||||
create(:payment_method, name: "Payment C", distributors: [distributor])
|
||||
end
|
||||
|
||||
let!(:order_a) { prepare_order(payment_method: payment_method_a) }
|
||||
let!(:order_b) { prepare_order(payment_method: payment_method_b) }
|
||||
let!(:order_c) { prepare_order(payment_method: payment_method_c) }
|
||||
|
||||
let(:parameters_attributes) do
|
||||
{ payment_method_ids: [payment_method_a.id, payment_method_b.id] }
|
||||
end
|
||||
|
||||
it "filters entries" do
|
||||
totals = service.list
|
||||
|
||||
expect_total_matches(totals, 1, fee_name: "Payment A")
|
||||
expect_total_matches(totals, 1, fee_name: "Payment B")
|
||||
expect_total_matches(totals, 0, fee_name: "Payment C")
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
# Helper methods for example group
|
||||
|
||||
def expect_total_attributes(total, expected_attribute_list)
|
||||
actual_attribute_list = [total.fee_type, total.enterprise_name, total.fee_name,
|
||||
total.customer_name, total.fee_placement,
|
||||
total.fee_calculated_on_transfer_through_name, total.tax_category_name,
|
||||
total.total_amount]
|
||||
expect(actual_attribute_list).to eq(expected_attribute_list)
|
||||
end
|
||||
|
||||
def expect_total_matches(totals, count, attributes)
|
||||
expect(count_totals(totals, attributes)).to eq(count)
|
||||
end
|
||||
|
||||
def default_order_options
|
||||
{ customer: customer, distributor: distributor, order_cycle: order_cycle,
|
||||
shipping_method: shipping_method, variant: variant }
|
||||
end
|
||||
|
||||
def prepare_incomplete_order(options = {})
|
||||
target_options = default_order_options.merge(options)
|
||||
create(:order, :with_line_item, target_options)
|
||||
end
|
||||
|
||||
def prepare_order(options = {})
|
||||
factory_trait_options = { payment_method: payment_method }
|
||||
target_options = default_order_options.merge(factory_trait_options).merge(options)
|
||||
create(:order, :with_line_item, :completed, target_options)
|
||||
end
|
||||
|
||||
def default_variant_options
|
||||
{ product: product, producer: producer, is_master: false, coordinator: coordinator,
|
||||
distributor: distributor, order_cycle: order_cycle }
|
||||
end
|
||||
|
||||
def prepare_variant(options = {})
|
||||
target_options = default_variant_options.merge(options)
|
||||
create(:variant, :with_order_cycle, target_options)
|
||||
end
|
||||
|
||||
def count_totals(totals, attributes)
|
||||
totals.count do |data|
|
||||
attributes.all? do |attribute_name, attribute_value|
|
||||
data.public_send(attribute_name) == attribute_value
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
10
engines/order_management/spec/spec_helper.rb
Normal file
10
engines/order_management/spec/spec_helper.rb
Normal file
@@ -0,0 +1,10 @@
|
||||
ENV["RAILS_ENV"] = "test"
|
||||
|
||||
require "order_management"
|
||||
require "../../spec/spec_helper"
|
||||
|
||||
# Require factories in Spree and main application.
|
||||
require 'spree/testing_support/factories'
|
||||
require '../../spec/factories'
|
||||
|
||||
Dir["#{File.dirname(__FILE__)}/support/**/*.rb"].each { |f| require f }
|
||||
79
lib/open_food_network/reports/list.rb
Normal file
79
lib/open_food_network/reports/list.rb
Normal file
@@ -0,0 +1,79 @@
|
||||
module OpenFoodNetwork
|
||||
module Reports
|
||||
class List
|
||||
def self.all
|
||||
new.all
|
||||
end
|
||||
|
||||
def all
|
||||
{
|
||||
orders_and_fulfillment: orders_and_fulfillment_report_types,
|
||||
products_and_inventory: products_and_inventory_report_types,
|
||||
customers: customers_report_types,
|
||||
enterprise_fee_summary: enterprise_fee_summary_report_types,
|
||||
order_cycle_management: order_cycle_management_report_types,
|
||||
sales_tax: sales_tax_report_types,
|
||||
packing: packing_report_types
|
||||
}
|
||||
end
|
||||
|
||||
protected
|
||||
|
||||
def orders_and_fulfillment_report_types
|
||||
[
|
||||
[i18n_translate("supplier_totals"), :order_cycle_supplier_totals],
|
||||
[i18n_translate("supplier_totals_by_distributor"),
|
||||
:order_cycle_supplier_totals_by_distributor],
|
||||
[i18n_translate("totals_by_supplier"), :order_cycle_distributor_totals_by_supplier],
|
||||
[i18n_translate("customer_totals"), :order_cycle_customer_totals]
|
||||
]
|
||||
end
|
||||
|
||||
def products_and_inventory_report_types
|
||||
[
|
||||
[i18n_translate("all_products"), :all_products],
|
||||
[i18n_translate("inventory"), :inventory],
|
||||
[i18n_translate("lettuce_share"), :lettuce_share]
|
||||
]
|
||||
end
|
||||
|
||||
def customers_report_types
|
||||
[
|
||||
[i18n_translate("mailing_list"), :mailing_list],
|
||||
[i18n_translate("addresses"), :addresses]
|
||||
]
|
||||
end
|
||||
|
||||
def enterprise_fee_summary_report_types
|
||||
[
|
||||
[i18n_translate("enterprise_fee_summary"), :enterprise_fee_summary]
|
||||
]
|
||||
end
|
||||
|
||||
def order_cycle_management_report_types
|
||||
[
|
||||
[i18n_translate("payment_methods"), :payment_methods],
|
||||
[i18n_translate("delivery"), :delivery]
|
||||
]
|
||||
end
|
||||
|
||||
def sales_tax_report_types
|
||||
[
|
||||
[i18n_translate("tax_types"), :tax_types],
|
||||
[i18n_translate("tax_rates"), :tax_rates]
|
||||
]
|
||||
end
|
||||
|
||||
def packing_report_types
|
||||
[
|
||||
[i18n_translate("pack_by_customer"), :pack_by_customer],
|
||||
[i18n_translate("pack_by_supplier"), :pack_by_supplier]
|
||||
]
|
||||
end
|
||||
|
||||
def i18n_translate(key)
|
||||
I18n.t(key, scope: "admin.reports")
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -33,6 +33,10 @@ module OpenFoodNetwork
|
||||
Spree::Variant.where(is_master: false).ransack(search_params.merge(m: 'or')).result
|
||||
end
|
||||
|
||||
def distributor
|
||||
Enterprise.find params[:distributor_id]
|
||||
end
|
||||
|
||||
def scope_to_schedule
|
||||
@variants = @variants.in_schedule(params[:schedule_id])
|
||||
end
|
||||
@@ -42,12 +46,29 @@ module OpenFoodNetwork
|
||||
end
|
||||
|
||||
def scope_to_distributor
|
||||
distributor = Enterprise.find params[:distributor_id]
|
||||
if params[:eligible_for_subscriptions]
|
||||
scope_to_eligible_for_subscriptions_in_distributor
|
||||
else
|
||||
scope_to_available_for_orders_in_distributor
|
||||
end
|
||||
end
|
||||
|
||||
def scope_to_available_for_orders_in_distributor
|
||||
@variants = @variants.in_distributor(distributor)
|
||||
scope_variants_to_distributor(@variants, distributor)
|
||||
end
|
||||
|
||||
def scope_to_eligible_for_subscriptions_in_distributor
|
||||
eligible_variants_scope = SubscriptionVariantsService.eligible_variants(distributor)
|
||||
@variants = @variants.merge(eligible_variants_scope)
|
||||
scope_variants_to_distributor(@variants, distributor)
|
||||
end
|
||||
|
||||
def scope_variants_to_distributor(variants, distributor)
|
||||
scoper = OpenFoodNetwork::ScopeVariantToHub.new(distributor)
|
||||
# Perform scoping after all filtering is done.
|
||||
# Filtering could be a problem on scoped variants.
|
||||
@variants.each { |v| scoper.scope(v) }
|
||||
variants.each { |v| scoper.scope(v) }
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -1 +1 @@
|
||||
producer,distributor,name,display_name,units,unit_type,price,on_hand
|
||||
producer,distributor,name,display_name,variant_unit_name,sku,units,unit_type,price,on_hand,on_demand
|
||||
|
||||
|
@@ -10,9 +10,9 @@ describe Admin::SubscriptionLineItemsController, type: :controller do
|
||||
let(:unmanaged_shop) { create(:enterprise) }
|
||||
let!(:product) { create(:product) }
|
||||
let!(:variant) { create(:variant, product: product, unit_value: '100', price: 15.00, option_values: []) }
|
||||
let!(:outgoing_exchange) { order_cycle.exchanges.create(sender: shop, receiver: shop, variants: [variant], enterprise_fees: [enterprise_fee]) }
|
||||
let!(:enterprise_fee) { create(:enterprise_fee, amount: 3.50) }
|
||||
let!(:order_cycle) { create(:simple_order_cycle, coordinator: shop, orders_open_at: 2.days.from_now, orders_close_at: 7.days.from_now) }
|
||||
let!(:outgoing_exchange) { order_cycle.exchanges.create(sender: shop, receiver: shop, variants: [variant], enterprise_fees: [enterprise_fee]) }
|
||||
let!(:schedule) { create(:schedule, order_cycles: [order_cycle]) }
|
||||
let(:unmanaged_schedule) { create(:schedule, order_cycles: [create(:simple_order_cycle, coordinator: unmanaged_shop)]) }
|
||||
|
||||
@@ -42,6 +42,8 @@ describe Admin::SubscriptionLineItemsController, type: :controller do
|
||||
before { params.merge!(shop_id: shop.id) }
|
||||
|
||||
context "but the shop doesn't have permission to sell product in question" do
|
||||
let!(:outgoing_exchange) { }
|
||||
|
||||
it "returns an error" do
|
||||
spree_post :build, params
|
||||
json_response = JSON.parse(response.body)
|
||||
|
||||
@@ -341,7 +341,7 @@ describe Admin::SubscriptionsController, type: :controller do
|
||||
end
|
||||
|
||||
context 'with subscription_line_items params' do
|
||||
let!(:product2) { create(:product, supplier: shop) }
|
||||
let!(:product2) { create(:product) }
|
||||
let!(:variant2) { create(:variant, product: product2, unit_value: '1000', price: 6.00, option_values: []) }
|
||||
|
||||
before do
|
||||
|
||||
@@ -2,10 +2,11 @@ require 'spec_helper'
|
||||
|
||||
xdescribe Spree::Admin::InvoicesController, type: :controller do
|
||||
let(:order) { create(:order_with_totals_and_distribution) }
|
||||
let(:user) { create(:admin_user) }
|
||||
let(:enterprise_user) { create(:user) }
|
||||
let!(:enterprise) { create(:enterprise, owner: enterprise_user) }
|
||||
|
||||
before do
|
||||
allow(controller).to receive(:spree_current_user) { user }
|
||||
allow(controller).to receive(:spree_current_user) { enterprise_user }
|
||||
end
|
||||
|
||||
describe "#create" do
|
||||
|
||||
@@ -0,0 +1,102 @@
|
||||
require "spec_helper"
|
||||
|
||||
describe Spree::Admin::Reports::EnterpriseFeeSummariesController, type: :controller do
|
||||
let(:report_klass) { OrderManagement::Reports::EnterpriseFeeSummary }
|
||||
|
||||
let!(:distributor) { create(:distributor_enterprise) }
|
||||
|
||||
let(:current_user) { distributor.owner }
|
||||
|
||||
before do
|
||||
feature_flags = instance_double(FeatureFlags, enterprise_fee_summary_enabled?: true)
|
||||
allow(FeatureFlags).to receive(:new).with(current_user) { feature_flags }
|
||||
|
||||
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
|
||||
|
||||
context "when feature flag is in effect" do
|
||||
before { allow(FeatureFlags).to receive(:new).with(current_user).and_call_original }
|
||||
|
||||
it "is unauthorized" do
|
||||
get :new
|
||||
expect(response).to redirect_to spree.unauthorized_path
|
||||
end
|
||||
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_format: "csv"
|
||||
|
||||
expect(response).to be_success
|
||||
expect(response.body).not_to be_blank
|
||||
expect(response.header["Content-Type"]).to eq("text/csv")
|
||||
end
|
||||
|
||||
context "when feature flag is in effect" do
|
||||
before { allow(FeatureFlags).to receive(:new).with(current_user).and_call_original }
|
||||
|
||||
it "is unauthorized" do
|
||||
post :create, report: { start_at: "2018-10-09 07:30:00" }, report_format: "csv"
|
||||
expect(response).to redirect_to spree.unauthorized_path
|
||||
end
|
||||
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_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_format: "csv"
|
||||
|
||||
expect(flash[:error]).to eq(report_klass::Authorizer.parameter_not_allowed_error_message)
|
||||
expect(response).to render_template(new_template_path)
|
||||
end
|
||||
end
|
||||
|
||||
describe "filtering results based on permissions" do
|
||||
let!(:distributor) { create(:distributor_enterprise) }
|
||||
let!(:other_distributor) { create(:distributor_enterprise) }
|
||||
|
||||
let!(:order_cycle) { create(:simple_order_cycle, coordinator: distributor) }
|
||||
let!(:other_order_cycle) { create(:simple_order_cycle, coordinator: other_distributor) }
|
||||
|
||||
let(:current_user) { distributor.owner }
|
||||
|
||||
it "applies permissions to report" do
|
||||
post :create, report: {}, report_format: "csv"
|
||||
|
||||
expect(assigns(:permissions).allowed_order_cycles.to_a).to eq([order_cycle])
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def i18n_scope
|
||||
"order_management.reports.enterprise_fee_summary"
|
||||
end
|
||||
|
||||
def new_template_path
|
||||
"spree/admin/reports/enterprise_fee_summaries/new"
|
||||
end
|
||||
end
|
||||
12
spec/factories/calculated_adjustment_factory.rb
Normal file
12
spec/factories/calculated_adjustment_factory.rb
Normal file
@@ -0,0 +1,12 @@
|
||||
attach_per_item_trait = proc do
|
||||
trait :per_item do
|
||||
transient { amount 1 }
|
||||
calculator { build(:calculator_per_item, preferred_amount: amount) }
|
||||
end
|
||||
end
|
||||
|
||||
FactoryBot.modify do
|
||||
factory :payment_method, &attach_per_item_trait
|
||||
factory :shipping_method, &attach_per_item_trait
|
||||
factory :enterprise_fee, &attach_per_item_trait
|
||||
end
|
||||
27
spec/factories/order_factory.rb
Normal file
27
spec/factories/order_factory.rb
Normal file
@@ -0,0 +1,27 @@
|
||||
FactoryBot.modify do
|
||||
factory :order do
|
||||
trait :with_line_item do
|
||||
transient do
|
||||
variant { FactoryGirl.create(:variant) }
|
||||
end
|
||||
|
||||
after(:create) do |order, evaluator|
|
||||
create(:line_item, order: order, variant: evaluator.variant)
|
||||
end
|
||||
end
|
||||
|
||||
trait :completed do
|
||||
transient do
|
||||
payment_method { create(:payment_method, distributors: [distributor]) }
|
||||
end
|
||||
|
||||
after(:create) do |order, evaluator|
|
||||
order.create_shipment!
|
||||
create(:payment, state: "checkout", order: order, amount: order.total,
|
||||
payment_method: evaluator.payment_method)
|
||||
order.update_distribution_charge!
|
||||
while !order.completed? do break unless order.next! end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
34
spec/factories/variant_factory.rb
Normal file
34
spec/factories/variant_factory.rb
Normal file
@@ -0,0 +1,34 @@
|
||||
FactoryBot.modify do
|
||||
factory :variant do
|
||||
trait :with_order_cycle do
|
||||
transient do
|
||||
order_cycle { create(:order_cycle) }
|
||||
producer { product.supplier }
|
||||
coordinator { create(:distributor_enterprise) }
|
||||
distributor { create(:distributor_enterprise) }
|
||||
incoming_exchange_fees { [] }
|
||||
outgoing_exchange_fees { [] }
|
||||
end
|
||||
|
||||
after(:create) do |variant, evaluator|
|
||||
exchange_attributes = { order_cycle_id: evaluator.order_cycle.id, incoming: true,
|
||||
sender_id: evaluator.producer.id,
|
||||
receiver_id: evaluator.coordinator.id }
|
||||
exchange = Exchange.where(exchange_attributes).first_or_create!(exchange_attributes)
|
||||
exchange.variants << variant
|
||||
evaluator.incoming_exchange_fees.each do |enterprise_fee|
|
||||
exchange.enterprise_fees << enterprise_fee
|
||||
end
|
||||
|
||||
exchange_attributes = { order_cycle_id: evaluator.order_cycle.id, incoming: false,
|
||||
sender_id: evaluator.coordinator.id,
|
||||
receiver_id: evaluator.distributor.id }
|
||||
exchange = Exchange.where(exchange_attributes).first_or_create!(exchange_attributes)
|
||||
exchange.variants << variant
|
||||
(evaluator.outgoing_exchange_fees || []).each do |enterprise_fee|
|
||||
exchange.enterprise_fees << enterprise_fee
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -439,7 +439,7 @@ feature %q{
|
||||
expect(page).to have_selector "tr#li_#{li3.id}"
|
||||
fill_in "quick_search", :with => o1.email
|
||||
expect(page).to have_selector "tr#li_#{li1.id}"
|
||||
expect(page).to have_no_selector "tr#li_#{li2.id}", true
|
||||
expect(page).to have_no_selector "tr#li_#{li2.id}"
|
||||
expect(page).to have_no_selector "tr#li_#{li3.id}"
|
||||
end
|
||||
end
|
||||
@@ -567,6 +567,7 @@ feature %q{
|
||||
context "when a filter has been applied" do
|
||||
it "only toggles checkboxes which are in filteredLineItems" do
|
||||
fill_in "quick_search", with: o1.number
|
||||
expect(page).to have_no_selector "tr#li_#{li2.id}"
|
||||
check "toggle_bulk"
|
||||
fill_in "quick_search", with: ''
|
||||
expect(find("tr#li_#{li1.id} input[type='checkbox'][name='bulk']").checked?).to be true
|
||||
@@ -577,11 +578,13 @@ feature %q{
|
||||
it "only applies the delete action to filteredLineItems" do
|
||||
check "toggle_bulk"
|
||||
fill_in "quick_search", with: o1.number
|
||||
expect(page).to have_no_selector "tr#li_#{li2.id}"
|
||||
find("div#bulk-actions-dropdown").click
|
||||
find("div#bulk-actions-dropdown div.menu_item", :text => "Delete Selected" ).click
|
||||
fill_in "quick_search", with: ''
|
||||
expect(page).to have_no_selector "tr#li_#{li1.id}"
|
||||
fill_in "quick_search", with: ''
|
||||
expect(page).to have_selector "tr#li_#{li2.id}"
|
||||
expect(page).to have_no_selector "tr#li_#{li1.id}"
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -740,10 +743,11 @@ feature %q{
|
||||
end
|
||||
|
||||
def select_date(date)
|
||||
current_month = Time.zone.today.strftime("%B")
|
||||
target_month = date.strftime("%B")
|
||||
# Wait for datepicker to open and be associated to the datepicker trigger.
|
||||
expect(page).to have_selector("#ui-datepicker-div")
|
||||
|
||||
navigate_datepicker_to_month date
|
||||
|
||||
find('#ui-datepicker-div .ui-datepicker-header .ui-datepicker-prev').click if current_month != target_month
|
||||
find('#ui-datepicker-div .ui-datepicker-calendar .ui-state-default', text: date.strftime("%e").to_s.strip, exact_text: true).click
|
||||
end
|
||||
end
|
||||
|
||||
@@ -776,9 +776,9 @@ feature %q{
|
||||
|
||||
# Shows spinner whilst loading
|
||||
expect(page).to have_css "img.spinner", visible: true
|
||||
expect(page).to have_no_css "img.spinner", visible: true
|
||||
end
|
||||
|
||||
expect(page).to have_no_css "img.spinner", visible: true
|
||||
expect(page).to have_no_selector "div.reveal-modal"
|
||||
|
||||
within "table#listing_products tr#p_#{product.id}" do
|
||||
|
||||
@@ -294,6 +294,49 @@ feature "Product Import", js: true do
|
||||
expect(page).to have_content 'Cabbage'
|
||||
end
|
||||
end
|
||||
|
||||
it "handles on_demand and on_hand validations with inventory" do
|
||||
csv_data = CSV.generate do |csv|
|
||||
csv << ["name", "distributor", "producer", "category", "on_hand", "price", "units", "on_demand"]
|
||||
csv << ["Beans", "Another Enterprise", "User Enterprise", "Vegetables", nil, "3.20", "500", "true"]
|
||||
csv << ["Sprouts", "Another Enterprise", "User Enterprise", "Vegetables", "6", "6.50", "500", "false"]
|
||||
csv << ["Cabbage", "Another Enterprise", "User Enterprise", "Vegetables", nil, "1.50", "500", nil]
|
||||
end
|
||||
File.write('/tmp/test.csv', csv_data)
|
||||
|
||||
visit main_app.admin_product_import_path
|
||||
select2_select I18n.t('admin.product_import.index.inventories'), from: "settings_import_into"
|
||||
attach_file 'file', '/tmp/test.csv'
|
||||
click_button 'Upload'
|
||||
|
||||
proceed_to_validation
|
||||
|
||||
expect(page).to have_selector '.item-count', text: "3"
|
||||
expect(page).to have_no_selector '.invalid-count'
|
||||
expect(page).to have_selector '.inv-create-count', text: '2'
|
||||
expect(page).to have_selector '.inv-update-count', text: '1'
|
||||
|
||||
save_data
|
||||
|
||||
expect(page).to have_selector '.inv-created-count', text: '2'
|
||||
expect(page).to have_selector '.inv-updated-count', text: '1'
|
||||
|
||||
beans_override = VariantOverride.where(variant_id: product2.variants.first.id, hub_id: enterprise2.id).first
|
||||
sprouts_override = VariantOverride.where(variant_id: product3.variants.first.id, hub_id: enterprise2.id).first
|
||||
cabbage_override = VariantOverride.where(variant_id: product4.variants.first.id, hub_id: enterprise2.id).first
|
||||
|
||||
expect(Float(beans_override.price)).to eq 3.20
|
||||
expect(beans_override.count_on_hand).to be_nil
|
||||
expect(beans_override.on_demand).to be_truthy
|
||||
|
||||
expect(Float(sprouts_override.price)).to eq 6.50
|
||||
expect(sprouts_override.count_on_hand).to eq 6
|
||||
expect(sprouts_override.on_demand).to eq false
|
||||
|
||||
expect(Float(cabbage_override.price)).to eq 1.50
|
||||
expect(cabbage_override.count_on_hand).to be_nil
|
||||
expect(cabbage_override.on_demand).to be_nil
|
||||
end
|
||||
end
|
||||
|
||||
describe "when dealing with uploaded files" do
|
||||
|
||||
159
spec/features/admin/reports/enterprise_fee_summaries_spec.rb
Normal file
159
spec/features/admin/reports/enterprise_fee_summaries_spec.rb
Normal file
@@ -0,0 +1,159 @@
|
||||
require "spec_helper"
|
||||
|
||||
xfeature "enterprise fee summaries", js: true do
|
||||
include AuthenticationWorkflow
|
||||
include WebHelper
|
||||
|
||||
let!(:distributor) { create(:distributor_enterprise) }
|
||||
let!(:other_distributor) { create(:distributor_enterprise) }
|
||||
|
||||
let!(:order_cycle) { create(:simple_order_cycle, coordinator: distributor) }
|
||||
let!(:other_order_cycle) { create(:simple_order_cycle, coordinator: other_distributor) }
|
||||
|
||||
before do
|
||||
feature_flags = instance_double(FeatureFlags, enterprise_fee_summary_enabled?: true)
|
||||
allow(FeatureFlags).to receive(:new).with(current_user) { feature_flags }
|
||||
|
||||
login_as current_user
|
||||
end
|
||||
|
||||
describe "navigation" do
|
||||
context "when accessing the report as an superadmin" do
|
||||
let(:current_user) { create(:admin_user) }
|
||||
|
||||
it "shows link and allows access to the report" do
|
||||
visit spree.admin_reports_path
|
||||
click_on I18n.t("admin.reports.enterprise_fee_summary.name")
|
||||
expect(page).to have_button(I18n.t("filters.generate_report", scope: i18n_scope))
|
||||
end
|
||||
end
|
||||
|
||||
context "when accessing the report as an admin" do
|
||||
let(:current_user) { distributor.owner }
|
||||
|
||||
it "shows link and allows access to the report" do
|
||||
visit spree.admin_reports_path
|
||||
click_on I18n.t("admin.reports.enterprise_fee_summary.name")
|
||||
expect(page).to have_button(I18n.t("filters.generate_report", scope: i18n_scope))
|
||||
end
|
||||
|
||||
context "when feature flag is in effect" do
|
||||
before { allow(FeatureFlags).to receive(:new).with(current_user).and_call_original }
|
||||
|
||||
it "does not show link now allow direct access to the report" do
|
||||
visit spree.admin_reports_path
|
||||
expect(page).to have_no_link I18n.t("admin.reports.enterprise_fee_summary.name")
|
||||
visit spree.new_admin_reports_enterprise_fee_summary_path
|
||||
expect(page).to have_no_button(I18n.t("filters.generate_report", scope: i18n_scope))
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context "when accessing the report as an enterprise user without sufficient permissions" do
|
||||
let(:current_user) { create(:user) }
|
||||
|
||||
it "does not allow access to the report" do
|
||||
visit spree.admin_reports_path
|
||||
expect(page).to have_no_link(I18n.t("admin.reports.enterprise_fee_summary.name"))
|
||||
visit spree.new_admin_reports_enterprise_fee_summary_path
|
||||
expect(page).to have_content(I18n.t("unauthorized"))
|
||||
end
|
||||
|
||||
context "when feature flag is in effect" do
|
||||
before { allow(FeatureFlags).to receive(:new).with(current_user).and_call_original }
|
||||
|
||||
it "does not show link now allow direct access to the report" do
|
||||
visit spree.admin_reports_path
|
||||
expect(page).to have_no_link I18n.t("admin.reports.enterprise_fee_summary.name")
|
||||
visit spree.new_admin_reports_enterprise_fee_summary_path
|
||||
expect(page).to have_no_button(I18n.t("filters.generate_report", scope: i18n_scope))
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe "smoke test for filters" do
|
||||
before do
|
||||
visit spree.new_admin_reports_enterprise_fee_summary_path
|
||||
end
|
||||
|
||||
context "when logged in as admin" do
|
||||
let(:current_user) { create(:admin_user) }
|
||||
|
||||
it "shows all available options" do
|
||||
expect(page).to have_select "report_order_cycle_ids", with_options: [order_cycle.name]
|
||||
end
|
||||
end
|
||||
|
||||
context "when logged in as enterprise user" do
|
||||
let!(:order) { create(:completed_order_with_fees, order_cycle: order_cycle, distributor: distributor) }
|
||||
|
||||
let(:current_user) { distributor.owner }
|
||||
|
||||
it "shows available options for the enterprise" do
|
||||
expect(page).to have_select "report_order_cycle_ids", options: [order_cycle.name]
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe "smoke test for generation of report based on permissions" do
|
||||
before do
|
||||
visit spree.new_admin_reports_enterprise_fee_summary_path
|
||||
end
|
||||
|
||||
context "when logged in as admin" do
|
||||
let!(:order) { create(:completed_order_with_fees, order_cycle: order_cycle, distributor: distributor) }
|
||||
|
||||
let(:current_user) { create(:admin_user) }
|
||||
|
||||
it "generates file with data for all enterprises" do
|
||||
check I18n.t("filters.report_format_csv", scope: i18n_scope)
|
||||
click_on I18n.t("filters.generate_report", scope: i18n_scope)
|
||||
expect(page.response_headers['Content-Type']).to eq "text/csv"
|
||||
expect(page.body).to have_content(distributor.name)
|
||||
end
|
||||
end
|
||||
|
||||
context "when logged in as enterprise user" do
|
||||
let!(:order) { create(:completed_order_with_fees, order_cycle: order_cycle, distributor: distributor) }
|
||||
let!(:other_order) { create(:completed_order_with_fees, order_cycle: other_order_cycle, distributor: other_distributor) }
|
||||
|
||||
let(:current_user) { distributor.owner }
|
||||
|
||||
it "generates file with data for the enterprise" do
|
||||
check I18n.t("filters.report_format_csv", scope: i18n_scope)
|
||||
click_on I18n.t("filters.generate_report", scope: i18n_scope)
|
||||
expect(page.response_headers['Content-Type']).to eq "text/csv"
|
||||
expect(page.body).to have_content(distributor.name)
|
||||
expect(page.body).not_to have_content(other_distributor.name)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe "smoke test for filtering report based on filters" do
|
||||
let!(:second_distributor) { create(:distributor_enterprise) }
|
||||
let!(:second_order_cycle) { create(:simple_order_cycle, coordinator: second_distributor) }
|
||||
|
||||
let!(:order) { create(:completed_order_with_fees, order_cycle: order_cycle, distributor: distributor) }
|
||||
let!(:second_order) { create(:completed_order_with_fees, order_cycle: second_order_cycle, distributor: second_distributor) }
|
||||
|
||||
let(:current_user) { create(:admin_user) }
|
||||
|
||||
before do
|
||||
visit spree.new_admin_reports_enterprise_fee_summary_path
|
||||
end
|
||||
|
||||
it "generates file with data for selected order cycle" do
|
||||
select order_cycle.name, from: "report_order_cycle_ids"
|
||||
check I18n.t("filters.report_format_csv", scope: i18n_scope)
|
||||
click_on I18n.t("filters.generate_report", scope: i18n_scope)
|
||||
expect(page.response_headers['Content-Type']).to eq "text/csv"
|
||||
expect(page.body).to have_content(distributor.name)
|
||||
expect(page.body).not_to have_content(second_distributor.name)
|
||||
end
|
||||
end
|
||||
|
||||
def i18n_scope
|
||||
"spree.admin.reports.enterprise_fee_summaries"
|
||||
end
|
||||
end
|
||||
@@ -31,10 +31,13 @@ feature 'shipping methods' do
|
||||
check "shipping_method_distributor_ids_#{d1.id}"
|
||||
check "shipping_method_distributor_ids_#{d2.id}"
|
||||
check "shipping_method_shipping_categories_"
|
||||
click_button 'Create'
|
||||
click_button I18n.t("actions.create")
|
||||
|
||||
expect(page).to have_no_button I18n.t("actions.create")
|
||||
|
||||
# Then the shipping method should have its distributor set
|
||||
expect(flash_message).to eq('Shipping method "Carrier Pidgeon" has been successfully created!')
|
||||
message = "Shipping method \"Carrier Pidgeon\" has been successfully created!"
|
||||
expect(page).to have_flash_message message
|
||||
|
||||
sm = Spree::ShippingMethod.last
|
||||
expect(sm.name).to eq('Carrier Pidgeon')
|
||||
@@ -104,9 +107,12 @@ feature 'shipping methods' do
|
||||
expect(page).to have_css '.tag-item'
|
||||
end
|
||||
|
||||
click_button 'Create'
|
||||
click_button I18n.t("actions.create")
|
||||
|
||||
expect(page).to have_no_button I18n.t("actions.create")
|
||||
message = "Shipping method \"Teleport\" has been successfully created!"
|
||||
expect(page).to have_flash_message message
|
||||
|
||||
expect(flash_message).to eq('Shipping method "Teleport" has been successfully created!')
|
||||
expect(first('tags-input .tag-list ti-tag-item')).to have_content "local"
|
||||
|
||||
shipping_method = Spree::ShippingMethod.find_by_name('Teleport')
|
||||
|
||||
@@ -145,23 +145,25 @@ xfeature 'Subscriptions' do
|
||||
let!(:customer_user) { create(:user) }
|
||||
let!(:credit_card1) { create(:credit_card, user: customer_user, cc_type: 'visa', last_digits: 1111, month: 10, year: 2030) }
|
||||
let!(:customer) { create(:customer, enterprise: shop, bill_address: address, user: customer_user, allow_charges: true) }
|
||||
let!(:product1) { create(:product, supplier: shop) }
|
||||
let!(:product2) { create(:product, supplier: shop) }
|
||||
let!(:variant1) { create(:variant, product: product1, unit_value: '100', price: 12.00, option_values: []) }
|
||||
let!(:variant2) { create(:variant, product: product2, unit_value: '1000', price: 6.00, option_values: []) }
|
||||
let!(:test_product) { create(:product, supplier: shop, distributors: []) }
|
||||
let!(:test_variant) { create(:variant, product: test_product, unit_value: "100", price: 12.00, option_values: []) }
|
||||
let!(:shop_product) { create(:product, supplier: shop, distributors: [shop]) }
|
||||
let!(:shop_variant) { create(:variant, product: shop_product, unit_value: "1000", price: 6.00, option_values: []) }
|
||||
let!(:enterprise_fee) { create(:enterprise_fee, amount: 1.75) }
|
||||
let!(:order_cycle) { create(:simple_order_cycle, coordinator: shop, orders_open_at: 2.days.from_now, orders_close_at: 7.days.from_now) }
|
||||
let!(:outgoing_exchange) { order_cycle.exchanges.create(sender: shop, receiver: shop, variants: [variant1, variant2], enterprise_fees: [enterprise_fee]) }
|
||||
let!(:outgoing_exchange) { order_cycle.exchanges.create(sender: shop, receiver: shop, variants: [test_variant, shop_variant], enterprise_fees: [enterprise_fee]) }
|
||||
let!(:schedule) { create(:schedule, order_cycles: [order_cycle]) }
|
||||
let!(:payment_method) { create(:stripe_payment_method, name: 'Credit Card', distributors: [shop], preferred_enterprise_id: shop.id) }
|
||||
let!(:shipping_method) { create(:shipping_method, distributors: [shop]) }
|
||||
|
||||
it "passes the smoke test" do
|
||||
before do
|
||||
visit admin_subscriptions_path
|
||||
click_link 'New Subscription'
|
||||
select2_select shop.name, from: 'new_subscription_shop_id'
|
||||
click_button 'Continue'
|
||||
click_link "New Subscription"
|
||||
select2_select shop.name, from: "new_subscription_shop_id"
|
||||
click_button "Continue"
|
||||
end
|
||||
|
||||
it "passes the smoke test" do
|
||||
select2_select customer.email, from: 'customer_id'
|
||||
select2_select schedule.name, from: 'schedule_id'
|
||||
select2_select payment_method.name, from: 'payment_method_id'
|
||||
@@ -215,11 +217,9 @@ xfeature 'Subscriptions' do
|
||||
expect(page).to have_content 'Please add at least one product'
|
||||
|
||||
# Adding a product and getting a price estimate
|
||||
select2_search_async product1.name, from: I18n.t(:name_or_sku), dropdown_css: '.select2-drop'
|
||||
fill_in 'add_quantity', with: 2
|
||||
click_link 'Add'
|
||||
add_variant_to_subscription test_variant, 2
|
||||
within 'table#subscription-line-items tr.item', match: :first do
|
||||
expect(page).to have_selector 'td.description', text: "#{product1.name} - #{variant1.full_name}"
|
||||
expect(page).to have_selector '.description', text: "#{test_product.name} - #{test_variant.full_name}"
|
||||
expect(page).to have_selector 'td.price', text: "$13.75"
|
||||
expect(page).to have_input 'quantity', with: "2"
|
||||
expect(page).to have_selector 'td.total', text: "$27.50"
|
||||
@@ -241,11 +241,9 @@ xfeature 'Subscriptions' do
|
||||
click_button('edit-products')
|
||||
|
||||
# Adding a new product
|
||||
select2_search_async product2.name, from: I18n.t(:name_or_sku), dropdown_css: '.select2-drop'
|
||||
fill_in 'add_quantity', with: 3
|
||||
click_link 'Add'
|
||||
add_variant_to_subscription shop_variant, 3
|
||||
within 'table#subscription-line-items tr.item', match: :first do
|
||||
expect(page).to have_selector 'td.description', text: "#{product2.name} - #{variant2.full_name}"
|
||||
expect(page).to have_selector '.description', text: "#{shop_product.name} - #{shop_variant.full_name}"
|
||||
expect(page).to have_selector 'td.price', text: "$7.75"
|
||||
expect(page).to have_input 'quantity', with: "3"
|
||||
expect(page).to have_selector 'td.total', text: "$23.25"
|
||||
@@ -264,7 +262,7 @@ xfeature 'Subscriptions' do
|
||||
|
||||
# Prices are shown in the index
|
||||
within 'table#subscription-line-items tr.item', match: :first do
|
||||
expect(page).to have_selector 'td.description', text: "#{product2.name} - #{variant2.full_name}"
|
||||
expect(page).to have_selector '.description', text: "#{shop_product.name} - #{shop_variant.full_name}"
|
||||
expect(page).to have_selector 'td.price', text: "$7.75"
|
||||
expect(page).to have_input 'quantity', with: "3"
|
||||
expect(page).to have_selector 'td.total', text: "$23.25"
|
||||
@@ -282,142 +280,249 @@ xfeature 'Subscriptions' do
|
||||
# Standing Line Items are created
|
||||
expect(subscription.subscription_line_items.count).to eq 1
|
||||
subscription_line_item = subscription.subscription_line_items.first
|
||||
expect(subscription_line_item.variant).to eq variant2
|
||||
expect(subscription_line_item.variant).to eq shop_variant
|
||||
expect(subscription_line_item.quantity).to eq 3
|
||||
end
|
||||
end
|
||||
|
||||
context 'editing an existing subscription' do
|
||||
let!(:customer) { create(:customer, enterprise: shop) }
|
||||
let!(:product1) { create(:product, supplier: shop) }
|
||||
let!(:product2) { create(:product, supplier: shop) }
|
||||
let!(:product3) { create(:product, supplier: shop) }
|
||||
let!(:variant1) { create(:variant, product: product1, unit_value: '100', price: 12.00, option_values: []) }
|
||||
let!(:variant2) { create(:variant, product: product2, unit_value: '1000', price: 6.00, option_values: []) }
|
||||
let!(:variant3) { create(:variant, product: product3, unit_value: '10000', price: 22.00, option_values: []) }
|
||||
let!(:enterprise_fee) { create(:enterprise_fee, amount: 1.75) }
|
||||
let!(:order_cycle) { create(:simple_order_cycle, coordinator: shop, orders_open_at: 2.days.from_now, orders_close_at: 7.days.from_now) }
|
||||
let!(:outgoing_exchange) { order_cycle.exchanges.create(sender: shop, receiver: shop, variants: [variant1, variant2], enterprise_fees: [enterprise_fee]) }
|
||||
let!(:schedule) { create(:schedule, order_cycles: [order_cycle]) }
|
||||
let!(:variant3_oc) { create(:simple_order_cycle, coordinator: shop, orders_open_at: 2.days.from_now, orders_close_at: 7.days.from_now) }
|
||||
let!(:variant3_ex) { variant3_oc.exchanges.create(sender: shop, receiver: shop, variants: [variant3]) }
|
||||
let!(:payment_method) { create(:payment_method, distributors: [shop]) }
|
||||
let!(:stripe_payment_method) { create(:stripe_payment_method, name: 'Credit Card', distributors: [shop], preferred_enterprise_id: shop.id) }
|
||||
let!(:shipping_method) { create(:shipping_method, distributors: [shop]) }
|
||||
let!(:subscription) {
|
||||
create(:subscription,
|
||||
shop: shop,
|
||||
customer: customer,
|
||||
schedule: schedule,
|
||||
payment_method: payment_method,
|
||||
shipping_method: shipping_method,
|
||||
subscription_line_items: [create(:subscription_line_item, variant: variant1, quantity: 2, price_estimate: 13.75)],
|
||||
with_proxy_orders: true)
|
||||
}
|
||||
context 'editing an existing subscription' do
|
||||
let!(:customer) { create(:customer, enterprise: shop) }
|
||||
let!(:product1) { create(:product, supplier: shop) }
|
||||
let!(:product2) { create(:product, supplier: shop) }
|
||||
let!(:product3) { create(:product, supplier: shop) }
|
||||
let!(:variant1) { create(:variant, product: product1, unit_value: '100', price: 12.00, option_values: []) }
|
||||
let!(:variant2) { create(:variant, product: product2, unit_value: '1000', price: 6.00, option_values: []) }
|
||||
let!(:variant3) { create(:variant, product: product3, unit_value: '10000', price: 22.00, option_values: []) }
|
||||
let!(:enterprise_fee) { create(:enterprise_fee, amount: 1.75) }
|
||||
let!(:order_cycle) { create(:simple_order_cycle, coordinator: shop, orders_open_at: 2.days.from_now, orders_close_at: 7.days.from_now) }
|
||||
let!(:outgoing_exchange) { order_cycle.exchanges.create(sender: shop, receiver: shop, variants: [variant1, variant2], enterprise_fees: [enterprise_fee]) }
|
||||
let!(:schedule) { create(:schedule, order_cycles: [order_cycle]) }
|
||||
let!(:variant3_oc) { create(:simple_order_cycle, coordinator: shop, orders_open_at: 2.days.from_now, orders_close_at: 7.days.from_now) }
|
||||
let!(:variant3_ex) { variant3_oc.exchanges.create(sender: shop, receiver: shop, variants: [variant3]) }
|
||||
let!(:payment_method) { create(:payment_method, distributors: [shop]) }
|
||||
let!(:stripe_payment_method) { create(:stripe_payment_method, name: 'Credit Card', distributors: [shop], preferred_enterprise_id: shop.id) }
|
||||
let!(:shipping_method) { create(:shipping_method, distributors: [shop]) }
|
||||
let!(:subscription) {
|
||||
create(:subscription,
|
||||
shop: shop,
|
||||
customer: customer,
|
||||
schedule: schedule,
|
||||
payment_method: payment_method,
|
||||
shipping_method: shipping_method,
|
||||
subscription_line_items: [create(:subscription_line_item, variant: variant1, quantity: 2, price_estimate: 13.75)],
|
||||
with_proxy_orders: true)
|
||||
}
|
||||
|
||||
it "passes the smoke test" do
|
||||
visit edit_admin_subscription_path(subscription)
|
||||
it "passes the smoke test" do
|
||||
visit edit_admin_subscription_path(subscription)
|
||||
|
||||
# Customer and Schedule cannot be edited
|
||||
click_button 'edit-details'
|
||||
expect(page).to have_selector '#s2id_customer_id.select2-container-disabled'
|
||||
expect(page).to have_selector '#s2id_schedule_id.select2-container-disabled'
|
||||
# Customer and Schedule cannot be edited
|
||||
click_button 'edit-details'
|
||||
expect(page).to have_selector '#s2id_customer_id.select2-container-disabled'
|
||||
expect(page).to have_selector '#s2id_schedule_id.select2-container-disabled'
|
||||
|
||||
# Can't use a Stripe payment method because customer does not allow it
|
||||
select2_select stripe_payment_method.name, from: 'payment_method_id'
|
||||
expect(page).to have_content I18n.t('admin.subscriptions.details.charges_not_allowed')
|
||||
click_button 'Save Changes'
|
||||
expect(page).to have_content 'Credit card charges are not allowed by this customer'
|
||||
select2_select payment_method.name, from: 'payment_method_id'
|
||||
click_button 'Review'
|
||||
# Can't use a Stripe payment method because customer does not allow it
|
||||
select2_select stripe_payment_method.name, from: 'payment_method_id'
|
||||
expect(page).to have_content I18n.t('admin.subscriptions.details.charges_not_allowed')
|
||||
click_button 'Save Changes'
|
||||
expect(page).to have_content 'Credit card charges are not allowed by this customer'
|
||||
select2_select payment_method.name, from: 'payment_method_id'
|
||||
click_button 'Review'
|
||||
|
||||
# Existing products should be visible
|
||||
click_button 'edit-products'
|
||||
within "#sli_0" do
|
||||
expect(page).to have_selector 'td.description', text: "#{product1.name} - #{variant1.full_name}"
|
||||
expect(page).to have_selector 'td.price', text: "$13.75"
|
||||
expect(page).to have_input 'quantity', with: "2"
|
||||
expect(page).to have_selector 'td.total', text: "$27.50"
|
||||
# Existing products should be visible
|
||||
click_button 'edit-products'
|
||||
within "#sli_0" do
|
||||
expect(page).to have_selector '.description', text: "#{product1.name} - #{variant1.full_name}"
|
||||
expect(page).to have_selector 'td.price', text: "$13.75"
|
||||
expect(page).to have_input 'quantity', with: "2"
|
||||
expect(page).to have_selector 'td.total', text: "$27.50"
|
||||
|
||||
# Remove variant1 from the subscription
|
||||
find("a.delete-item").click
|
||||
end
|
||||
|
||||
# Attempting to submit without a product
|
||||
click_button 'Save Changes'
|
||||
expect(page).to have_content 'Please add at least one product'
|
||||
|
||||
# Add variant2 to the subscription
|
||||
select2_search_async product2.name, from: I18n.t(:name_or_sku), dropdown_css: '.select2-drop'
|
||||
fill_in 'add_quantity', with: 1
|
||||
click_link 'Add'
|
||||
within "#sli_0" do
|
||||
expect(page).to have_selector 'td.description', text: "#{product2.name} - #{variant2.full_name}"
|
||||
expect(page).to have_selector 'td.price', text: "$7.75"
|
||||
expect(page).to have_input 'quantity', with: "1"
|
||||
expect(page).to have_selector 'td.total', text: "$7.75"
|
||||
end
|
||||
|
||||
# Total should be $7.75
|
||||
expect(page).to have_selector '#order_form_total', text: "$7.75"
|
||||
|
||||
# Add variant3 to the subscription (even though it is not available)
|
||||
select2_search_async product3.name, from: I18n.t(:name_or_sku), dropdown_css: '.select2-drop'
|
||||
fill_in 'add_quantity', with: 1
|
||||
click_link 'Add'
|
||||
within "#sli_1" do
|
||||
expect(page).to have_selector 'td.description', text: "#{product3.name} - #{variant3.full_name}"
|
||||
expect(page).to have_selector 'td.price', text: "$22.00"
|
||||
expect(page).to have_input 'quantity', with: "1"
|
||||
expect(page).to have_selector 'td.total', text: "$22.00"
|
||||
end
|
||||
|
||||
# Total should be $29.75
|
||||
expect(page).to have_selector '#order_form_total', text: "$29.75"
|
||||
|
||||
click_button 'Save Changes'
|
||||
expect(page).to have_content "#{product3.name} - #{variant3.full_name} is not available from the selected schedule"
|
||||
|
||||
# Remove variant3 from the subscription
|
||||
within '#sli_1' do
|
||||
find("a.delete-item").click
|
||||
end
|
||||
|
||||
click_button 'Save Changes'
|
||||
expect(page).to have_current_path admin_subscriptions_path
|
||||
|
||||
select2_select shop.name, from: "shop_id"
|
||||
expect(page).to have_selector "td.items.panel-toggle"
|
||||
first("td.items.panel-toggle").click
|
||||
|
||||
# Total should be $7.75
|
||||
expect(page).to have_selector '#order_form_total', text: "$7.75"
|
||||
expect(page).to have_selector 'tr.item', count: 1
|
||||
expect(subscription.reload.subscription_line_items.length).to eq 1
|
||||
expect(subscription.subscription_line_items.first.variant).to eq variant2
|
||||
# Remove variant1 from the subscription
|
||||
find("a.delete-item").click
|
||||
end
|
||||
|
||||
context "with initialised order that has been changed" do
|
||||
let(:proxy_order) { subscription.proxy_orders.first }
|
||||
let(:order) { proxy_order.initialise_order! }
|
||||
let(:line_item) { order.line_items.first }
|
||||
# Attempting to submit without a product
|
||||
click_button 'Save Changes'
|
||||
expect(page).to have_content 'Please add at least one product'
|
||||
|
||||
before { line_item.update_attributes(quantity: 3) }
|
||||
# Add variant2 to the subscription
|
||||
add_variant_to_subscription(variant2, 1)
|
||||
within "#sli_0" do
|
||||
expect(page).to have_selector '.description', text: "#{product2.name} - #{variant2.full_name}"
|
||||
expect(page).to have_selector 'td.price', text: "$7.75"
|
||||
expect(page).to have_input 'quantity', with: "1"
|
||||
expect(page).to have_selector 'td.total', text: "$7.75"
|
||||
end
|
||||
|
||||
it "reports issues encountered during the update" do
|
||||
visit edit_admin_subscription_path(subscription)
|
||||
click_button 'edit-products'
|
||||
# Total should be $7.75
|
||||
expect(page).to have_selector '#order_form_total', text: "$7.75"
|
||||
|
||||
within "#sli_0" do
|
||||
fill_in 'quantity', with: "1"
|
||||
end
|
||||
# Add variant3 to the subscription (even though it is not available)
|
||||
add_variant_to_subscription(variant3, 1)
|
||||
within "#sli_1" do
|
||||
expect(page).to have_selector '.description', text: "#{product3.name} - #{variant3.full_name}"
|
||||
expect(page).to have_selector 'td.price', text: "$22.00"
|
||||
expect(page).to have_input 'quantity', with: "1"
|
||||
expect(page).to have_selector 'td.total', text: "$22.00"
|
||||
end
|
||||
|
||||
click_button 'Save Changes'
|
||||
expect(page).to have_content 'Saved'
|
||||
# Total should be $29.75
|
||||
expect(page).to have_selector '#order_form_total', text: "$29.75"
|
||||
|
||||
expect(page).to have_selector "#order_update_issues_dialog .message", text: I18n.t("admin.subscriptions.order_update_issues_msg")
|
||||
# Remove variant3 from the subscription
|
||||
within '#sli_1' do
|
||||
find("a.delete-item").click
|
||||
end
|
||||
|
||||
click_button 'Save Changes'
|
||||
expect(page).to have_current_path admin_subscriptions_path
|
||||
|
||||
select2_select shop.name, from: "shop_id"
|
||||
expect(page).to have_selector "td.items.panel-toggle"
|
||||
first("td.items.panel-toggle").click
|
||||
|
||||
# Total should be $7.75
|
||||
expect(page).to have_selector '#order_form_total', text: "$7.75"
|
||||
expect(page).to have_selector 'tr.item', count: 1
|
||||
expect(subscription.reload.subscription_line_items.length).to eq 1
|
||||
expect(subscription.subscription_line_items.first.variant).to eq variant2
|
||||
end
|
||||
|
||||
context "with initialised order that has been changed" do
|
||||
let(:proxy_order) { subscription.proxy_orders.first }
|
||||
let(:order) { proxy_order.initialise_order! }
|
||||
let(:line_item) { order.line_items.first }
|
||||
|
||||
before { line_item.update_attributes(quantity: 3) }
|
||||
|
||||
it "reports issues encountered during the update" do
|
||||
visit edit_admin_subscription_path(subscription)
|
||||
click_button 'edit-products'
|
||||
|
||||
within "#sli_0" do
|
||||
fill_in 'quantity', with: "1"
|
||||
end
|
||||
|
||||
click_button 'Save Changes'
|
||||
expect(page).to have_content 'Saved'
|
||||
|
||||
expect(page).to have_selector "#order_update_issues_dialog .message", text: I18n.t("admin.subscriptions.order_update_issues_msg")
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe "allowed variants" do
|
||||
let!(:customer) { create(:customer, enterprise: shop, allow_charges: true) }
|
||||
let!(:credit_card) { create(:credit_card, user: customer.user) }
|
||||
let!(:shop_product) { create(:product, supplier: shop, distributors: [shop]) }
|
||||
let!(:shop_variant) { create(:variant, product: shop_product, unit_value: "2000") }
|
||||
let!(:permitted_supplier) do
|
||||
create(:supplier_enterprise).tap do |supplier|
|
||||
create(:enterprise_relationship, child: shop, parent: supplier, permissions_list: [:add_to_order_cycle])
|
||||
end
|
||||
end
|
||||
let!(:permitted_supplier_product) { create(:product, supplier: permitted_supplier, distributors: [shop]) }
|
||||
let!(:permitted_supplier_variant) { create(:variant, product: permitted_supplier_product, unit_value: "2000") }
|
||||
let!(:incoming_exchange_product) { create(:product, distributors: [shop]) }
|
||||
let!(:incoming_exchange_variant) do
|
||||
create(:variant, product: incoming_exchange_product, unit_value: "2000").tap do |variant|
|
||||
create(:exchange, order_cycle: order_cycle, incoming: true, receiver: shop, variants: [variant])
|
||||
end
|
||||
end
|
||||
let!(:outgoing_exchange_product) { create(:product, distributors: [shop]) }
|
||||
let!(:outgoing_exchange_variant) do
|
||||
create(:variant, product: outgoing_exchange_product, unit_value: "2000").tap do |variant|
|
||||
create(:exchange, order_cycle: order_cycle, incoming: false, receiver: shop, variants: [variant])
|
||||
end
|
||||
end
|
||||
let!(:enterprise_fee) { create(:enterprise_fee, amount: 1.75) }
|
||||
let!(:order_cycle) { create(:simple_order_cycle, coordinator: shop) }
|
||||
let!(:schedule) { create(:schedule, order_cycles: [order_cycle]) }
|
||||
let!(:payment_method) { create(:stripe_payment_method, distributors: [shop], preferred_enterprise_id: shop.id) }
|
||||
let!(:shipping_method) { create(:shipping_method, distributors: [shop]) }
|
||||
|
||||
before do
|
||||
visit admin_subscriptions_path
|
||||
click_link "New Subscription"
|
||||
select2_select shop.name, from: "new_subscription_shop_id"
|
||||
click_button "Continue"
|
||||
end
|
||||
|
||||
it "permit creating and editing of the subscription" do
|
||||
# Fill in other details
|
||||
fill_in_subscription_basic_details
|
||||
click_button "Next"
|
||||
expect(page).to have_content "BILLING ADDRESS"
|
||||
click_button "Next"
|
||||
|
||||
# Add products
|
||||
expect(page).to have_content "NAME OR SKU"
|
||||
add_variant_to_subscription shop_variant, 3
|
||||
expect_not_in_open_or_upcoming_order_cycle_warning 1
|
||||
add_variant_to_subscription permitted_supplier_variant, 4
|
||||
expect_not_in_open_or_upcoming_order_cycle_warning 2
|
||||
add_variant_to_subscription incoming_exchange_variant, 5
|
||||
expect_not_in_open_or_upcoming_order_cycle_warning 3
|
||||
add_variant_to_subscription outgoing_exchange_variant, 6
|
||||
expect_not_in_open_or_upcoming_order_cycle_warning 3
|
||||
click_button "Next"
|
||||
|
||||
# Submit form
|
||||
expect {
|
||||
click_button "Create Subscription"
|
||||
expect(page).to have_current_path admin_subscriptions_path
|
||||
}.to change(Subscription, :count).by(1)
|
||||
|
||||
# Subscription line items are created
|
||||
subscription = Subscription.last
|
||||
expect(subscription.subscription_line_items.count).to eq 4
|
||||
|
||||
# Edit the subscription
|
||||
visit edit_admin_subscription_path(subscription)
|
||||
|
||||
# Remove shop_variant from the subscription
|
||||
click_button "edit-products"
|
||||
within "#sli_0" do
|
||||
expect(page).to have_selector ".description", text: shop_variant.name
|
||||
find("a.delete-item").click
|
||||
end
|
||||
|
||||
# Submit form
|
||||
click_button "Save Changes"
|
||||
expect(page).to have_current_path admin_subscriptions_path
|
||||
|
||||
# Subscription is saved
|
||||
visit edit_admin_subscription_path(subscription)
|
||||
expect(page).to have_selector "#subscription-line-items .item", count: 3
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def fill_in_subscription_basic_details
|
||||
select2_select customer.email, from: "customer_id"
|
||||
select2_select schedule.name, from: "schedule_id"
|
||||
select2_select payment_method.name, from: "payment_method_id"
|
||||
select2_select shipping_method.name, from: "shipping_method_id"
|
||||
|
||||
find_field("begins_at").click
|
||||
choose_today_from_datepicker
|
||||
end
|
||||
|
||||
def expect_not_in_open_or_upcoming_order_cycle_warning(count)
|
||||
expect(page).to have_content variant_not_in_open_or_upcoming_order_cycle_warning, count: count
|
||||
end
|
||||
|
||||
def add_variant_to_subscription(variant, quantity)
|
||||
row_count = all("#subscription-line-items .item").length
|
||||
variant_name = variant.full_name.present? ? "#{variant.name} - #{variant.full_name}" : variant.name
|
||||
select2_search variant.name, from: I18n.t(:name_or_sku), dropdown_css: ".select2-drop", select_text: variant_name
|
||||
fill_in "add_quantity", with: quantity
|
||||
click_link "Add"
|
||||
expect(page).to have_selector("#subscription-line-items .item", count: row_count + 1)
|
||||
end
|
||||
|
||||
def variant_not_in_open_or_upcoming_order_cycle_warning
|
||||
I18n.t("not_in_open_and_upcoming_order_cycles_warning",
|
||||
scope: "admin.subscriptions.subscription_line_items")
|
||||
end
|
||||
end
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
require 'spec_helper'
|
||||
|
||||
describe FeatureFlags do
|
||||
describe '.product_import_enabled?' do
|
||||
let(:user) { build_stubbed(:user) }
|
||||
let(:feature_flags) { described_class.new(user) }
|
||||
let(:user) { build_stubbed(:user) }
|
||||
let(:feature_flags) { described_class.new(user) }
|
||||
|
||||
describe '#product_import_enabled?' do
|
||||
context 'when the user is superadmin' do
|
||||
before do
|
||||
allow(user).to receive(:has_spree_role?).with('admin') { true }
|
||||
@@ -25,4 +25,22 @@ describe FeatureFlags do
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe "#enterprise_fee_summary_enabled?" do
|
||||
context "when the user is superadmin" do
|
||||
let!(:user) { create(:admin_user) }
|
||||
|
||||
it "returns true" do
|
||||
expect(feature_flags).to be_enterprise_fee_summary_enabled
|
||||
end
|
||||
end
|
||||
|
||||
context "when the user is not superadmin" do
|
||||
let!(:user) { create(:user) }
|
||||
|
||||
it "returns false" do
|
||||
expect(feature_flags).not_to be_enterprise_fee_summary_enabled
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -26,7 +26,7 @@ xdescribe ProductImport::ProductImporter do
|
||||
let!(:variant) { create(:variant, product_id: product.id, price: '8.50', on_hand: '100', unit_value: '500', display_name: 'Preexisting Banana') }
|
||||
let!(:product2) { create(:simple_product, supplier: enterprise, on_hand: '100', name: 'Beans', unit_value: '500', primary_taxon_id: category.id, description: nil) }
|
||||
let!(:product3) { create(:simple_product, supplier: enterprise, on_hand: '100', name: 'Sprouts', unit_value: '500', primary_taxon_id: category.id) }
|
||||
let!(:product4) { create(:simple_product, supplier: enterprise, on_hand: '100', name: 'Cabbage', unit_value: '500', primary_taxon_id: category.id) }
|
||||
let!(:product4) { create(:simple_product, supplier: enterprise, on_hand: '100', name: 'Cabbage', unit_value: '1', variant_unit_scale: nil, variant_unit: "items", variant_unit_name: "Whole", primary_taxon_id: category.id) }
|
||||
let!(:product5) { create(:simple_product, supplier: enterprise2, on_hand: '100', name: 'Lettuce', unit_value: '500', primary_taxon_id: category.id) }
|
||||
let!(:product6) { create(:simple_product, supplier: enterprise3, on_hand: '100', name: 'Beetroot', unit_value: '500', on_demand: true, variant_unit_scale: 1, variant_unit: 'weight', primary_taxon_id: category.id, description: nil) }
|
||||
let!(:product7) { create(:simple_product, supplier: enterprise3, on_hand: '100', name: 'Tomato', unit_value: '500', variant_unit_scale: 1, variant_unit: 'weight', primary_taxon_id: category.id, description: nil) }
|
||||
@@ -467,50 +467,106 @@ xdescribe ProductImport::ProductImporter do
|
||||
end
|
||||
|
||||
describe "importing items into inventory" do
|
||||
before do
|
||||
csv_data = CSV.generate do |csv|
|
||||
csv << ["name", "distributor", "producer", "on_hand", "price", "units", "unit_type"]
|
||||
csv << ["Beans", "Another Enterprise", "User Enterprise", "5", "3.20", "500", "g"]
|
||||
csv << ["Sprouts", "Another Enterprise", "User Enterprise", "6", "6.50", "500", "g"]
|
||||
csv << ["Cabbage", "Another Enterprise", "User Enterprise", "2001", "1.50", "500", "g"]
|
||||
describe "creating and updating inventory" do
|
||||
before do
|
||||
csv_data = CSV.generate do |csv|
|
||||
csv << ["name", "distributor", "producer", "on_hand", "price", "units", "unit_type", "variant_unit_name"]
|
||||
csv << ["Beans", "Another Enterprise", "User Enterprise", "5", "3.20", "500", "g", ""]
|
||||
csv << ["Sprouts", "Another Enterprise", "User Enterprise", "6", "6.50", "500", "g", ""]
|
||||
csv << ["Cabbage", "Another Enterprise", "User Enterprise", "2001", "1.50", "1", "", "Whole"]
|
||||
end
|
||||
File.write('/tmp/test-m.csv', csv_data)
|
||||
file = File.new('/tmp/test-m.csv')
|
||||
settings = {'import_into' => 'inventories'}
|
||||
@importer = ProductImport::ProductImporter.new(file, admin, start: 1, end: 100, settings: settings)
|
||||
end
|
||||
File.write('/tmp/test-m.csv', csv_data)
|
||||
file = File.new('/tmp/test-m.csv')
|
||||
settings = {'import_into' => 'inventories'}
|
||||
@importer = ProductImport::ProductImporter.new(file, admin, start: 1, end: 100, settings: settings)
|
||||
end
|
||||
after { File.delete('/tmp/test-m.csv') }
|
||||
after { File.delete('/tmp/test-m.csv') }
|
||||
|
||||
it "validates entries" do
|
||||
@importer.validate_entries
|
||||
entries = JSON.parse(@importer.entries_json)
|
||||
it "validates entries" do
|
||||
@importer.validate_entries
|
||||
entries = JSON.parse(@importer.entries_json)
|
||||
|
||||
expect(filter('valid', entries)).to eq 3
|
||||
expect(filter('invalid', entries)).to eq 0
|
||||
expect(filter('create_inventory', entries)).to eq 2
|
||||
expect(filter('update_inventory', entries)).to eq 1
|
||||
expect(filter('valid', entries)).to eq 3
|
||||
expect(filter('invalid', entries)).to eq 0
|
||||
expect(filter('create_inventory', entries)).to eq 2
|
||||
expect(filter('update_inventory', entries)).to eq 1
|
||||
end
|
||||
|
||||
it "saves and updates inventory" do
|
||||
@importer.save_entries
|
||||
|
||||
expect(@importer.inventory_created_count).to eq 2
|
||||
expect(@importer.inventory_updated_count).to eq 1
|
||||
expect(@importer.updated_ids).to be_a(Array)
|
||||
expect(@importer.updated_ids.count).to eq 3
|
||||
|
||||
beans_override = VariantOverride.where(variant_id: product2.variants.first.id, hub_id: enterprise2.id).first
|
||||
sprouts_override = VariantOverride.where(variant_id: product3.variants.first.id, hub_id: enterprise2.id).first
|
||||
cabbage_override = VariantOverride.where(variant_id: product4.variants.first.id, hub_id: enterprise2.id).first
|
||||
|
||||
expect(Float(beans_override.price)).to eq 3.20
|
||||
expect(beans_override.count_on_hand).to eq 5
|
||||
|
||||
expect(Float(sprouts_override.price)).to eq 6.50
|
||||
expect(sprouts_override.count_on_hand).to eq 6
|
||||
|
||||
expect(Float(cabbage_override.price)).to eq 1.50
|
||||
expect(cabbage_override.count_on_hand).to eq 2001
|
||||
end
|
||||
end
|
||||
|
||||
it "saves and updates inventory" do
|
||||
@importer.save_entries
|
||||
describe "updating existing inventory referenced by display_name" do
|
||||
before do
|
||||
csv_data = CSV.generate do |csv|
|
||||
csv << ["name", "display_name", "distributor", "producer", "on_hand", "price", "units"]
|
||||
csv << ["Oats", "Porridge Oats", "Another Enterprise", "User Enterprise", "900", "", "500"]
|
||||
end
|
||||
File.write('/tmp/test-m.csv', csv_data)
|
||||
file = File.new('/tmp/test-m.csv')
|
||||
settings = {'import_into' => 'inventories'}
|
||||
@importer = ProductImport::ProductImporter.new(file, admin, start: 1, end: 100, settings: settings)
|
||||
end
|
||||
after { File.delete('/tmp/test-m.csv') }
|
||||
|
||||
expect(@importer.inventory_created_count).to eq 2
|
||||
expect(@importer.inventory_updated_count).to eq 1
|
||||
expect(@importer.updated_ids).to be_a(Array)
|
||||
expect(@importer.updated_ids.count).to eq 3
|
||||
it "updates inventory item correctly" do
|
||||
@importer.save_entries
|
||||
|
||||
beans_override = VariantOverride.where(variant_id: product2.variants.first.id, hub_id: enterprise2.id).first
|
||||
sprouts_override = VariantOverride.where(variant_id: product3.variants.first.id, hub_id: enterprise2.id).first
|
||||
cabbage_override = VariantOverride.where(variant_id: product4.variants.first.id, hub_id: enterprise2.id).first
|
||||
expect(@importer.inventory_created_count).to eq 1
|
||||
|
||||
expect(Float(beans_override.price)).to eq 3.20
|
||||
expect(beans_override.count_on_hand).to eq 5
|
||||
override = VariantOverride.where(variant_id: variant2.id, hub_id: enterprise2.id).first
|
||||
visible = InventoryItem.where(variant_id: variant2.id, enterprise_id: enterprise2.id).first.visible
|
||||
|
||||
expect(Float(sprouts_override.price)).to eq 6.50
|
||||
expect(sprouts_override.count_on_hand).to eq 6
|
||||
expect(override.count_on_hand).to eq 900
|
||||
expect(visible).to be_truthy
|
||||
end
|
||||
end
|
||||
|
||||
expect(Float(cabbage_override.price)).to eq 1.50
|
||||
expect(cabbage_override.count_on_hand).to eq 2001
|
||||
describe "updating existing item that was set to hidden in inventory" do
|
||||
before do
|
||||
InventoryItem.create(variant_id: product4.variants.first.id, enterprise_id: enterprise2.id, visible: false)
|
||||
|
||||
csv_data = CSV.generate do |csv|
|
||||
csv << ["name", "distributor", "producer", "on_hand", "price", "units", "variant_unit_name"]
|
||||
csv << ["Cabbage", "Another Enterprise", "User Enterprise", "900", "", "1", "Whole"]
|
||||
end
|
||||
File.write('/tmp/test-m.csv', csv_data)
|
||||
file = File.new('/tmp/test-m.csv')
|
||||
settings = {'import_into' => 'inventories'}
|
||||
@importer = ProductImport::ProductImporter.new(file, admin, start: 1, end: 100, settings: settings)
|
||||
end
|
||||
after { File.delete('/tmp/test-m.csv') }
|
||||
|
||||
it "sets the item to visible in inventory when the item is updated" do
|
||||
@importer.save_entries
|
||||
|
||||
expect(@importer.inventory_updated_count).to eq 1
|
||||
|
||||
override = VariantOverride.where(variant_id: product4.variants.first.id, hub_id: enterprise2.id).first
|
||||
visible = InventoryItem.where(variant_id: product4.variants.first.id, enterprise_id: enterprise2.id).first.visible
|
||||
|
||||
expect(override.count_on_hand).to eq 900
|
||||
expect(visible).to be_truthy
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
@@ -4,9 +4,11 @@ require 'support/cancan_helper'
|
||||
|
||||
module Spree
|
||||
describe User do
|
||||
|
||||
describe "broad permissions" do
|
||||
subject { AbilityDecorator.new(user) }
|
||||
|
||||
include ::AbilityHelper
|
||||
|
||||
let(:user) { create(:user) }
|
||||
let(:enterprise_any) { create(:enterprise, sells: 'any') }
|
||||
let(:enterprise_own) { create(:enterprise, sells: 'own') }
|
||||
@@ -215,6 +217,8 @@ module Spree
|
||||
should have_ability([:admin, :index, :customers, :bulk_coop, :orders_and_fulfillment, :products_and_inventory, :order_cycle_management], for: :report)
|
||||
end
|
||||
|
||||
include_examples "allows access to Enterprise Fee Summary only if feature flag enabled"
|
||||
|
||||
it "should not be able to read other reports" do
|
||||
should_not have_ability([:sales_total, :group_buys, :payments, :orders_and_distributors, :users_and_enterprises, :xero_invoices], for: :report)
|
||||
end
|
||||
@@ -406,6 +410,8 @@ module Spree
|
||||
should have_ability([:admin, :index, :customers, :sales_tax, :group_buys, :bulk_coop, :payments, :orders_and_distributors, :orders_and_fulfillment, :products_and_inventory, :order_cycle_management, :xero_invoices], for: :report)
|
||||
end
|
||||
|
||||
include_examples "allows access to Enterprise Fee Summary only if feature flag enabled"
|
||||
|
||||
it "should not be able to read other reports" do
|
||||
should_not have_ability([:sales_total, :users_and_enterprises], for: :report)
|
||||
end
|
||||
|
||||
@@ -43,5 +43,24 @@ module Spree
|
||||
order.add_variant(product.master)
|
||||
expect(flat_percent_payment_method.compute_amount(order)).to eq 2.0
|
||||
end
|
||||
|
||||
describe "scope" do
|
||||
describe "filtering to specified distributors" do
|
||||
let!(:distributor_a) { create(:distributor_enterprise) }
|
||||
let!(:distributor_b) { create(:distributor_enterprise) }
|
||||
let!(:distributor_c) { create(:distributor_enterprise) }
|
||||
|
||||
let!(:payment_method_a) { create(:payment_method, distributors: [distributor_a, distributor_b]) }
|
||||
let!(:payment_method_b) { create(:payment_method, distributors: [distributor_b]) }
|
||||
let!(:payment_method_c) { create(:payment_method, distributors: [distributor_c]) }
|
||||
|
||||
it "includes only unique records under specified distributors" do
|
||||
result = described_class.for_distributors([distributor_a, distributor_b])
|
||||
expect(result.length).to eq(2)
|
||||
expect(result).to include(payment_method_a)
|
||||
expect(result).to include(payment_method_b)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -18,13 +18,32 @@ module Spree
|
||||
sm.reload.distributors.should match_array [d1, d2]
|
||||
end
|
||||
|
||||
it "finds shipping methods for a particular distributor" do
|
||||
d1 = create(:distributor_enterprise)
|
||||
d2 = create(:distributor_enterprise)
|
||||
sm1 = create(:shipping_method, distributors: [d1])
|
||||
sm2 = create(:shipping_method, distributors: [d2])
|
||||
describe "scope" do
|
||||
describe "filtering to specified distributors" do
|
||||
let!(:distributor_a) { create(:distributor_enterprise) }
|
||||
let!(:distributor_b) { create(:distributor_enterprise) }
|
||||
let!(:distributor_c) { create(:distributor_enterprise) }
|
||||
|
||||
ShippingMethod.for_distributor(d1).should == [sm1]
|
||||
let!(:shipping_method_a) { create(:shipping_method, distributors: [distributor_a, distributor_b]) }
|
||||
let!(:shipping_method_b) { create(:shipping_method, distributors: [distributor_b]) }
|
||||
let!(:shipping_method_c) { create(:shipping_method, distributors: [distributor_c]) }
|
||||
|
||||
it "includes only unique records under specified distributors" do
|
||||
result = described_class.for_distributors([distributor_a, distributor_b])
|
||||
expect(result.length).to eq(2)
|
||||
expect(result).to include(shipping_method_a)
|
||||
expect(result).to include(shipping_method_b)
|
||||
end
|
||||
end
|
||||
|
||||
it "finds shipping methods for a particular distributor" do
|
||||
d1 = create(:distributor_enterprise)
|
||||
d2 = create(:distributor_enterprise)
|
||||
sm1 = create(:shipping_method, distributors: [d1])
|
||||
sm2 = create(:shipping_method, distributors: [d2])
|
||||
|
||||
ShippingMethod.for_distributor(d1).should == [sm1]
|
||||
end
|
||||
end
|
||||
|
||||
it "orders shipping methods by name" do
|
||||
|
||||
@@ -1,8 +1,11 @@
|
||||
require "spec_helper"
|
||||
|
||||
describe SubscriptionValidator do
|
||||
let(:shop) { instance_double(Enterprise, name: "Shop") }
|
||||
let(:owner) { create(:user) }
|
||||
let(:shop) { create(:enterprise, name: "Shop", owner: owner) }
|
||||
|
||||
describe "delegation" do
|
||||
let(:subscription) { create(:subscription) }
|
||||
let(:subscription) { create(:subscription, shop: shop) }
|
||||
let(:validator) { SubscriptionValidator.new(subscription) }
|
||||
|
||||
it "delegates to subscription" do
|
||||
@@ -438,6 +441,7 @@ describe SubscriptionValidator do
|
||||
|
||||
context "but some variants are unavailable" do
|
||||
let(:product) { instance_double(Spree::Product, name: "some_name") }
|
||||
|
||||
before do
|
||||
allow(validator).to receive(:available_variant_ids) { [variant2.id] }
|
||||
allow(variant1).to receive(:product) { product }
|
||||
@@ -451,7 +455,9 @@ describe SubscriptionValidator do
|
||||
end
|
||||
|
||||
context "and all requested variants are available" do
|
||||
before { allow(validator).to receive(:available_variant_ids) { [variant1.id, variant2.id] } }
|
||||
before do
|
||||
allow(validator).to receive(:available_variant_ids) { [variant1.id, variant2.id] }
|
||||
end
|
||||
|
||||
it "returns true" do
|
||||
expect(validator.valid?).to be true
|
||||
|
||||
130
spec/services/subscription_variants_service_spec.rb
Normal file
130
spec/services/subscription_variants_service_spec.rb
Normal file
@@ -0,0 +1,130 @@
|
||||
require "spec_helper"
|
||||
|
||||
describe SubscriptionVariantsService do
|
||||
describe "variant eligibility for subscription" do
|
||||
let!(:shop) { create(:distributor_enterprise) }
|
||||
let!(:producer) { create(:supplier_enterprise) }
|
||||
let!(:product) { create(:product, supplier: producer) }
|
||||
let!(:variant) { product.variants.first }
|
||||
|
||||
let!(:schedule) { create(:schedule, order_cycles: [order_cycle]) }
|
||||
let!(:subscription) { create(:subscription, shop: shop, schedule: schedule) }
|
||||
let!(:subscription_line_item) do
|
||||
create(:subscription_line_item, subscription: subscription, variant: variant)
|
||||
end
|
||||
|
||||
let(:current_order_cycle) do
|
||||
create(:simple_order_cycle, coordinator: shop, orders_open_at: 1.week.ago,
|
||||
orders_close_at: 1.week.from_now)
|
||||
end
|
||||
|
||||
let(:future_order_cycle) do
|
||||
create(:simple_order_cycle, coordinator: shop, orders_open_at: 1.week.from_now,
|
||||
orders_close_at: 2.weeks.from_now)
|
||||
end
|
||||
|
||||
let(:past_order_cycle) do
|
||||
create(:simple_order_cycle, coordinator: shop, orders_open_at: 2.weeks.ago,
|
||||
orders_close_at: 1.week.ago)
|
||||
end
|
||||
|
||||
let!(:order_cycle) { current_order_cycle }
|
||||
|
||||
context "if the shop is the supplier for the product" do
|
||||
let!(:producer) { shop }
|
||||
|
||||
it "is eligible" do
|
||||
expect(described_class.eligible_variants(shop)).to include(variant)
|
||||
end
|
||||
end
|
||||
|
||||
context "if the supplier is permitted for the shop" do
|
||||
let!(:enterprise_relationship) { create(:enterprise_relationship, child: shop, parent: product.supplier, permissions_list: [:add_to_order_cycle]) }
|
||||
|
||||
it "is eligible" do
|
||||
expect(described_class.eligible_variants(shop)).to include(variant)
|
||||
end
|
||||
end
|
||||
|
||||
context "if the variant is involved in an exchange" do
|
||||
let!(:order_cycle) { create(:simple_order_cycle, coordinator: shop) }
|
||||
let!(:schedule) { create(:schedule, order_cycles: [order_cycle]) }
|
||||
|
||||
context "if it is an incoming exchange where the shop is the receiver" do
|
||||
let!(:incoming_exchange) { order_cycle.exchanges.create(sender: product.supplier, receiver: shop, incoming: true, variants: [variant]) }
|
||||
|
||||
it "is not eligible" do
|
||||
expect(described_class.eligible_variants(shop)).to_not include(variant)
|
||||
end
|
||||
end
|
||||
|
||||
context "if it is an outgoing exchange where the shop is the receiver" do
|
||||
let!(:outgoing_exchange) { order_cycle.exchanges.create(sender: product.supplier, receiver: shop, incoming: false, variants: [variant]) }
|
||||
|
||||
context "if the order cycle is currently open" do
|
||||
let!(:order_cycle) { current_order_cycle }
|
||||
|
||||
it "is eligible" do
|
||||
expect(described_class.eligible_variants(shop)).to include(variant)
|
||||
end
|
||||
end
|
||||
|
||||
context "if the order cycle opens in the future" do
|
||||
let!(:order_cycle) { future_order_cycle }
|
||||
|
||||
it "is eligible" do
|
||||
expect(described_class.eligible_variants(shop)).to include(variant)
|
||||
end
|
||||
end
|
||||
|
||||
context "if the order cycle closed in the past" do
|
||||
let!(:order_cycle) { past_order_cycle }
|
||||
|
||||
it "is eligible" do
|
||||
expect(described_class.eligible_variants(shop)).to include(variant)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context "if the variant is unrelated" do
|
||||
it "is not eligible" do
|
||||
expect(described_class.eligible_variants(shop)).to_not include(variant)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe "checking if variant in open and upcoming order cycles" do
|
||||
let!(:shop) { create(:enterprise) }
|
||||
let!(:product) { create(:product) }
|
||||
let!(:variant) { product.variants.first }
|
||||
let!(:schedule) { create(:schedule) }
|
||||
|
||||
context "if the variant is involved in an exchange" do
|
||||
let!(:order_cycle) { create(:simple_order_cycle, coordinator: shop) }
|
||||
let!(:schedule) { create(:schedule, order_cycles: [order_cycle]) }
|
||||
|
||||
context "if it is an incoming exchange where the shop is the receiver" do
|
||||
let!(:incoming_exchange) { order_cycle.exchanges.create(sender: product.supplier, receiver: shop, incoming: true, variants: [variant]) }
|
||||
|
||||
it "is is false" do
|
||||
expect(described_class).not_to be_in_open_and_upcoming_order_cycles(shop, schedule, variant)
|
||||
end
|
||||
end
|
||||
|
||||
context "if it is an outgoing exchange where the shop is the receiver" do
|
||||
let!(:outgoing_exchange) { order_cycle.exchanges.create(sender: product.supplier, receiver: shop, incoming: false, variants: [variant]) }
|
||||
|
||||
it "is true" do
|
||||
expect(described_class).to be_in_open_and_upcoming_order_cycles(shop, schedule, variant)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context "if the variant is unrelated" do
|
||||
it "is false" do
|
||||
expect(described_class).to_not be_in_open_and_upcoming_order_cycles(shop, schedule, variant)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -135,6 +135,7 @@ RSpec.configure do |config|
|
||||
config.extend Spree::Api::TestingSupport::Setup, :type => :controller
|
||||
config.include Spree::Api::TestingSupport::Helpers, :type => :controller
|
||||
config.include OpenFoodNetwork::ControllerHelper, :type => :controller
|
||||
config.include Features::DatepickerHelper, type: :feature
|
||||
config.include OpenFoodNetwork::FeatureToggleHelper
|
||||
config.include OpenFoodNetwork::FiltersHelper
|
||||
config.include OpenFoodNetwork::EnterpriseGroupsHelper
|
||||
|
||||
28
spec/support/ability_helper.rb
Normal file
28
spec/support/ability_helper.rb
Normal file
@@ -0,0 +1,28 @@
|
||||
module AbilityHelper
|
||||
shared_examples "allows access to Enterprise Fee Summary only if feature flag enabled" do
|
||||
it "should not be able to read Enterprise Fee Summary" do
|
||||
is_expected.not_to have_link_to_enterprise_fee_summary
|
||||
is_expected.not_to have_direct_access_to_enterprise_fee_summary
|
||||
end
|
||||
|
||||
context "when feature flag for Enterprise Fee Summary is enabled absolutely" do
|
||||
before do
|
||||
feature_flags = instance_double(FeatureFlags, enterprise_fee_summary_enabled?: true)
|
||||
allow(FeatureFlags).to receive(:new).with(user) { feature_flags }
|
||||
end
|
||||
|
||||
it "should be able to see link and read report" do
|
||||
is_expected.to have_link_to_enterprise_fee_summary
|
||||
is_expected.to have_direct_access_to_enterprise_fee_summary
|
||||
end
|
||||
end
|
||||
|
||||
def have_link_to_enterprise_fee_summary
|
||||
have_ability([:enterprise_fee_summary], for: :report)
|
||||
end
|
||||
|
||||
def have_direct_access_to_enterprise_fee_summary
|
||||
have_ability([:admin, :new, :create], for: :enterprise_fee_summary)
|
||||
end
|
||||
end
|
||||
end
|
||||
33
spec/support/features/datepicker_helper.rb
Normal file
33
spec/support/features/datepicker_helper.rb
Normal file
@@ -0,0 +1,33 @@
|
||||
module Features
|
||||
module DatepickerHelper
|
||||
def choose_today_from_datepicker
|
||||
within(".ui-datepicker-calendar") do
|
||||
find(".ui-datepicker-today").click
|
||||
end
|
||||
end
|
||||
|
||||
def navigate_datepicker_to_month(date, reference_date = Time.zone.today)
|
||||
month_and_year = date.strftime("%B %Y")
|
||||
|
||||
until datepicker_month_and_year == month_and_year.upcase
|
||||
if date < reference_date
|
||||
navigate_datepicker_to_previous_month
|
||||
elsif date > reference_date
|
||||
navigate_datepicker_to_next_month
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def navigate_datepicker_to_previous_month
|
||||
find('#ui-datepicker-div .ui-datepicker-header .ui-datepicker-prev').click
|
||||
end
|
||||
|
||||
def navigate_datepicker_to_next_month
|
||||
find('#ui-datepicker-div .ui-datepicker-header .ui-datepicker-next').click
|
||||
end
|
||||
|
||||
def datepicker_month_and_year
|
||||
find("#ui-datepicker-div .ui-datepicker-title").text
|
||||
end
|
||||
end
|
||||
end
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user