Compare commits

..

1 Commits

Author SHA1 Message Date
dependabot[bot]
34867fb85a Bump bigdecimal from 3.3.1 to 4.0.1
Bumps [bigdecimal](https://github.com/ruby/bigdecimal) from 3.3.1 to 4.0.1.
- [Release notes](https://github.com/ruby/bigdecimal/releases)
- [Changelog](https://github.com/ruby/bigdecimal/blob/master/CHANGES.md)
- [Commits](https://github.com/ruby/bigdecimal/compare/v3.3.1...v4.0.1)

---
updated-dependencies:
- dependency-name: bigdecimal
  dependency-version: 4.0.1
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-02-17 09:34:54 +00:00
62 changed files with 314 additions and 1407 deletions

View File

@@ -207,7 +207,7 @@ GEM
bcp47_spec (0.2.1)
bcrypt (3.1.20)
benchmark (0.5.0)
bigdecimal (3.3.1)
bigdecimal (4.0.1)
bindata (2.5.1)
bindex (0.8.1)
bootsnap (1.22.0)
@@ -326,21 +326,19 @@ GEM
factory_bot_rails (6.2.0)
factory_bot (~> 6.2.0)
railties (>= 5.0.0)
faraday (2.14.1)
faraday-net_http (>= 2.0, < 3.5)
json
logger
faraday (2.9.0)
faraday-net_http (>= 2.0, < 3.2)
faraday-follow_redirects (0.4.0)
faraday (>= 1, < 3)
faraday-net_http (3.4.2)
net-http (~> 0.5)
faraday-net_http (3.1.1)
net-http
ferrum (0.14)
addressable (~> 2.5)
concurrent-ruby (~> 1.1)
webrick (~> 1.7)
websocket-driver (>= 0.6, < 0.8)
ffaker (2.25.0)
ffi (1.17.3)
ffi (1.17.2)
flipper (1.3.6)
concurrent-ruby (< 2)
flipper-active_record (1.3.6)
@@ -442,7 +440,7 @@ GEM
thor (>= 0.14, < 2.0)
jquery-ui-rails (4.2.1)
railties (>= 3.2.16)
json (2.18.1)
json (2.15.2)
json-canonicalization (1.0.0)
json-jwt (1.17.0)
activesupport (>= 4.2)
@@ -468,7 +466,7 @@ GEM
activesupport (>= 4.2)
jwt (2.10.2)
base64
knapsack_pro (9.2.2)
knapsack_pro (8.4.0)
rake
language_server-protocol (3.17.0.5)
launchy (3.0.0)
@@ -478,8 +476,7 @@ GEM
launchy (>= 2.2, < 4)
link_header (0.0.8)
lint_roller (1.1.0)
listen (3.10.0)
logger
listen (3.9.0)
rb-fsevent (~> 0.10, >= 0.10.3)
rb-inotify (~> 0.9, >= 0.9.10)
logger (1.7.0)
@@ -516,8 +513,8 @@ GEM
multi_json (1.17.0)
multi_xml (0.6.0)
mutex_m (0.3.0)
net-http (0.9.1)
uri (>= 0.11.1)
net-http (0.7.0)
uri
net-imap (0.5.12)
date
net-protocol
@@ -575,7 +572,7 @@ GEM
parallel (1.27.0)
paranoia (2.6.4)
activerecord (>= 5.1, < 7.2)
parser (3.3.10.2)
parser (3.3.10.1)
ast (~> 2.4.1)
racc
paypal-sdk-core (0.3.4)
@@ -618,7 +615,7 @@ GEM
railties (>= 4.2)
raabro (1.4.0)
racc (1.8.1)
rack (2.2.22)
rack (2.2.21)
rack-mini-profiler (2.3.4)
rack (>= 1.2.0)
rack-oauth2 (2.3.0)
@@ -692,21 +689,15 @@ GEM
activesupport (>= 6.1.5)
i18n
rb-fsevent (0.11.2)
rb-inotify (0.11.1)
rb-inotify (0.10.1)
ffi (~> 1.0)
rdf (3.3.4)
rdf (3.3.1)
bcp47_spec (~> 0.2)
bigdecimal (~> 3.1, >= 3.1.5)
link_header (~> 0.0, >= 0.0.8)
logger (~> 1.5)
ostruct (~> 0.6)
readline (~> 0.0)
rdoc (7.2.0)
erb
psych (>= 4.0.0)
tsort
readline (0.0.4)
reline
redcarpet (3.6.1)
redis (5.4.1)
redis-client (>= 0.22.0)
@@ -774,7 +765,7 @@ GEM
rswag-ui (2.17.0)
actionpack (>= 5.2, < 8.2)
railties (>= 5.2, < 8.2)
rubocop (1.84.2)
rubocop (1.81.7)
json (~> 2.3)
language_server-protocol (~> 3.17.0.2)
lint_roller (~> 1.1.0)
@@ -782,7 +773,7 @@ GEM
parser (>= 3.3.0.2)
rainbow (>= 2.2.2, < 4.0)
regexp_parser (>= 2.9.3, < 3.0)
rubocop-ast (>= 1.49.0, < 2.0)
rubocop-ast (>= 1.47.1, < 2.0)
ruby-progressbar (~> 1.7)
unicode-display_width (>= 2.4.0, < 4.0)
rubocop-ast (1.49.0)
@@ -794,7 +785,7 @@ GEM
rubocop-factory_bot (2.28.0)
lint_roller (~> 1.1)
rubocop (~> 1.72, >= 1.72.1)
rubocop-rails (2.34.3)
rubocop-rails (2.34.2)
activesupport (>= 4.2.0)
lint_roller (~> 1.1)
rack (>= 1.1)
@@ -852,7 +843,7 @@ GEM
caxlsx (<= 4.0)
csv
rodf
spring (4.4.2)
spring (4.4.0)
spring-commands-rspec (1.0.4)
spring (>= 0.9.1)
spring-commands-rubocop (0.4.0)
@@ -902,8 +893,7 @@ GEM
tilt (2.6.1)
timeout (0.6.0)
tsort (0.2.0)
ttfunk (1.8.0)
bigdecimal (~> 3.1)
ttfunk (1.7.0)
turbo-rails (2.0.20)
actionpack (>= 7.1.0)
railties (>= 7.1.0)

View File

@@ -1,18 +1,17 @@
# frozen_string_literal: true
class SearchableDropdownComponent < ViewComponent::Base
REMOVED_SEARCH_PLUGIN = { 'tom-select-options-value': '{ "plugins": [] }' }.freeze
MINIMUM_OPTIONS_FOR_SEARCH_FIELD = 11 # at least 11 options are required for the search field
def initialize(
form:,
name:,
options:,
selected_option:,
form: nil,
placeholder_value: '',
placeholder_value:,
include_blank: false,
aria_label: '',
multiple: false,
remote_url: nil,
other_attrs: {}
)
@f = form
@@ -22,15 +21,13 @@ class SearchableDropdownComponent < ViewComponent::Base
@placeholder_value = placeholder_value
@include_blank = include_blank
@aria_label = aria_label
@multiple = multiple
@remote_url = remote_url
@other_attrs = other_attrs
end
private
attr_reader :f, :name, :options, :selected_option, :placeholder_value, :include_blank,
:aria_label, :multiple, :remote_url, :other_attrs
:aria_label, :other_attrs
def classes
"fullwidth #{'no-input' if remove_search_plugin?}"
@@ -39,33 +36,11 @@ class SearchableDropdownComponent < ViewComponent::Base
def data
{
controller: "tom-select",
'tom-select-placeholder-value': placeholder_value,
'tom-select-options-value': tom_select_options_value,
'tom-select-remote-url-value': remote_url,
}
end
def tom_select_options_value
plugins = []
plugins << 'virtual_scroll' if @remote_url.present?
plugins << 'dropdown_input' unless remove_search_plugin?
plugins << 'remove_button' if multiple
{
plugins:,
maxItems: multiple ? nil : 1,
}
end
def uses_form_builder?
f.present?
'tom-select-placeholder-value': placeholder_value
}.merge(remove_search_plugin? ? REMOVED_SEARCH_PLUGIN : {})
end
def remove_search_plugin?
# Remove the search plugin when:
# - the select is multiple (it already includes a search field), or
# - there is no remote URL and the options are below the search threshold
@remove_search_plugin ||= multiple ||
(@remote_url.nil? && options.count < MINIMUM_OPTIONS_FOR_SEARCH_FIELD)
@remove_search_plugin ||= options.count < MINIMUM_OPTIONS_FOR_SEARCH_FIELD
end
end

View File

@@ -1,4 +1 @@
- if uses_form_builder?
= f.select name, options, { selected: selected_option, include_blank:, multiple: }, class: classes, data:, 'aria-label': aria_label, **other_attrs
- else
= select_tag name, options_for_select(options, selected_option), include_blank:, multiple:, class: classes, data:, 'aria-label': aria_label, **other_attrs
= f.select name, options_for_select(options, selected_option), { include_blank: }, class: classes, data:, 'aria-label': aria_label, **other_attrs

View File

@@ -4,7 +4,6 @@ module Admin
class ReportsController < Spree::Admin::BaseController
include ActiveStorage::SetCurrent
include ReportsActions
include Reports::AjaxSearch
helper ReportsHelper

View File

@@ -1,108 +0,0 @@
# frozen_string_literal: true
module Reports
module AjaxSearch
extend ActiveSupport::Concern
def search_enterprise_fees
report = report_class.new(spree_current_user, params, render: false)
fee_ids = enterprise_fee_ids(report.search.result)
query = EnterpriseFee.where(id: fee_ids)
render json: build_search_response(query)
end
def search_enterprise_fee_owners
report = report_class.new(spree_current_user, params, render: false)
owner_ids = enterprise_fee_owner_ids(report.search.result)
query = Enterprise.where(id: owner_ids)
render json: build_search_response(query)
end
def search_distributors
query = frontend_data.distributors
render json: build_search_response(query)
end
def search_order_cycles
query = frontend_data.order_cycles
render json: build_search_response(query)
end
def search_order_customers
query = frontend_data.order_customers
render json: build_search_response(query)
end
def search_suppliers
query = frontend_data.orders_suppliers
render json: build_search_response(query)
end
private
def build_search_response(query)
page = (params[:page] || 1).to_i
per_page = 30
filtered_query = apply_search_filter(query)
total_count = filtered_query.size
items = paginated_items(filtered_query, page, per_page)
results = format_results(items)
{ results: results, pagination: { more: (page * per_page) < total_count } }
end
def apply_search_filter(query)
search_term = params[:q]
return query if search_term.blank?
escaped_search_term = ActiveRecord::Base.sanitize_sql_like(search_term)
pattern = "%#{escaped_search_term}%"
# Handle different model types
if query.model == OrderCycle
query.where("order_cycles.name ILIKE ?", pattern)
elsif query.model == Customer
query.where("customers.email ILIKE ?", pattern)
else
query.where("name ILIKE ?", pattern)
end
end
def paginated_items(query, page, per_page)
if query.model == Customer
query.order(:email).offset((page - 1) * per_page).limit(per_page).pluck(:email, :id)
elsif query.model == OrderCycle
query.order('order_cycles.orders_close_at DESC')
.offset((page - 1) * per_page)
.limit(per_page).pluck(
:name, :id
)
else
query.order(:name).offset((page - 1) * per_page).limit(per_page).pluck(:name, :id)
end
end
def format_results(items)
items.map { |label, value| { value:, label: } }
end
def frontend_data
@frontend_data ||= Reporting::FrontendData.new(spree_current_user)
end
def enterprise_fee_owner_ids(orders)
EnterpriseFee.where(id: enterprise_fee_ids(orders)).select(:enterprise_id)
end
def enterprise_fee_ids(orders)
Spree::Adjustment.enterprise_fee.where(order_id: orders.select(:id)).select(:originator_id)
end
end
end

View File

@@ -138,7 +138,7 @@ module Spree
providers.delete("Spree::Gateway::StripeSCA") unless show_stripe?
providers
providers.map(&:constantize)
end
# Show Stripe as an option if enabled, or if the

View File

@@ -95,7 +95,8 @@ module Spree
private
def load_payment_source
if @payment.payment_method.is_a?(Gateway::StripeSCA) &&
if @payment.payment_method.is_a?(Spree::Gateway) &&
@payment.payment_method.payment_profiles_supported? &&
params[:card].present? &&
(params[:card] != 'new')
@payment.source = CreditCard.find_by(id: params[:card])

View File

@@ -34,8 +34,29 @@ module ReportsHelper
end
end
def fee_name_options(orders)
EnterpriseFee.where(id: enterprise_fee_ids(orders))
.pluck(:name, :id)
end
def fee_owner_options(orders)
Enterprise.where(id: enterprise_fee_owner_ids(orders))
.pluck(:name, :id)
end
delegate :currency_symbol, to: :'Spree::Money'
def enterprise_fee_owner_ids(orders)
EnterpriseFee.where(id: enterprise_fee_ids(orders))
.pluck(:enterprise_id)
end
def enterprise_fee_ids(orders)
Spree::Adjustment.enterprise_fee
.where(order_id: orders.map(&:id))
.pluck(:originator_id)
end
def datepicker_time(datetime)
datetime = Time.zone.parse(datetime) if datetime.is_a? String
datetime.strftime('%Y-%m-%d %H:%M')

View File

@@ -1,18 +0,0 @@
# frozen_string_literal: true
module Spree
module Admin
module PaymentMethodsHelper
def payment_method_type_name(class_name)
scope = "spree.admin.payment_methods.providers"
key = class_name.demodulize.downcase
I18n.t(key, scope:)
end
def payment_method_type_options(providers)
providers.map { |p| [payment_method_type_name(p), p] }
end
end
end
end

View File

@@ -6,11 +6,6 @@ module Spree
class Ability
include CanCan::Ability
REPORTS_SEARCH_ACTIONS = [
:search_enterprise_fees, :search_enterprise_fee_owners, :search_distributors,
:search_suppliers, :search_order_cycles, :search_order_customers
].freeze
def initialize(user)
clear_aliased_actions
@@ -265,8 +260,7 @@ module Spree
can [:admin, :index, :import], ::Admin::DfcProductImportsController
# Reports page
can [:admin, :index, :show, :create, *REPORTS_SEARCH_ACTIONS],
::Admin::ReportsController
can [:admin, :index, :show, :create], ::Admin::ReportsController
can [:admin, :show, :create, :customers, :orders_and_distributors, :group_buys, :payments,
:orders_and_fulfillment, :products_and_inventory, :order_cycle_management,
:packing, :enterprise_fee_summary, :bulk_coop, :suppliers], :report
@@ -398,7 +392,7 @@ module Spree
end
# Reports page
can [:admin, :index, :show, :create, *REPORTS_SEARCH_ACTIONS], ::Admin::ReportsController
can [:admin, :index, :show, :create], ::Admin::ReportsController
can [:admin, :customers, :group_buys, :sales_tax, :payments,
:orders_and_distributors, :orders_and_fulfillment, :products_and_inventory,
:order_cycle_management, :xero_invoices, :enterprise_fee_summary, :bulk_coop], :report

View File

@@ -5,7 +5,7 @@ module Spree
acts_as_taggable
include PaymentMethodDistributors
delegate :authorize, :purchase, :capture, :void, :credit, :refund, to: :provider
delegate :authorize, :purchase, :capture, :void, :credit, to: :provider
validates :name, :type, presence: true
@@ -35,10 +35,6 @@ module Spree
end
def method_missing(method, *)
message = "Deprecated delegation of Gateway##{method}"
Alert.raise(message)
raise message if Rails.env.local?
if @provider.nil? || !@provider.respond_to?(method)
super
else
@@ -46,6 +42,10 @@ module Spree
end
end
def payment_profiles_supported?
false
end
def method_type
'gateway'
end

View File

@@ -35,6 +35,10 @@ module Spree
ActiveMerchant::Billing::StripePaymentIntentsGateway
end
def payment_profiles_supported?
true
end
def stripe_account_id
StripeAccount.find_by(enterprise_id: preferred_enterprise_id)&.stripe_user_id
end
@@ -80,7 +84,7 @@ module Spree
end
# NOTE: this method is required by Spree::Payment::Processing
def void(payment_intent_id, gateway_options)
def void(payment_intent_id, _creditcard, gateway_options)
payment_intent_response = Stripe::PaymentIntent.retrieve(
payment_intent_id, stripe_account: stripe_account_id
)
@@ -97,13 +101,7 @@ module Spree
end
# NOTE: this method is required by Spree::Payment::Processing
def credit(money, payment_intent_id, gateway_options)
gateway_options[:stripe_account] = stripe_account_id
provider.refund(money, payment_intent_id, gateway_options)
end
# NOTE: this method is required by Spree::Payment::Processing
def refund(money, payment_intent_id, gateway_options)
def credit(money, _creditcard, payment_intent_id, gateway_options)
gateway_options[:stripe_account] = stripe_account_id
provider.refund(money, payment_intent_id, gateway_options)
end

View File

@@ -34,7 +34,7 @@ module Spree
# invalidate previously entered payments
after_create :invalidate_old_payments
after_save :create_payment_profile
after_save :create_payment_profile, if: :profiles_supported?
# update the order totals, etc.
after_save :ensure_correct_adjustment, :update_order
@@ -217,13 +217,18 @@ module Spree
errors.blank?
end
def profiles_supported?
payment_method.respond_to?(:payment_profiles_supported?) &&
payment_method.payment_profiles_supported?
end
def create_payment_profile
return unless source.is_a?(CreditCard)
return unless source.try(:save_requested_by_customer?)
return unless source.number || source.gateway_payment_profile_id
return unless source.gateway_customer_profile_id.nil?
payment_method.try(:create_profile, self)
payment_method.create_profile(self)
rescue ActiveMerchant::ConnectionError => e
gateway_error e
end

View File

@@ -58,7 +58,16 @@ module Spree
protect_from_connection_error do
check_environment
response = payment_method.void(response_code, gateway_options)
response = if payment_method.payment_profiles_supported?
# Gateways supporting payment profiles will need access to credit
# card object because this stores the payment profile information
# so supply the authorization itself as well as the credit card,
# rather than just the authorization code
payment_method.void(response_code, source, gateway_options)
else
# Standard ActiveMerchant void usage
payment_method.void(response_code, gateway_options)
end
record_response(response)
@@ -77,11 +86,20 @@ module Spree
credit_amount = calculate_refund_amount(credit_amount)
response = payment_method.credit(
(credit_amount * 100).round,
response_code,
gateway_options
)
response = if payment_method.payment_profiles_supported?
payment_method.credit(
(credit_amount * 100).round,
source,
response_code,
gateway_options
)
else
payment_method.credit(
(credit_amount * 100).round,
response_code,
gateway_options
)
end
record_response(response)
@@ -107,11 +125,20 @@ module Spree
refund_amount = calculate_refund_amount(refund_amount)
response = payment_method.refund(
(refund_amount * 100).round,
response_code,
gateway_options
)
response = if payment_method.payment_profiles_supported?
payment_method.refund(
(refund_amount * 100).round,
source,
response_code,
gateway_options
)
else
payment_method.refund(
(refund_amount * 100).round,
response_code,
gateway_options
)
end
record_response(response)

View File

@@ -93,6 +93,10 @@ module Spree
unscoped { find(*) }
end
def payment_profiles_supported?
false
end
def source_required?
true
end
@@ -109,6 +113,11 @@ module Spree
distributors.include?(distributor)
end
def self.clean_name
scope = "spree.admin.payment_methods.providers"
I18n.t(name.demodulize.downcase, scope:)
end
private
def distributor_validation

View File

@@ -251,7 +251,7 @@ module Spree
transaction do
ExchangeVariant.
where(exchange_variants: { variant_id: variants.with_deleted.
select(:id) }).destroy_all
select(:id) }).destroy_all
yield
end

View File

@@ -35,8 +35,8 @@ module Spree
taxons
.pluck('spree_taxons.id, enterprises.id AS enterprise_id')
.each_with_object({}) do |(taxon_id, enterprise_id), collection|
collection[enterprise_id.to_i] ||= Set.new
collection[enterprise_id.to_i] << taxon_id
collection[enterprise_id.to_i] ||= Set.new
collection[enterprise_id.to_i] << taxon_id
end
end

View File

@@ -167,8 +167,8 @@ module Spree
# In Rails 3, merging two scopes on the same column will consider only the last scope.
def self.in_distributor(distributor)
where(id: ExchangeVariant.select(:variant_id).
joins(:exchange).
where('exchanges.incoming = ? AND exchanges.receiver_id = ?', false, distributor))
joins(:exchange).
where('exchanges.incoming = ? AND exchanges.receiver_id = ?', false, distributor))
end
def self.indexed
@@ -179,11 +179,11 @@ module Spree
# "where(id:" is necessary so that the returned relation has no includes
# The relation without includes will not be readonly and allow updates on it
where(spree_variants: { id: joins(:prices).
where(deleted_at: nil).
where('spree_prices.currency' =>
where(deleted_at: nil).
where('spree_prices.currency' =>
currency || CurrentConfig.get(:currency)).
where.not(spree_prices: { amount: nil }).
select("spree_variants.id") })
where.not(spree_prices: { amount: nil }).
select("spree_variants.id") })
end
def self.linked_to(semantic_id)

View File

@@ -60,8 +60,8 @@ class ProductScopeQuery
def product_query_includes
[
{ image: { attachment_attachment: :blob },
variants: [:default_price, :stock_items, :variant_overrides] }
image: { attachment_attachment: :blob },
variants: [:default_price, :stock_items, :variant_overrides]
]
end

View File

@@ -42,10 +42,10 @@ class DfcCatalogImporter
.includes(:semantic_links).references(:semantic_links)
.where.not(semantic_links: { semantic_id: present_ids })
.select do |variant|
# Variants that were in the same catalog before:
variant.semantic_links.map(&:semantic_id).any? do |semantic_id|
FdcUrlBuilder.new(semantic_id).catalog_url == catalog_url
end
# Variants that were in the same catalog before:
variant.semantic_links.map(&:semantic_id).any? do |semantic_id|
FdcUrlBuilder.new(semantic_id).catalog_url == catalog_url
end
end
end
end

View File

@@ -51,8 +51,8 @@ class LineItemSyncer
def destroy_obsolete_items(order)
order.line_items.
where(variant_id: subscription_line_items.
select(&:marked_for_destruction?).
map(&:variant_id)).
select(&:marked_for_destruction?).
map(&:variant_id)).
destroy_all
end

View File

@@ -130,11 +130,12 @@ module Orders
def order_cycle_fees
return @order_cycle_fees if defined? @order_cycle_fees
return [] unless order_cycle && distributor
@order_cycle_fees = begin
fees = []
return fees unless order_cycle && distributor
order_cycle.exchanges.supplying_to(distributor).each do |exchange|
exchange.enterprise_fees.per_item.each do |enterprise_fee|
fee_value = FeeValue.new(fee: enterprise_fee, role: exchange.role)

View File

@@ -14,12 +14,12 @@ module PermittedAttributes
def self.attributes
basic_permitted_attributes + [
{ group_ids: [], user_ids: [],
shipping_method_ids: [], payment_method_ids: [],
address_attributes: PermittedAttributes::Address.attributes,
business_address_attributes: PermittedAttributes::BusinessAddress.attributes,
producer_properties_attributes: [:id, :property_name, :value, :_destroy],
custom_tab_attributes: PermittedAttributes::CustomTab.attributes },
group_ids: [], user_ids: [],
shipping_method_ids: [], payment_method_ids: [],
address_attributes: PermittedAttributes::Address.attributes,
business_address_attributes: PermittedAttributes::BusinessAddress.attributes,
producer_properties_attributes: [:id, :property_name, :value, :_destroy],
custom_tab_attributes: PermittedAttributes::CustomTab.attributes,
]
end

View File

@@ -25,8 +25,8 @@ module PermittedAttributes
private
def attributes
self.class.basic_attributes + [{ incoming_exchanges: permitted_exchange_attributes,
outgoing_exchanges: permitted_exchange_attributes }]
self.class.basic_attributes + [incoming_exchanges: permitted_exchange_attributes,
outgoing_exchanges: permitted_exchange_attributes]
end
def permitted_exchange_attributes

View File

@@ -26,11 +26,11 @@ module PermittedAttributes
def other_permitted_attributes
[
{ subscription_line_items_attributes: [
:id, :quantity, :variant_id, :price_estimate, :_destroy
],
bill_address_attributes: PermittedAttributes::Address.attributes,
ship_address_attributes: PermittedAttributes::Address.attributes }
subscription_line_items_attributes: [
:id, :quantity, :variant_id, :price_estimate, :_destroy
],
bill_address_attributes: PermittedAttributes::Address.attributes,
ship_address_attributes: PermittedAttributes::Address.attributes
]
end
end

View File

@@ -1,50 +1,23 @@
- search_url_query = {report_type: :enterprise_fee_summary, report_subtype: :enterprise_fees_with_tax_report_by_order}
.row
.alpha.two.columns= label_tag nil, t(:report_hubs)
.omega.fourteen.columns
= render(SearchableDropdownComponent.new(form: f,
name: :distributor_id_in,
options: [],
selected_option: params.dig(:q, :distributor_id_in),
multiple: true,
remote_url: admin_reports_search_distributors_url))
.omega.fourteen.columns= f.collection_select(:distributor_id_in, @data.distributors, :id, :name, {}, {class: "select2 fullwidth", multiple: true})
.row
.alpha.two.columns= label_tag nil, t(:report_customers_cycle)
.omega.fourteen.columns
= render(SearchableDropdownComponent.new(form: f,
name: :order_cycle_id_in,
options: [],
selected_option: params.dig(:q, :order_cycle_id_in),
multiple: true,
remote_url: admin_reports_search_order_cycles_url))
= f.select(:order_cycle_id_in, report_order_cycle_options(@data.order_cycles), {selected: params.dig(:q, :order_cycle_id_in)}, {class: "select2 fullwidth", multiple: true})
.row
.alpha.two.columns= label_tag nil, t(:fee_name)
.omega.fourteen.columns
= render(SearchableDropdownComponent.new(form: f,
name: :enterprise_fee_id_in,
options: [],
selected_option: params.dig(:q, :enterprise_fee_id_in),
multiple: true,
remote_url: admin_reports_search_enterprise_fees_url(search_url_query)))
= f.select(:enterprise_fee_id_in, fee_name_options(@report.search.result), {selected: params.dig(:q, :enterprise_fee_id_in)}, {class: "select2 fullwidth", multiple: true})
.row
.alpha.two.columns= label_tag nil, t(:fee_owner)
.omega.fourteen.columns
= render(SearchableDropdownComponent.new(form: f,
name: :enterprise_fee_owner_id_in,
options: [],
selected_option: params.dig(:q, :enterprise_fee_owner_id_in),
multiple: true,
remote_url: admin_reports_search_enterprise_fee_owners_url(search_url_query)))
= f.select(:enterprise_fee_owner_id_in, fee_owner_options(@report.search.result), {selected: params.dig(:q, :enterprise_fee_owner_id_in)}, {class: "select2 fullwidth", multiple: true})
.row
.alpha.two.columns= label_tag nil, t(:report_customers)
.omega.fourteen.columns
= render(SearchableDropdownComponent.new(form: f,
name: :customer_id_in,
options: [],
selected_option: params.dig(:q, :customer_id_in),
multiple: true,
remote_url: admin_reports_search_order_customers_url))
= f.select(:customer_id_in, customer_email_options(@data.order_customers), {selected: params.dig(:q, :customer_id_in)}, {class: "select2 fullwidth", multiple: true})

View File

@@ -1,57 +1,27 @@
- search_url_query = {report_type: :enterprise_fee_summary, report_subtype: :enterprise_fees_with_tax_report_by_producer}
.row
.alpha.two.columns= label_tag nil, t(:report_hubs)
.omega.fourteen.columns
= render(SearchableDropdownComponent.new(form: f,
name: :distributor_id_in,
options: [],
selected_option: params.dig(:q, :distributor_id_in),
multiple: true,
remote_url: admin_reports_search_distributors_url))
.omega.fourteen.columns= f.collection_select(:distributor_id_in, @data.distributors, :id, :name, {}, {class: "select2 fullwidth", multiple: true})
.row
.alpha.two.columns= label_tag nil, t(:report_producers)
.omega.fourteen.columns
= render(SearchableDropdownComponent.new(name: :supplier_id_in,
options: [],
selected_option: params.dig(:supplier_id_in),
multiple: true,
remote_url: admin_reports_search_suppliers_url))
.omega.fourteen.columns= select_tag(:supplier_id_in, options_from_collection_for_select(@data.orders_suppliers, :id, :name, params[:supplier_id_in]), {class: "select2 fullwidth", multiple: true})
.row
.alpha.two.columns= label_tag nil, t(:report_customers_cycle)
.omega.fourteen.columns
= render(SearchableDropdownComponent.new(form: f,
name: :order_cycle_id_in,
options: [],
selected_option: params.dig(:q, :order_cycle_id_in),
multiple: true,
remote_url: admin_reports_search_order_cycles_url))
= f.select(:order_cycle_id_in, report_order_cycle_options(@data.order_cycles), {selected: params.dig(:q, :order_cycle_id_in)}, {class: "select2 fullwidth", multiple: true})
.row
.alpha.two.columns= label_tag nil, t(:fee_name)
.omega.fourteen.columns
= render(SearchableDropdownComponent.new(form: f,
name: :enterprise_fee_id_in,
options: [],
selected_option: params.dig(:q, :enterprise_fee_id_in),
multiple: true,
remote_url: admin_reports_search_enterprise_fees_url(search_url_query)))
= f.select(:enterprise_fee_id_in, fee_name_options(@report.search.result), {selected: params.dig(:q, :enterprise_fee_id_in)}, {class: "select2 fullwidth", multiple: true})
.row
.alpha.two.columns= label_tag nil, t(:fee_owner)
.omega.fourteen.columns
= render(SearchableDropdownComponent.new(form: f,
name: :enterprise_fee_owner_id_in,
options: [],
selected_option: params.dig(:q, :enterprise_fee_owner_id_in),
multiple: true,
remote_url: admin_reports_search_enterprise_fee_owners_url(search_url_query)))
= f.select(:enterprise_fee_owner_id_in, fee_owner_options(@report.search.result), {selected: params.dig(:q, :enterprise_fee_owner_id_in)}, {class: "select2 fullwidth", multiple: true})
.row
.alpha.two.columns= label_tag nil, t(:report_customers)
.omega.fourteen.columns
= render(SearchableDropdownComponent.new(form: f,
name: :customer_id_in,
options: [],
selected_option: params.dig(:q, :customer_id_in),
multiple: true,
remote_url: admin_reports_search_order_customers_url))
= f.select(:customer_id_in, customer_email_options(@data.order_customers), {selected: params.dig(:q, :customer_id_in)}, {class: "select2 fullwidth", multiple: true})

View File

@@ -3,6 +3,6 @@
.alpha.four.columns
= label :payment_method, :type, t('.provider')
.omega.twelve.columns
= select(:payment_method, :type, payment_method_type_options(@providers), {}, { class: 'select2 fullwidth', required: true, placeholder: t("admin.choose"), 'provider-prefs-for' => "#{@object.id}"})
= collection_select(:payment_method, :type, @providers, :to_s, :clean_name, {}, { class: 'select2 fullwidth', 'provider-prefs-for' => "#{@object.id}"})
%div{"ng-include" => "include_html" }

View File

@@ -37,7 +37,7 @@
- method.distributors.each do |distributor|
= distributor.name
%br/
%td= payment_method_type_name(method.class.name)
%td= method.class.clean_name
- if spree_current_user.admin?
%td.align-center= method.environment.to_s.titleize
%td.align-center= method.display_on.blank? ? t('.both') : t('.' + method.display_on.to_s)

View File

@@ -1,16 +1,11 @@
import { Controller } from "stimulus";
import TomSelect from "tom-select/dist/esm/tom-select.complete";
import showHttpError from "../../webpacker/js/services/show_http_error";
export default class extends Controller {
static values = {
options: Object,
placeholder: String,
remoteUrl: String,
};
static values = { options: Object, placeholder: String };
connect(options = {}) {
let tomSelectOptions = {
this.control = new TomSelect(this.element, {
maxItems: 1,
maxOptions: null,
plugins: ["dropdown_input"],
@@ -21,13 +16,7 @@ export default class extends Controller {
},
...this.optionsValue,
...options,
};
if (this.remoteUrlValue) {
this.#addRemoteOptions(tomSelectOptions);
}
this.control = new TomSelect(this.element, tomSelectOptions);
});
}
disconnect() {
@@ -40,78 +29,4 @@ export default class extends Controller {
const optionsArray = [...this.element.options];
return optionsArray.find((option) => [null, ""].includes(option.value))?.text;
}
#buildUrl(query, page = 1) {
const url = new URL(this.remoteUrlValue, window.location.origin);
url.searchParams.set("q", query);
url.searchParams.set("page", page);
return url.toString();
}
#fetchOptions(query, callback) {
const url = this.control.getUrl(query);
fetch(url)
.then((response) => {
if (!response.ok) {
showHttpError(response.status);
throw response;
}
return response.json();
})
.then((json) => {
/**
* Expected API shape:
* {
* results: [{ value, label }],
* pagination: { more: boolean }
* }
*/
if (json.pagination?.more) {
const currentUrl = new URL(url);
const currentPage = parseInt(currentUrl.searchParams.get("page") || "1");
const nextUrl = this.#buildUrl(query, currentPage + 1);
this.control.setNextUrl(query, nextUrl);
}
callback(json.results || []);
})
.catch((error) => {
callback();
console.error(error);
});
}
#addRemoteOptions(options) {
this.openedByClick = false;
options.firstUrl = (query) => {
return this.#buildUrl(query, 1);
};
options.load = this.#fetchOptions.bind(this);
options.onFocus = function () {
this.control.load("", () => {});
}.bind(this);
options.onDropdownOpen = function () {
this.openedByClick = true;
}.bind(this);
options.onType = function () {
this.openedByClick = false;
}.bind(this);
// As per TomSelect source code, Loading state is shown on the UI when this function returns true.
// By default it shows loading state only when there is some input in the search box.
// We want to show loading state on focus as well (when there is no input) to indicate that options are being loaded.
options.shouldLoad = function (query) {
return this.openedByClick || query.length > 0;
}.bind(this);
options.valueField = "value";
options.labelField = "label";
options.searchField = "label";
}
}

View File

@@ -246,9 +246,6 @@ en_CA:
disconnect_failure: "Failed to disconnecct Stripe."
success_code:
disconnected: "Stripe account disconnected."
taler:
order_status:
claimed: "The payment request expired. Please try again."
activemodel:
errors:
messages:
@@ -3361,8 +3358,6 @@ en_CA:
payment_processing_failed: "Payment could not be processed, please check the details you entered."
payment_method_not_supported: "That payment method is unsupported. Please choose another one."
payment_updated: "Payment Updated"
payment_method_taler:
order_summary: "Open Food Network order"
cannot_perform_operation: "Could not update the payment"
action_required: "Action required"
tag_rules: "Tag Rules"
@@ -4176,7 +4171,6 @@ en_CA:
alt_text: "Alternative Text"
thumbnail: "Thumbnail"
back_to_images_list: "Back to Images List"
backend_url: "Backend URL"
api_key: "API key"
email: Email
account_updated: "Account updated!"
@@ -4496,7 +4490,6 @@ en_CA:
check: "Cash/EFT/Bank Transfer etc. (payments for which automatic validation is not required)"
paypalexpress: "PayPal Express"
stripesca: "Stripe SCA"
taler: "Taler"
payments:
source_forms:
stripe:

View File

@@ -115,67 +115,6 @@ en_GB:
blank: "can't be blank"
too_short: "is too short (minimum is %{count} characters)"
errors:
messages:
content_type_invalid:
one: "has an invalid content type (authorised content type is %{authorized_human_content_types})"
other: "has an invalid content type (authorised content types are\n%{authorized_human_content_types})"
content_type_spoofed:
one: "has a content type that is not equivalent to the one that is detected through its content (authorised content type is %{authorized_human_content_types})"
other: "has a content type that is not equivalent to the one that is detected through its content (authorised content types are %{authorized_human_content_types})"
file_size_not_less_than: "file size must be less than %{max} (current size is %{file_size})"
file_size_not_less_than_or_equal_to: "file size must be less than or equal to %{max} (current size is %{file_size})"
file_size_not_greater_than: "file size must be greater than %{min} (current size is %{file_size})"
file_size_not_greater_than_or_equal_to: "file size must be greater than or equal to %{min} (current size is %{file_size})"
file_size_not_between: "file size must be between %{min} and %{max} (current size is %{file_size})"
file_size_not_equal_to: "file size must be equal to %{exact} (current size is %{file_size})"
total_file_size_not_less_than: "total file size must be less than %{max} (current size is %{total_file_size})"
total_file_size_not_less_than_or_equal_to: "total file size must be less than or equal to %{max} (current size is %{total_file_size})"
total_file_size_not_greater_than: "total file size must be greater than %{min} (current size is %{total_file_size})"
total_file_size_not_greater_than_or_equal_to: "total file size must be greater than or equal to %{min} (current size is %{total_file_size})"
total_file_size_not_between: "total file size must be between %{min} and %{max} (current size is %{total_file_size})"
total_file_size_not_equal_to: "total file size must be equal to %{exact} (current size is %{total_file_size})"
duration_not_less_than: "duration must be less than %{max} (current duration is %{duration})"
duration_not_less_than_or_equal_to: "duration must be less than or equal to %{max} (current duration is %{duration})"
duration_not_greater_than: "duration must be greater than %{min} (current duration is %{duration})"
duration_not_greater_than_or_equal_to: "duration must be greater than or equal to %{min} (current duration is %{duration})"
duration_not_between: "duration must be between %{min} and %{max} (current duration is %{duration})"
duration_not_equal_to: "duration must be equal to %{exact} (current duration is %{duration})"
limit_out_of_range:
zero: "no files attached (must have between %{min} and %{max}files)"
one: "only 1 file attached (must have between %{min} and %{max}files)"
other: "total number of files must be between %{min} and %{max} files (there are %{count}files attached)"
limit_min_not_reached:
zero: "no files attached (must have at least %{min} files)"
one: "only 1 file attached (must have at least %{min} files)"
other: "%{count} files attached (must have at least %{min} files)"
limit_max_exceeded:
zero: "no files attached (maximum is %{max} files)"
one: "too many files attached (maximum is %{max} files, got %{count})"
other: "too many files attached (maximum is %{max} files, got %{count})"
attachment_missing: "is missing its attachment"
media_metadata_missing: "is not a valid media file"
dimension_min_not_included_in: "must be greater than or equal to %{width} x %{height} pixels"
dimension_max_not_included_in: "must be less than or equal to %{width} x %{height} pixels"
dimension_width_not_included_in: "width is not included between %{min} and %{max} pixels"
dimension_height_not_included_in: "height is not included between %{min} and %{max} pixels"
dimension_width_not_greater_than_or_equal_to: "width must be greater than or equal to %{length} pixels"
dimension_height_not_greater_than_or_equal_to: "height must be greater than or equal to %{length} pixels"
dimension_width_not_less_than_or_equal_to: "width must be less than or equal to %{length} pixels"
dimension_height_not_less_than_or_equal_to: "height must be less than or equal to %{length} pixels"
dimension_width_not_equal_to: "width must be equal to %{length} pixels"
dimension_height_not_equal_to: "height must be equal to %{length} pixels"
aspect_ratio_not_square: "must be square (current file is %{width}x%{height}px)"
aspect_ratio_not_portrait: "must be portrait (current file is %{width}x%{height}px)"
aspect_ratio_not_landscape: "must be landscape (current file is %{width}x%{height}px)"
aspect_ratio_not_x_y: "must be %{authorized_aspect_ratios} (current file is %{width}x%{height}px)"
aspect_ratio_invalid: "has an invalid aspect ratio (valid aspect ratios are %{authorized_aspect_ratios})"
file_not_processable: "is not identified as a valid media file"
pages_not_less_than: "page count must be less than %{max} (current page count is %{pages})"
pages_not_less_than_or_equal_to: "page count must be less than or equal to %{max} (current page count is %{pages})"
pages_not_greater_than: "page count must be greater than %{min} (current page count is %{pages})"
pages_not_greater_than_or_equal_to: "page count must be greater than or equal to %{min} (current page count is %{pages})"
pages_not_between: "page count must be between %{min} and %{max} (current page count is %{pages})"
pages_not_equal_to: "page count must be equal to %{exact} (current page count is %{pages})"
not_found:
title: "The page you were looking for doesn't exist (404)"
message_html: "<b>Please try again</b> <p>This might be a temporary problem. Please click the back button to return to the previous screen or go back to <a href='/'>Home</a> and try again.</p> <b>Contact support</b> <p>If the problem persists or is urgent, please tell us about it. Find our contact details from the global <a href='https://openfoodnetwork.org/ofn-local/' target='blank'>Open Food Network Local page</a>.</p> <p>It really helps us if you can give as much detail as possible about what the missing page is about.</p>"
@@ -246,9 +185,6 @@ en_GB:
disconnect_failure: "Failed to disconnect Stripe."
success_code:
disconnected: "Stripe account disconnected."
taler:
order_status:
claimed: "The payment request expired. Please try again."
activemodel:
errors:
messages:
@@ -584,7 +520,6 @@ en_GB:
errors:
vine_api: "There was an error communicating with the API, please try again later."
invalid_voucher: "The voucher is not valid"
expired: "The voucher has expired"
not_found_voucher: "Sorry, we couldn't find that voucher, please check the code."
vine_voucher_redeemer_service:
errors:
@@ -999,10 +934,6 @@ en_GB:
clone:
success: Successfully cloned the product
error: Unable to clone the product
tag_rules:
rules_per_tag:
one: "%{tag} has 1 rule"
other: "%{tag} has %{count} rules"
product_import:
title: Product Import
file_not_found: File not found or could not be opened
@@ -2443,9 +2374,9 @@ en_GB:
order_includes_tax: (includes tax)
order_payment_paypal_successful: Your payment via PayPal has been processed successfully.
order_hub_info: Hub info
order_back_to_store: Back to shop
order_back_to_cart: Back to cart
order_back_to_website: Back to website
order_back_to_store: Back To Store
order_back_to_cart: Back To Cart
order_back_to_website: Back To Website
checkout_details_title: Checkout Details
checkout_payment_title: Checkout Payment
checkout_summary_title: Checkout Summary
@@ -3361,13 +3292,11 @@ en_GB:
payment_processing_failed: "Payment could not be processed, please check the details you entered"
payment_method_not_supported: "That payment method is unsupported. Please choose another one."
payment_updated: "Payment Updated"
payment_method_taler:
order_summary: "Open Food Network order"
cannot_perform_operation: "Could not update the payment"
action_required: "Action required"
tag_rules: "Tag Rules"
enterprise_fee_whole_order: Whole order
enterprise_fee_by_name: "%{name} fee by %{role} %{enterprise_name}"
enterprise_fee_by_name: "%{name} fee by %{role}%{enterprise_name}"
validation_msg_relationship_already_established: "^That relationship is already established."
validation_msg_at_least_one_hub: "^At least one hub must be selected"
validation_msg_tax_category_cant_be_blank: "^Tax Category can't be blank"
@@ -3408,7 +3337,6 @@ en_GB:
order_cycles_no_permission_to_coordinate_error: "None of your enterprises have permission to coordinate an order cycle"
order_cycles_no_permission_to_create_error: "You don't have permission to create an order cycle coordinated by that enterprise"
order_cycle_closed: "The order cycle you've selected has just closed."
order_cycle_closed_next_steps: "The order cycle you've selected has just closed. Please contact us to complete your order #\n%{order_number}!"
back_to_orders_list: "Back To Orders List"
no_orders_found: "No Orders Found"
order_information: "Order Information"
@@ -3983,8 +3911,6 @@ en_GB:
destroy:
success: Webhook endpoint successfully deleted
error: Webhook endpoint failed to delete
test:
success: Some test data will be sent to the webhook url
spree:
order_updated: "Order Updated"
cannot_perform_operation: "Can not perform this operation"
@@ -4091,7 +4017,6 @@ en_GB:
logourl: "Logo url"
are_you_sure_delete: "Are you sure you want to delete this record?"
confirm_delete: "Confirm Deletion"
tag_rule: "Tag Rule"
voucher: "Voucher"
configurations: "Configurations"
general_settings: "General Settings"
@@ -4183,7 +4108,6 @@ en_GB:
alt_text: "Alternative Text"
thumbnail: "Thumbnail"
back_to_images_list: "Back To Images List"
backend_url: "Backend URL"
api_key: "API key"
email: Email
account_updated: "Account updated!"
@@ -4503,7 +4427,6 @@ en_GB:
check: "Cash/EFT/etc. (payments for which automatic validation is not required)"
paypalexpress: "PayPal Express"
stripesca: "Stripe SCA "
taler: "Taler"
payments:
source_forms:
stripe:
@@ -4960,7 +4883,6 @@ en_GB:
order_cycle_tagged_bottom: "are:"
inventory_tagged_top: "Inventory variants tagged"
inventory_tagged_bottom: "are:"
variant_tagged_top: "Variants tagged"
variant_tagged_bottom: "are:"
visible: VISIBLE
not_visible: NOT VISIBLE
@@ -4976,8 +4898,6 @@ en_GB:
create_placeholder: Enter the URL of the remote webhook endpoint
event_types:
order_cycle_opened: Order Cycle Opened
payment_status_changed: Post webhook on Payment status change
test_endpoint: Test webhook endpoint
invisible_captcha:
sentence_for_humans: "Please leave empty"
timestamp_error_message: "Please try again after 5 seconds."

View File

@@ -18,7 +18,7 @@ fi:
phone: "Puhelinnumero"
firstname: "Etunimi"
lastname: "Sukunimi"
zipcode: "Toimitusosoitteen postinumero"
zipcode: "Toimituspostinumero"
spree/order/bill_address:
address1: "Laskutusosoite (Katu ja talonumero)"
zipcode: "Laskutuspostinumero"
@@ -115,67 +115,6 @@ fi:
blank: "ei voi olla tyhjä"
too_short: "on liian lyhyt (vähintään %{count} merkkiä)"
errors:
messages:
content_type_invalid:
one: "sisältää virheellisen sisältötyypin (valtuutettu sisältötyyppi on %{authorized_human_content_types} )"
other: "sisältää virheellisen sisältötyypin (valtuutetut sisältötyypit ovat %{authorized_human_content_types} )"
content_type_spoofed:
one: "sisältötyyppi ei vastaa sisällöstä havaittua tyyppiä (valtuutettu sisältötyyppi on %{authorized_human_content_types} )"
other: "sisältötyyppi ei vastaa sisällöstä havaittua tyyppiä (valtuutetut sisältötyypit ovat %{authorized_human_content_types} )"
file_size_not_less_than: "tiedoston koko on oltava pienempi kuin %{max} (nykyinen koko on %{file_size} )"
file_size_not_less_than_or_equal_to: "tiedoston koko on oltava pienempi tai yhtä suuri kuin %{max} (nykyinen koko on %{file_size} )"
file_size_not_greater_than: "tiedoston koko on oltava suurempi kuin %{min} (nykyinen koko on %{file_size} )"
file_size_not_greater_than_or_equal_to: "tiedoston koko on oltava suurempi tai yhtä suuri kuin %{min} (nykyinen koko on %{file_size} )"
file_size_not_between: "koko on oltava %{min} ja %{max} välillä (nykyinen koko on %{file_size} )"
file_size_not_equal_to: "tiedoston koko on oltava yhtä suuri kuin %{exact} (nykyinen koko on %{file_size} )"
total_file_size_not_less_than: "yhteensä tiedoston koko on oltava pienempi kuin %{max} (nykyinen koko on %{total_file_size} )"
total_file_size_not_less_than_or_equal_to: "yhteensä tiedoston koko on oltava pienempi tai yhtä suuri kuin %{max} (nykyinen koko on %{total_file_size} )"
total_file_size_not_greater_than: "yhteensä tiedoston koko on oltava suurempi kuin %{min} (nykyinen koko on %{total_file_size} )"
total_file_size_not_greater_than_or_equal_to: "yhteensä tiedoston koko on oltava suurempi tai yhtä suuri kuin %{min} (nykyinen koko on %{total_file_size} )"
total_file_size_not_between: "yhteensä tiedoston koko on oltava %{min} ja %{max} välillä (nykyinen koko on %{total_file_size} )"
total_file_size_not_equal_to: "yhteensä tiedoston koko on oltava yhtä suuri kuin %{exact} (nykyinen koko on %{total_file_size} )"
duration_not_less_than: "keston on oltava pienempi kuin %{max} (nykyinen kesto on %{duration} )"
duration_not_less_than_or_equal_to: "keston on oltava pienempi tai yhtä suuri kuin %{max} (nykyinen kesto on %{duration} )"
duration_not_greater_than: "keston on oltava suurempi kuin %{min} (nykyinen kesto on %{duration} )"
duration_not_greater_than_or_equal_to: "keston on oltava suurempi tai yhtä suuri kuin %{min} (nykyinen kesto on %{duration} )"
duration_not_between: "keston on oltava välillä %{min} ja %{max} (nykyinen kesto on %{duration} )"
duration_not_equal_to: "keston on oltava yhtä suuri kuin %{exact} (nykyinen kesto on %{duration} )"
limit_out_of_range:
zero: "ei liitettyjä tiedostoja (tiedostojen on oltava %{min} ja %{max} välillä)"
one: "vain yksi liitetty tiedosto (tiedostojen on oltava %{min} ja %{max} välillä)"
other: "Tiedostojen yhteensä on oltava %{min} ja %{max} välillä (liitteenä on %{count} tiedostot)"
limit_min_not_reached:
zero: "ei liitettyjä tiedostoja (tiedostoissa on oltava vähintään %{min} )"
one: "vain yksi tiedosto liitettynä (tiedostojen on oltava vähintään %{min} )"
other: "%{count} tiedostot liitteenä (vähintään %{min} tiedostoja on oltava)"
limit_max_exceeded:
zero: "ei liitettyjä tiedostoja (enintään %{max} tiedostoja)"
one: "liian monta tiedostoa liitettynä (suurin sallittu määrä on %{max} , sain tulokseksi %{count} )"
other: "liian monta tiedostoa liitettynä (suurin sallittu määrä on %{max} , sain tulokseksi %{count} )"
attachment_missing: "puuttuu sen liite"
media_metadata_missing: "ei ole kelvollinen mediatiedosto"
dimension_min_not_included_in: "on oltava suurempi tai yhtä suuri kuin %{width} x %{height} pikseliä"
dimension_max_not_included_in: "on oltava pienempi tai yhtä suuri kuin %{width} x %{height} pikseliä"
dimension_width_not_included_in: "Leveys ei sisälly %{min} ja %{max} pikselien väliin"
dimension_height_not_included_in: "Korkeutta ei ole %{min} ja %{max} pikselien välillä."
dimension_width_not_greater_than_or_equal_to: "leveyden on oltava suurempi tai yhtä suuri kuin %{length} pikseliä"
dimension_height_not_greater_than_or_equal_to: "korkeuden on oltava suurempi tai yhtä suuri kuin %{length} pikseliä"
dimension_width_not_less_than_or_equal_to: "leveyden on oltava pienempi tai yhtä suuri kuin %{length} pikseliä"
dimension_height_not_less_than_or_equal_to: "korkeuden on oltava pienempi tai yhtä suuri kuin %{length} pikseliä"
dimension_width_not_equal_to: "leveyden on oltava yhtä suuri kuin %{length} pikseliä"
dimension_height_not_equal_to: "korkeuden on oltava yhtä suuri kuin %{length} pikselit"
aspect_ratio_not_square: "on oltava neliön muotoinen (nykyinen tiedosto on %{width} x %{height} px)"
aspect_ratio_not_portrait: "tiedoston on oltava pystysuuntainen (nykyinen tiedosto on %{width} x %{height} px)"
aspect_ratio_not_landscape: "tiedoston on oltava vaakasuuntainen (nykyinen tiedosto on %{width} x %{height} px)"
aspect_ratio_not_x_y: "tiedoston on oltava %{authorized_aspect_ratios} (nykyinen tiedosto on %{width} x %{height} px)"
aspect_ratio_invalid: "kuvasuhde on virheellinen (kelvolliset kuvasuhteet ovat %{authorized_aspect_ratios} )"
file_not_processable: "ei ole tunnistettu kelvolliseksi mediatiedostoksi"
pages_not_less_than: "sivumäärän on oltava pienempi kuin %{max} (nykyinen sivumäärä on %{pages} )"
pages_not_less_than_or_equal_to: "sivumäärän on oltava pienempi tai yhtä suuri kuin %{max} (nykyinen sivumäärä on %{pages} )"
pages_not_greater_than: "sivumäärän on oltava suurempi kuin %{min} (nykyinen sivumäärä on %{pages} )"
pages_not_greater_than_or_equal_to: "sivumäärän on oltava suurempi tai yhtä suuri kuin %{min} (nykyinen sivumäärä on %{pages} )"
pages_not_between: "sivumäärän on oltava välillä %{min} ja %{max} (nykyinen sivumäärä on %{pages} )"
pages_not_equal_to: "sivumäärän on oltava yhtä suuri kuin %{exact} (nykyinen sivumäärä on %{pages} )"
not_found:
title: "Etsimääsi sivua ei löytynyt (404)"
message_html: "<b>Yritä uudelleen</b> <p>Tämä voi olla tilapäinen ongelma. Palaa edelliselle sivulle tai <a href='/'>etusivulle</a> ja yritä uudelleen.</p> <b>Ota yhteyttä tukeen</b> <p>Jos ongelma jatkuu tai on kiireellinen, kerro siitä meille. Löydä yhteystiedot globaalilta <a href='https://openfoodnetwork.org/ofn-local/' target='blank'>Open Food Network Local -sivulta</a>.</p> <p>Autat meitä paljon, jos annat mahdollisimman paljon yksityiskohtia puuttuvasta sivusta.</p>"
@@ -246,9 +185,6 @@ fi:
disconnect_failure: "Stripe-tilin irrottaminen epäonnistui."
success_code:
disconnected: "Stripe-tili irrotettu."
taler:
order_status:
claimed: "Maksupyyntö vanheni. Yritä uudelleen."
activemodel:
errors:
messages:
@@ -582,7 +518,6 @@ fi:
errors:
vine_api: "API-yhteydessä tapahtui virhe, yritä myöhemmin uudelleen."
invalid_voucher: "Alennuskuponki ei ole kelvollinen"
expired: "Kuponki on vanhentunut"
not_found_voucher: "Anteeksi, emme löytäneet tätä alennuskuponkia, tarkista koodi."
vine_voucher_redeemer_service:
errors:
@@ -997,10 +932,6 @@ fi:
clone:
success: Tuote kloonattiin onnistuneesti
error: Tuotteen kloonaaminen epäonnistui
tag_rules:
rules_per_tag:
one: "%{tag} llä on 1 sääntö"
other: "%{tag} llä on %{count} säännöt."
product_import:
title: Tuontituotteet
file_not_found: Tiedostoa ei löytynyt tai sitä ei voitu avata
@@ -1395,9 +1326,6 @@ fi:
add_new_button: '+ Lisää uusi oletussääntö'
no_tags_yet: Tähän yritykseen ei vielä liity tägejä
add_new_tag: '+ Lisää uusi tägi'
show_hide_variants: 'Näytä tai Piilota variantit myymälästäni'
show_hide_shipping: 'Näytä tai piilota toimitustavat kassalla'
show_hide_payment: 'Näytä tai Piilota maksutavat kassalla'
show_hide_order_cycles: 'Näytä tai piilota tilausjaksot verkkokaupassani'
users:
legend: "Käyttäjät"
@@ -1551,10 +1479,6 @@ fi:
invite_manager:
user_already_exists: "Käyttäjä on jo olemassa"
error: "Jotain meni pieleen"
tag_rules:
not_supported_type: tägi -sääntötyyppiä ei tueta
confirm_delete: Haluatko varmasti poistaa tämän säännön?
destroy_error: tägi -säännön poistamisessa oli ongelma.
order_cycles:
loading_flash:
loading_order_cycles: LADATAAN TILAUSJAKSOJA
@@ -1778,8 +1702,6 @@ fi:
not_visible: "%{enterprise} ei ole näkyvissä, joten sitä ei löydy kartalta tai hauista"
reports:
none: ei yhtään
metadata:
report_title: Ilmianna otsikko
deprecated: "Tämä raportti on vanhentunut ja se poistetaan tulevasta julkaisusta."
hidden_field: "< Piilotettu >"
unitsize: YKSIKKÖKOKO
@@ -1882,7 +1804,6 @@ fi:
display: Näyttö
summary_row: Yhteenvetorivi
header_row: Otsikkorivi
metadata_rows: Metadata-rivit
raw_data: Raakadata
formatted_data: Muotoiltu data
packing:
@@ -2458,15 +2379,8 @@ fi:
email_confirmed: "Kiitos sähköpostiosoitteesi vahvistamisesta."
email_confirmation_activate_account: "Ennen kuin voimme aktivoida uuden tilisi, meidän on vahvistettava sähköpostiosoitteesi."
email_confirmation_greeting: "Hei, %{contact} !"
email_confirmation_profile_created: >
Profiili käyttäjälle %{name} on luotu onnistuneesti! Profiilisi aktivoimiseksi
meidän on vahvistettava tämä sähköpostiosoite.
email_confirmation_click_link: "Vahvista sähköpostiosoitteesi ja jatka profiilisi luomista napsauttamalla alla olevaa linkkiä."
email_confirmation_link_label: "Vahvista tämä sähköpostiosoite »"
email_confirmation_help_html: >
Kun olet vahvistanut sähköpostiosoitteesi, voit käyttää ylläpitäjän tiliäsi
tässä yrityksessä. Katso %{link} saadaksesi about %{sitename} :n ominaisuuksista
ja aloittaaksesi profiilisi tai verkkokauppasi käytön.
email_confirmation_notice_unexpected: "Sait tämän viestin, koska rekisteröidyit palvelussa %{sitename} tai joku luultavasti tuntemasi henkilö kutsui sinut rekisteröitymään. Jos et ymmärrä, miksi saat tämän sähköpostin, kirjoita osoitteeseen %{contact} ."
email_social: "Ota yhteyttä:"
email_contact: "Lähetä meille sähköpostia:"
@@ -3362,8 +3276,6 @@ fi:
payment_processing_failed: "Payment could not be processed, please check the details you entered"
payment_method_not_supported: "Maksutapaa ei tueta. Valitse toinen."
payment_updated: "Maksu päivitetty"
payment_method_taler:
order_summary: "Open Food Network tilaus"
cannot_perform_operation: "Maksua ei voitu päivittää"
action_required: "Toimenpide vaaditaan"
tag_rules: "Tägisäännöt"
@@ -3409,7 +3321,6 @@ fi:
order_cycles_no_permission_to_coordinate_error: "Yhdelläkään yritykselläsi ei ole lupaa koordinoida tilauskiertoa."
order_cycles_no_permission_to_create_error: "Sinulla ei ole oikeutta luoda kyseisen yrityksen koordinoimaa tilausjaksoa."
order_cycle_closed: "Valitsemasi tilausjakso on juuri päättynyt. Yritä uudelleen!"
order_cycle_closed_next_steps: "Valitsemasi tilausjakso on juuri sulkeutunut. Ota meihin yhteyttä täydentääksesi tilaustasi# %{order_number} !"
back_to_orders_list: "Takaisin tilauslistaan"
no_orders_found: "Ei tilaukset löytynyt"
order_information: "Tilaustiedot"
@@ -3977,8 +3888,6 @@ fi:
destroy:
success: Webhook-päätepiste poistettu onnistuneesti
error: Webhook-päätepisteen poistaminen epäonnistui
test:
success: Joitakin testitietoja lähetetään webhookin URL-osoitteeseen
spree:
order_updated: "Tilaus päivitetty"
cannot_perform_operation: "Tätä toimintoa ei voida suorittaa"
@@ -4085,7 +3994,6 @@ fi:
logourl: "Logourl"
are_you_sure_delete: "Haluatko varmasti poistaa tämän tietueen?"
confirm_delete: "Vahvista poisto"
tag_rule: "tägi Rule"
voucher: "Alennuskuponki"
configurations: "Asetukset"
general_settings: "Yleiset asetukset"
@@ -4177,8 +4085,6 @@ fi:
alt_text: "Vaihtoehtoinen teksti"
thumbnail: "Pienoiskuva"
back_to_images_list: "Takaisin kuvaluetteloon"
backend_url: "Taustapalvelun URL-osoite"
api_key: "API-avain"
email: Sähköposti
account_updated: "Tili päivitetty!"
email_updated: "Tili päivitetään, kun uusi sähköpostiosoite on vahvistettu."
@@ -4497,7 +4403,6 @@ fi:
check: "Käteinen/sähköinen maksu/jne. (maksut, joille ei vaadita automaattista vahvistusta)"
paypalexpress: "PayPal Express"
stripesca: "Stripe SCA"
taler: "Taleri"
payments:
source_forms:
stripe:
@@ -4674,8 +4579,8 @@ fi:
or_enter_new_card: "Tai anna uuden kortin tiedot:"
remember_this_card: Muistatko tämän kortin?
date_picker:
flatpickr_date_format: "Vuosi"
flatpickr_datetime_format: "Vuosi H:i"
flatpickr_date_format: "d.m.Y"
flatpickr_datetime_format: "d.m.Y H:i"
today: "Tänään"
now: "Nyt"
close: "Sulje"
@@ -4947,31 +4852,13 @@ fi:
tag_rule_form:
tag_rules:
shipping_method_tagged_top: "Toimitustavat merkitty tägillä"
shipping_method_tagged_bottom: "ovat:"
payment_method_tagged_top: "Maksutavat merkitty tägillä"
payment_method_tagged_bottom: "ovat:"
order_cycle_tagged_top: "Tilausjaksot merkitty tägillä"
order_cycle_tagged_bottom: "ovat:"
inventory_tagged_top: "Tägätyt varastomuunnelmat"
inventory_tagged_bottom: "ovat:"
variant_tagged_top: "Variantit -tunnisteella"
variant_tagged_bottom: "ovat:"
visible: NÄKYVÄ
not_visible: EI NÄKYVÄ
tag_rule_group_form:
for_customers_tagged: 'Asiakkaille, jotka on tägätty:'
add_new_rule: '+ Lisää uusi sääntö'
no_rules_yet: Tähän tägiin ei vielä sovelleta sääntöjä
add_tag_rule_modal:
select_rule_type: "Valitse säännön tyyppi:"
add_rule: "lisää sääntö"
webhook_endpoint_form:
url:
create_placeholder: Anna etäwebhook-päätepisteen URL-osoite
event_types:
order_cycle_opened: Tilausjakso avattu
payment_status_changed: Lähetä webhook maksun tilan muutoksesta
test_endpoint: Testaa webhookin päätepiste
invisible_captcha:
sentence_for_humans: "Jätä tyhjäksi"
timestamp_error_message: "Yritä uudelleen 5 sekunnin kuluttua."

View File

@@ -2945,7 +2945,7 @@ fr:
shipping_method_destroy_error: "Cette méthode de livraison ne peut pas être supprimée car elle est référencée dans une commande : %{number}."
fees: "Frais"
fee_name: "Nom de la marge/commission"
fee_owner: "Propriétaire de la marge/commission"
fee_owner: "Propriétaire des droits"
item_cost: "Coût du produit"
bulk: "Vrac"
shop_variant_quantity_min: "min"

View File

@@ -115,69 +115,6 @@ fr_CA:
blank: "Champ obligatoire"
too_short: "est trop court (minimum %{count} caractère)"
errors:
messages:
content_type_invalid:
one: "a un contenu invalide (le type de contenu autorisé est %{authorized_human_content_types})"
many: "a un contenu invalide (les types de contenus autorisés sont %{authorized_human_content_types})"
other: "a un contenu invalide (les types de contenus autorisés sont %{authorized_human_content_types})"
content_type_spoofed:
one: "a un type de contenu qui n'est pas équivalent avec celui détecté (le type de contenu autorisé est %{authorized_human_content_types})"
many: "a un type de contenu qui n'est pas équivalent avec celui détecté (les types de contenus autorisés sont %{authorized_human_content_types})"
other: "a un type de contenu qui n'est pas équivalent avec celui détecté (les types de contenus autorisés sont %{authorized_human_content_types})"
file_size_not_less_than: "la taille du fichier doit être inférieure à %{max} (la taille actuelle est %{file_size})"
file_size_not_less_than_or_equal_to: "la taille du fichier doit être inférieure ou égale à %{max} (la taille actuelle est %{file_size})"
file_size_not_greater_than: "la taille du fichier doit être supérieure à %{min} (la taille actuelle est%{file_size})"
file_size_not_greater_than_or_equal_to: "la taille du fichier doit être supérieure ou égale à %{min} (la taille actuelle est %{file_size})"
file_size_not_between: "la taille du fichier doit être comprise entre%{min} et %{max} (la taille actuelle est %{file_size})"
file_size_not_equal_to: "la taille du fichier doit être égale à %{exact} (la taille actuelle est %{file_size})"
total_file_size_not_less_than: "La taille totale du fichier doit être inférieure à %{max} (la taille actuelle est%{total_file_size})"
total_file_size_not_less_than_or_equal_to: "La taille totale du fichier doit être inférieure ou égale à %{max} (la taille actuelle est %{total_file_size})"
total_file_size_not_greater_than: "La taille totale du fichier doit être supérieure à %{min} (la taille actuelle est%{total_file_size})"
total_file_size_not_greater_than_or_equal_to: "La taille totale du fichier doit être supérieure ou égale à %{min} (la taille actuelle est %{total_file_size})"
total_file_size_not_between: "La taille totale du fichier doit être comprise entre %{min} et %{max} (la taille actuelle est %{total_file_size})"
total_file_size_not_equal_to: "La taille totale du fichier doit être égale à %{exact} (la taille actuelle est%{total_file_size})"
duration_not_less_than: "La durée doit être inférieure à %{max} (la durée actuelle est %{duration})"
duration_not_less_than_or_equal_to: "La durée doit être inférieure ou égale à %{max} (la durée actuelle est%{duration})"
duration_not_greater_than: "La durée doit être supérieure à %{min} (la durée actuelle est %{duration})"
duration_not_greater_than_or_equal_to: "La durée doit être supérieure ou égale à %{min} (la durée actuelle est%{duration})"
duration_not_between: "La durée doit être comprise entre %{min} et %{max} (la durée actuelle est %{duration})"
duration_not_equal_to: "La durée doit être égale à %{exact} (la durée actuelle est %{duration})"
limit_out_of_range:
zero: "Aucun fichier joint (doit contenir entre %{min} et %{max} fichiers)"
one: "Seulement 1 fichier joint (doit contenir entre%{min} et %{max} fichiers)"
other: "Le nombre total de fichiers doit être compris entre %{min} et %{max} fichiers (il y a %{count} fichiers joints)"
limit_min_not_reached:
zero: "Aucun fichier joint (doit contenir au moins %{min} fichiers)"
one: "Seulement 1 fichier joint (doit contenir au moins %{min} fichiers)"
other: "%{count} Aucun fichier joint (doit contenir au moins %{min} fichiers)"
limit_max_exceeded:
zero: "Aucun fichier joint (au maximum %{max} fichiers)"
one: "Trop de fichiers joints (le maximum est %{max} fichiers, il y en a %{count})"
other: "Trop de fichiers joints (le maximum est %{max} fichiers, il y en a%{count})"
attachment_missing: "une pièce jointe est manquante"
media_metadata_missing: "n'est pas un fichier multimédia valide"
dimension_min_not_included_in: "doit être plus grand ou égal à %{width} x %{height} pixels"
dimension_max_not_included_in: "doit être plus petit ou égal à %{width} x %{height} pixels"
dimension_width_not_included_in: "la largeur n'est pas comprise entre %{min} et %{max} pixels"
dimension_height_not_included_in: "la hauteur n'est pas comprise entre %{min} et %{max} pixels"
dimension_width_not_greater_than_or_equal_to: "la largeur doit être supérieure ou égale à %{length} pixels"
dimension_height_not_greater_than_or_equal_to: "la hauteur doit être supérieure ou égale à %{length} pixels"
dimension_width_not_less_than_or_equal_to: "la largeur doit être inférieure ou égale à %{length} pixels"
dimension_height_not_less_than_or_equal_to: "la hauteur doit être inférieure ou égale à %{length} pixels"
dimension_width_not_equal_to: "la largeur doit être égale à %{length} pixels"
dimension_height_not_equal_to: "La hauteur doit être égale à %{length} pixels"
aspect_ratio_not_square: "doit être carré (le fichier actuel est %{width}x%{height}px)"
aspect_ratio_not_portrait: "doit être au format portrait (le fichier actuel mesure %{width}x%{height}px)"
aspect_ratio_not_landscape: "doit être au format paysage (le fichier actuel mesure %{width}x%{height}px)"
aspect_ratio_not_x_y: "doit être %{authorized_aspect_ratios} (le fichier actuel est %{width}x%{height}px)"
aspect_ratio_invalid: "possède un ratio d'image invalide (les ratios d'image valides sont %{authorized_aspect_ratios})"
file_not_processable: "n'est pas identifié comme un fichier multimédia valide"
pages_not_less_than: "La durée doit être inférieure à %{max} (la durée actuelle est %{pages})"
pages_not_less_than_or_equal_to: "La durée doit être inférieure ou égale à %{max} (la durée actuelle est %{pages})"
pages_not_greater_than: "La durée doit être supérieure à %{min}(la durée actuelle est %{pages})"
pages_not_greater_than_or_equal_to: "La durée doit être supérieure ou égale à %{min} (la durée actuelle %{pages})"
pages_not_between: "La durée doit être comprise entre %{min} et %{max} (la durée actuelle est %{pages})"
pages_not_equal_to: "La durée doit être égale à %{exact} (la durée actuelle est %{pages})"
not_found:
title: "La page que vous recherchez n'existe pas (erreur 404)"
message_html: "<b>Veuillez essayer à nouveau </b> <p> Il s'agit peut-être d'un problème temporaire. Veuillez cliquer sur le bouton retour de votre navigateur ou retourner à l'<a href='/'> Accueil </a> et recommencez. <b> Contacter notre support  </b> <p> Si le problème persiste ou si c'est urgent, veuillez nous contacter. </p>"
@@ -248,9 +185,6 @@ fr_CA:
disconnect_failure: "Déconnecter Stripe a échoué."
success_code:
disconnected: "Le compte Stripe est déconnecté."
taler:
order_status:
claimed: "La demande de paiement a expiré. Merci d'essayer à nouveau."
activemodel:
errors:
messages:
@@ -586,7 +520,6 @@ fr_CA:
errors:
vine_api: "There was an error communicating with the API, please try again later."
invalid_voucher: "The voucher is not valid"
expired: "Le bon de réduction a expiré."
not_found_voucher: "Désolé, nous n'avons pas trouvé ce bon de réduction. Merci de vérifier le code qui vous a été transmis."
vine_voucher_redeemer_service:
errors:
@@ -3372,8 +3305,6 @@ fr_CA:
payment_processing_failed: "Le paiement n'a pas pu être traité, veuillez vérifier les informations saisies"
payment_method_not_supported: "Cette méthode de paiement n'est pas maintenue. Veuillez en sélectionner une autre."
payment_updated: "Paiement mis à jour"
payment_method_taler:
order_summary: "Commande Open Food Network"
cannot_perform_operation: "Le paiement n'a pas pu être mis à jour."
action_required: "Une action est requise"
tag_rules: "Règles de tag"
@@ -4034,8 +3965,6 @@ fr_CA:
destroy:
success: Le webhook a bien été supprimé.
error: Le webhook n'a pas pu être supprimé.
test:
success: Des données test vont être envoyées à l'URL du webhook
spree:
order_updated: "La commande a été mise à jour"
cannot_perform_operation: "Cette opération ne peut pas être réalisée"
@@ -4142,7 +4071,6 @@ fr_CA:
logourl: "URL du logo"
are_you_sure_delete: "Etes-vous certain de vouloir supprimer cet élément ?"
confirm_delete: "Confirmer la suppression"
tag_rule: "Règle de tag"
voucher: "Bon de réduction"
configurations: "Configurations"
general_settings: "Configurations générales"
@@ -4234,7 +4162,6 @@ fr_CA:
alt_text: "Texte alternatif"
thumbnail: "Miniature"
back_to_images_list: "Retour à la liste des images"
backend_url: "URL du backend"
api_key: "Clé API"
email: Email
account_updated: "Compte mis à jour!"
@@ -4554,7 +4481,6 @@ fr_CA:
check: "Espèces / chèques / virements / autres "
paypalexpress: "PayPal Express"
stripesca: "Stripe SCA"
taler: "Taler"
payments:
source_forms:
stripe:
@@ -5041,8 +4967,6 @@ fr_CA:
create_placeholder: Entrez l'URL du point de terminaison du webhook
event_types:
order_cycle_opened: Cycle de vente ouvert
payment_status_changed: Poster webhook sur le changement du statut de paiement
test_endpoint: Tester le point de terminaison du webhook
invisible_captcha:
sentence_for_humans: "Merci de laisser ce champ libre"
timestamp_error_message: "S'il vous plaît réessayez après 5 secondes."

View File

@@ -136,15 +136,7 @@ Openfoodnetwork::Application.routes.draw do
put :resume, on: :member, format: :json
end
scope :reports, as: :reports do
get '/', to: 'reports#index'
get '/search_enterprise_fees', to: 'reports#search_enterprise_fees', as: :search_enterprise_fees
get '/search_enterprise_fee_owners', to: 'reports#search_enterprise_fee_owners', as: :search_enterprise_fee_owners
get '/search_distributors', to: 'reports#search_distributors', as: :search_distributors
get '/search_suppliers', to: 'reports#search_suppliers', as: :search_suppliers
get '/search_order_cycles', to: 'reports#search_order_cycles', as: :search_order_cycles
get '/search_order_customers', to: 'reports#search_order_customers', as: :search_order_customers
end
get '/reports', to: 'reports#index', as: :reports
match '/reports/:report_type(/:report_subtype)', to: 'reports#show', via: :get, as: :report
match '/reports/:report_type(/:report_subtype)', to: 'reports#create', via: :post
end

View File

@@ -36,7 +36,7 @@ RSpec.describe "CatalogItems", swagger_doc: "dfc.yaml" do
get "List CatalogItems" do
produces "application/json"
security [{ oidc_token: [] }]
security [oidc_token: []]
response "404", "not found" do
context "as platform user" do

View File

@@ -35,7 +35,7 @@ RSpec.describe "ProductGroups", swagger_doc: "dfc.yaml" do
get "Show ProductGroup" do
produces "application/json"
security [{ oidc_token: [] }]
security [oidc_token: []]
response "200", "success" do
let(:id) { product.id }

View File

@@ -179,7 +179,7 @@ module.exports = {
// transform: { "\\.[jt]sx?$": "babel-jest" },
// An array of regexp pattern strings that are matched against all source file paths, matched files will skip transformation
transformIgnorePatterns: ["/node_modules/(?!(stimulus.+|tom-select)/)"],
transformIgnorePatterns: ["/node_modules/(?!stimulus)/"],
// An array of regexp pattern strings that are matched against all modules before the module loader will automatically return a mock for them
// unmockedModulePathPatterns: undefined,

View File

@@ -46,7 +46,7 @@ module Reporting
end
def order_customers
Customer.where(id: visible_order_customer_ids_query).select("customers.id, customers.email")
Customer.where(id: visible_order_customer_ids).select("customers.id, customers.email")
end
private
@@ -57,8 +57,8 @@ module Reporting
@permissions ||= OpenFoodNetwork::Permissions.new(current_user)
end
def visible_order_customer_ids_query
Permissions::Order.new(current_user).visible_orders.select(:customer_id)
def visible_order_customer_ids
Permissions::Order.new(current_user).visible_orders.pluck(:customer_id)
end
end
end

View File

@@ -15,7 +15,7 @@ module Reporting
@orders ||= search_orders
end
def list(line_item_includes = [{ variant: [:supplier, :product] }])
def list(line_item_includes = [variant: [:supplier, :product]])
line_items = order_permissions.visible_line_items.in_orders(orders.result)
.order(
"supplier.name",

View File

@@ -10,10 +10,10 @@ module Reporting
.complete.not_state(:canceled)
.order(:id))
.group_by do |order|
{
customer_id: order.customer_id || order.email,
hub_id: order.distributor_id,
}
{
customer_id: order.customer_id || order.email,
hub_id: order.distributor_id,
}
end.values
end

View File

@@ -44,11 +44,11 @@ module Reporting
.filter(&method(:filter_enterprise_fee_by_id))
.filter(&method(:filter_enterprise_fee_by_owner))
.map do |enterprise_fee_id, enterprise_fee_adjustment_ids|
{
enterprise_fee_id:,
enterprise_fee_adjustment_ids:,
order:
}
{
enterprise_fee_id:,
enterprise_fee_adjustment_ids:,
order:
}
end
end
end

View File

@@ -1,296 +0,0 @@
/**
* @jest-environment jsdom
*/
import { Application } from "stimulus";
import { fireEvent, waitFor } from "@testing-library/dom";
import tom_select_controller from "controllers/tom_select_controller";
import showHttpError from "js/services/show_http_error";
jest.mock("js/services/show_http_error", () => ({
__esModule: true,
default: jest.fn(),
}));
/* ------------------------------------------------------------------
* Helpers
* ------------------------------------------------------------------ */
const buildResults = (count, start = 1) =>
Array.from({ length: count }, (_, i) => ({
value: String(start + i),
label: `Option ${start + i}`,
}));
const setupDOM = (html) => {
document.body.innerHTML = html;
};
const getSelect = () => document.getElementById("select");
const getTomSelect = () => getSelect().tomselect;
const openDropdown = () => fireEvent.click(document.getElementById("select-ts-control"));
const mockRemoteFetch = (...responses) => {
responses.forEach((response) => {
fetch.mockResolvedValueOnce({
ok: true,
json: async () => response,
});
});
};
const mockDropdownScroll = (
dropdown,
{ scrollHeight = 1000, clientHeight = 300, scrollTop = 700 } = {},
) => {
Object.defineProperty(dropdown, "scrollHeight", {
configurable: true,
value: scrollHeight,
});
Object.defineProperty(dropdown, "clientHeight", {
configurable: true,
value: clientHeight,
});
Object.defineProperty(dropdown, "scrollTop", {
configurable: true,
writable: true,
value: scrollTop,
});
fireEvent.scroll(dropdown);
};
/* ------------------------------------------------------------------
* Expectation helpers
* ------------------------------------------------------------------ */
const expectOptionsCount = (count) => {
expect(document.querySelectorAll('.ts-dropdown-content [role="option"]').length).toBe(count);
};
const expectDropdownToContain = (text) => {
expect(document.querySelector(".ts-dropdown-content")?.textContent).toContain(text);
};
const expectDropdownWithNoResults = () => {
expect(document.querySelector(".ts-dropdown-content")?.textContent).toBe("No results found");
};
/* ------------------------------------------------------------------
* Specs
* ------------------------------------------------------------------ */
describe("TomSelectController", () => {
let application;
beforeAll(() => {
application = Application.start();
application.register("tom-select", tom_select_controller);
});
beforeEach(() => {
global.fetch = jest.fn();
global.I18n = { t: (key) => key };
});
afterEach(() => {
document.body.innerHTML = "";
jest.clearAllMocks();
});
describe("connect()", () => {
beforeEach(() => {
setupDOM(`
<select id="select" data-controller="tom-select">
<option value="">Default Option</option>
<option value="1">Option 1</option>
<option value="2">Option 2</option>
</select>
`);
});
it("initializes TomSelect with default options", () => {
const settings = getTomSelect().settings;
expect(settings.placeholder).toBe("Default Option");
expect(settings.maxItems).toBe(1);
expect(settings.plugins).toEqual(["dropdown_input"]);
expect(settings.allowEmptyOption).toBe(true);
});
});
describe("connect() with custom values", () => {
beforeEach(() => {
setupDOM(`
<select
id="select"
data-controller="tom-select"
data-tom-select-placeholder-value="Choose an option"
data-tom-select-options-value='{"maxItems": 3, "plugins": ["remove_button"]}'
>
<option value="1">Option 1</option>
<option value="2">Option 2</option>
</select>
`);
});
it("applies custom placeholder and options", () => {
const settings = getTomSelect().settings;
expect(settings.placeholder).toBe("Choose an option");
expect(settings.maxItems).toBe(3);
expect(settings.plugins).toEqual(["remove_button"]);
});
});
describe("connect() with remoteUrl", () => {
beforeEach(() => {
setupDOM(`
<select
id="select"
data-controller="tom-select"
data-tom-select-options-value='{"plugins":["virtual_scroll"]}'
data-tom-select-remote-url-value="https://ofn-tests.com/api/search"
></select>
`);
});
it("configures remote loading callbacks", () => {
const settings = getTomSelect().settings;
expect(settings.valueField).toBe("value");
expect(settings.labelField).toBe("label");
expect(settings.searchField).toBe("label");
expect(settings.load).toEqual(expect.any(Function));
expect(settings.firstUrl).toEqual(expect.any(Function));
expect(settings.onFocus).toEqual(expect.any(Function));
});
it("fetches page 1 on focus", async () => {
mockRemoteFetch({
results: buildResults(1),
pagination: { more: false },
});
openDropdown();
await waitFor(() => expect(fetch).toHaveBeenCalledTimes(1));
expect(fetch).toHaveBeenCalledWith(expect.stringContaining("q=&page=1"));
await waitFor(() => {
expectOptionsCount(1);
expectDropdownToContain("Option 1");
});
});
it("fetches remote options using search query", async () => {
const appleOption = { value: "apple", label: "Apple" };
mockRemoteFetch({
results: [...buildResults(1), appleOption],
pagination: { more: false },
});
openDropdown();
await waitFor(() => {
expectOptionsCount(2);
});
mockRemoteFetch({
results: [appleOption],
pagination: { more: false },
});
fireEvent.input(document.getElementById("select-ts-control"), {
target: { value: "apple" },
});
await waitFor(() =>
expect(fetch).toHaveBeenCalledWith(expect.stringContaining("q=apple&page=1")),
);
await waitFor(() => {
expectOptionsCount(1);
expectDropdownToContain("Apple");
});
});
it("loads next page on scroll (infinite scroll)", async () => {
mockRemoteFetch(
{
results: buildResults(30),
pagination: { more: true },
},
{
results: buildResults(1, 31),
pagination: { more: false },
},
);
openDropdown();
await waitFor(() => {
expectOptionsCount(30);
});
const dropdown = document.querySelector(".ts-dropdown-content");
mockDropdownScroll(dropdown);
await waitFor(() => {
expectOptionsCount(31);
});
expect(fetch).toHaveBeenCalledTimes(2);
});
it("handles fetch errors gracefully", async () => {
fetch.mockRejectedValueOnce(new Error("Fetch error"));
openDropdown();
await waitFor(() => {
expectDropdownWithNoResults();
});
expect(showHttpError).not.toHaveBeenCalled();
});
it("displays HTTP error on failure", async () => {
fetch.mockResolvedValueOnce({
ok: false,
status: 500,
json: async () => ({}),
});
openDropdown();
await waitFor(() => {
expect(showHttpError).toHaveBeenCalledWith(500);
});
expectDropdownWithNoResults();
});
it("controls loading behavior based on user interaction", () => {
const settings = getTomSelect().settings;
// Initial state: openedByClick is false, query is empty
expect(settings.shouldLoad("")).toBe(false);
// Simulating opening the dropdown
settings.onDropdownOpen();
expect(settings.shouldLoad("")).toBe(true);
// Simulating typing
settings.onType();
expect(settings.shouldLoad("")).toBe(false);
// Query present
expect(settings.shouldLoad("a")).toBe(true);
});
});
});

View File

@@ -109,7 +109,7 @@ RSpec.describe Spree::Gateway::StripeSCA, :vcr, :stripe_version do
end
it "refunds the payment" do
response = subject.void(payment_intent.id, {})
response = subject.void(payment_intent.id, nil, {})
expect(response.success?).to eq true
end
@@ -131,7 +131,7 @@ RSpec.describe Spree::Gateway::StripeSCA, :vcr, :stripe_version do
end
it "void the payment" do
response = subject.void(payment_intent.id, {})
response = subject.void(payment_intent.id, nil, {})
expect(response.success?).to eq true
end
@@ -162,7 +162,7 @@ RSpec.describe Spree::Gateway::StripeSCA, :vcr, :stripe_version do
stripe_account: stripe_test_account
)
response = subject.credit(1000, payment_intent.id, {})
response = subject.credit(1000, nil, payment_intent.id, {})
expect(response.success?).to eq true
end

View File

@@ -14,14 +14,8 @@ RSpec.describe Spree::Gateway do
end
it "passes through all arguments on a method_missing call" do
expect(Rails.env).to receive(:local?).and_return(false)
gateway = test_gateway.new
expect(gateway.provider).to receive(:imaginary_method).with('foo')
gateway.imaginary_method('foo')
end
it "raises an error in test env" do
gateway = test_gateway.new
expect { gateway.imaginary_method('foo') }.to raise_error StandardError
end
end

View File

@@ -86,11 +86,11 @@ RSpec.describe Spree::Order do
(Spree::Shipment.state_machine.states.keys - [:pending, :backorder, :ready])
.each do |shipment_state|
it "should be false if shipment_state is #{shipment_state}" do
allow(order).to receive_messages completed?: true
order.shipment_state = shipment_state
expect(order.can_cancel?).to be_falsy
end
it "should be false if shipment_state is #{shipment_state}" do
allow(order).to receive_messages completed?: true
order.shipment_state = shipment_state
expect(order.can_cancel?).to be_falsy
end
end
end

View File

@@ -131,6 +131,13 @@ RSpec.describe Spree::PaymentMethod do
expect(pm.errors.to_a).to eq(["Name can't be blank", "At least one hub must be selected"])
end
it "generates a clean name for known Payment Method types" do
expect(Spree::PaymentMethod::Check.clean_name)
.to eq('Cash/EFT/etc. (payments for which automatic validation is not required)')
expect(Spree::Gateway::PayPalExpress.clean_name).to eq('PayPal Express')
expect(Spree::Gateway::StripeSCA.clean_name).to eq('Stripe SCA')
end
it "computes the amount of fees" do
order = create(:order)

View File

@@ -345,6 +345,24 @@ RSpec.describe Spree::Payment do
allow(payment_method).to receive(:void).and_return(success_response)
end
context "when profiles are supported" do
it "should call payment_enterprise.void with the payment's response_code" do
allow(payment_method).to receive(:payment_profiles_supported) { true }
expect(payment_method).to receive(:void).with('123', card,
anything).and_return(success_response)
payment.void_transaction!
end
end
context "when profiles are not supported" do
it "should call payment_gateway.void with the payment's response_code" do
allow(payment_method).to receive(:payment_profiles_supported) { false }
expect(payment_method).to receive(:void).with('123', card,
anything).and_return(success_response)
payment.void_transaction!
end
end
it "should log the response" do
payment.void_transaction!
expect(payment).to have_received(:record_response)
@@ -419,7 +437,7 @@ RSpec.describe Spree::Payment do
end
it "should call credit on the gateway with the credit amount and response_code" do
expect(payment_method).to receive(:credit).with(1000, '123',
expect(payment_method).to receive(:credit).with(1000, card, '123',
anything).and_return(success_response)
payment.credit!
end
@@ -445,7 +463,7 @@ RSpec.describe Spree::Payment do
it "should call credit on the gateway with the credit amount and response_code" do
expect(payment_method).to receive(:credit).with(
amount_in_cents, '123', anything
amount_in_cents, card, '123', anything
).and_return(success_response)
payment.credit!
end
@@ -458,7 +476,7 @@ RSpec.describe Spree::Payment do
it "should call credit on the gateway with original payment amount and response_code" do
expect(payment_method).to receive(:credit).with(
amount_in_cents.to_f, '123', anything
amount_in_cents.to_f, card, '123', anything
).and_return(success_response)
payment.credit!
end
@@ -640,6 +658,7 @@ RSpec.describe Spree::Payment do
context "when profiles are supported" do
before do
allow(payment_method).to receive(:payment_profiles_supported?) { true }
allow(payment.source).to receive(:has_payment_profile?) { false }
end
@@ -681,6 +700,26 @@ RSpec.describe Spree::Payment do
end
end
context "when profiles are not supported" do
before do
allow(payment_method).to receive(:payment_profiles_supported?) { false }
end
it "should not create a payment profile" do
payment_method.name = 'Gateway'
payment_method.distributors << create(:distributor_enterprise)
payment_method.save!
expect(payment_method).not_to receive :create_profile
payment = Spree::Payment.create(
amount: 100,
order: create(:order),
source: card,
payment_method:
)
end
end
context 'when the payment was completed but now void' do
let(:payment) { create(:payment, :completed, amount: 100, order:) }
@@ -838,6 +877,23 @@ RSpec.describe Spree::Payment do
end
end
describe "performing refunds" do
before do
allow(payment).to receive(:calculate_refund_amount) { 123 }
expect(payment.payment_method).to receive(:refund).and_return(success)
end
it "performs the refund without payment profiles" do
allow(payment.payment_method).to receive(:payment_profiles_supported?) { false }
payment.refund!
end
it "performs the refund with payment profiles" do
allow(payment.payment_method).to receive(:payment_profiles_supported?) { true }
payment.refund!
end
end
it "records the response" do
allow(payment).to receive(:calculate_refund_amount) { 123 }
allow(payment.payment_method).to receive(:refund).and_return(success)

View File

@@ -17,7 +17,7 @@ RSpec.describe Spree::User do
bill_address_attributes: new_bill_address.dup.attributes.merge(
'id' => old_bill_address.id
)
.except!('created_at', 'updated_at')
.except!('created_at', 'updated_at')
)
expect(user.bill_address.id).to eq old_bill_address.id

View File

@@ -10,7 +10,7 @@ RSpec.describe OutstandingBalanceQuery do
let(:normalized_sql_statement) { normalize(query.statement) }
it 'returns the CASE statement necessary to compute the order balance' do
expect(normalized_sql_statement).to eq(normalize(<<~SQL.squish))
expect(normalized_sql_statement).to eq(normalize(<<-SQL.squish))
CASE WHEN "spree_orders"."state" IN ('canceled', 'returned') THEN "spree_orders"."payment_total"
WHEN "spree_orders"."state" IS NOT NULL THEN "spree_orders"."payment_total" - "spree_orders"."total"
ELSE 0 END

View File

@@ -1,328 +0,0 @@
# frozen_string_literal: true
RSpec.describe "Admin Reports AJAX Search API" do
let(:bill_address) { create(:address) }
let(:ship_address) { create(:address) }
let(:instructions) { "pick up on thursday please" }
let(:coordinator1) { create(:distributor_enterprise) }
let(:supplier1) { create(:supplier_enterprise) }
let(:supplier2) { create(:supplier_enterprise) }
let(:supplier3) { create(:supplier_enterprise) }
let(:distributor1) { create(:distributor_enterprise) }
let(:distributor2) { create(:distributor_enterprise) }
let(:product1) { create(:product, price: 12.34, supplier_id: supplier1.id) }
let(:product2) { create(:product, price: 23.45, supplier_id: supplier2.id) }
let(:product3) { create(:product, price: 34.56, supplier_id: supplier3.id) }
let(:enterprise_fee1) { create(:enterprise_fee, name: "Delivery Fee", enterprise: distributor1) }
let(:enterprise_fee2) { create(:enterprise_fee, name: "Admin Fee", enterprise: distributor2) }
let(:ocA) {
create(:simple_order_cycle, coordinator: coordinator1,
distributors: [distributor1, distributor2],
suppliers: [supplier1, supplier2, supplier3],
variants: [product1.variants.first, product3.variants.first])
}
let(:ocB) {
create(:simple_order_cycle, coordinator: coordinator1,
distributors: [distributor1, distributor2],
suppliers: [supplier1, supplier2, supplier3],
variants: [product2.variants.first])
}
let(:orderA1) do
order = create(:order, distributor: distributor1, bill_address:,
ship_address:, special_instructions: instructions,
order_cycle: ocA)
order.line_items << create(:line_item, variant: product1.variants.first)
order.line_items << create(:line_item, variant: product3.variants.first)
order.finalize!
order.save
order
end
let(:orderA2) do
order = create(:order, distributor: distributor2, bill_address:,
ship_address:, special_instructions: instructions,
order_cycle: ocA)
order.line_items << create(:line_item, variant: product2.variants.first)
order.finalize!
order.save
order
end
let(:orderB1) do
order = create(:order, distributor: distributor1, bill_address:,
ship_address:, special_instructions: instructions,
order_cycle: ocB)
order.line_items << create(:line_item, variant: product1.variants.first)
order.line_items << create(:line_item, variant: product3.variants.first)
order.finalize!
order.save
order
end
let(:base_params) do
{
report_type: :enterprise_fee_summary,
report_subtype: :enterprise_fees_with_tax_report_by_order
}
end
def create_adjustment(order, fee, amount)
order.adjustments.create!(
originator: fee,
label: fee.name,
amount:,
state: "finalized",
order:
)
end
context "when user is an admin" do
before do
login_as create(:admin_user)
create_adjustment(orderA1, enterprise_fee1, 5.0)
create_adjustment(orderB1, enterprise_fee2, 3.0)
end
describe "GET /admin/reports/search_enterprise_fees" do
it "returns enterprise fees sorted alphabetically by name" do
get "/admin/reports/search_enterprise_fees", params: base_params
expect(response).to have_http_status(:ok)
json_response = response.parsed_body
expect(json_response["results"].pluck("label")).to eq(['Admin Fee', 'Delivery Fee'])
expect(json_response["pagination"]["more"]).to be false
end
context "with more than 30 records" do
before do
create_list(:enterprise_fee, 35, enterprise: distributor1) do |fee, i|
index = (i + 1).to_s.rjust(2, "0")
fee.update!(name: "Fee #{index}")
create_adjustment(orderA1, fee, 1.0)
end
end
it "returns first page with 30 results and more flag as true" do
get "/admin/reports/search_enterprise_fees", params: base_params.merge(page: 1)
json_response = response.parsed_body
expect(json_response["results"].length).to eq(30)
expect(json_response["pagination"]["more"]).to be true
end
it "returns remaining results on second page with more flag as false" do
get "/admin/reports/search_enterprise_fees", params: base_params.merge(page: 2)
json_response = response.parsed_body
expect(json_response["results"].length).to eq(7)
expect(json_response["pagination"]["more"]).to be false
end
end
end
describe "GET /admin/reports/search_enterprise_fee_owners" do
it "returns unique enterprise fee owners sorted alphabetically by name" do
distributor1.update!(name: "Zebra Farm")
distributor2.update!(name: "Alpha Market")
get "/admin/reports/search_enterprise_fee_owners", params: base_params
expect(response).to have_http_status(:ok)
json_response = response.parsed_body
expect(json_response["results"].pluck("label")).to eq(['Alpha Market', 'Zebra Farm'])
expect(json_response["pagination"]["more"]).to be false
end
end
describe "GET /admin/reports/search_order_customers" do
let!(:customer1) { create(:customer, email: "alice@example.com", enterprise: distributor1) }
let!(:customer2) { create(:customer, email: "bob@example.com", enterprise: distributor1) }
before do
orderA1.update!(customer: customer1)
orderA2.update!(customer: customer2)
end
it "returns all customers sorted by email" do
get "/admin/reports/search_order_customers", params: base_params
json_response = response.parsed_body
expect(json_response["results"].pluck("label")).to eq(["alice@example.com",
"bob@example.com"])
expect(json_response["pagination"]["more"]).to be false
end
it "filters customers by email query" do
get "/admin/reports/search_order_customers", params: base_params.merge(q: "alice")
json_response = response.parsed_body
expect(json_response["results"].pluck("label")).to eq(["alice@example.com"])
expect(json_response["pagination"]["more"]).to be false
end
context "with more than 30 customers" do
before do
create_list(:customer, 35, enterprise: distributor1) do |customer, i|
customer.update!(email: "customer#{(i + 1).to_s.rjust(2, '0')}@example.com")
order = create(:order, distributor: distributor1, order_cycle: ocA, customer:)
order.line_items << create(:line_item, variant: product1.variants.first)
order.finalize!
end
end
it "returns first page with 30 results and more flag as true" do
get "/admin/reports/search_order_customers", params: base_params.merge(page: 1)
json_response = response.parsed_body
expect(json_response["results"].length).to eq(30)
expect(json_response["pagination"]["more"]).to be true
end
it "returns remaining results on second page with more flag as false" do
get "/admin/reports/search_order_customers", params: base_params.merge(page: 2)
json_response = response.parsed_body
expect(json_response["results"].length).to eq(7)
expect(json_response["pagination"]["more"]).to be false
end
end
end
describe "GET /admin/reports/search_order_cycles" do
before do
ocA.update!(name: "Winter Market")
ocB.update!(name: "Summer Market")
end
it "returns order cycles sorted by close date" do
get "/admin/reports/search_order_cycles", params: base_params
json_response = response.parsed_body
expect(json_response["results"].pluck("label")).to eq(["Summer Market", "Winter Market"])
expect(json_response["pagination"]["more"]).to be false
end
it "filters order cycles by name query" do
get "/admin/reports/search_order_cycles", params: base_params.merge(q: "Winter")
json_response = response.parsed_body
expect(json_response["results"].pluck("label")).to eq(["Winter Market"])
expect(json_response["pagination"]["more"]).to be false
end
end
describe "GET /admin/reports/search_distributors" do
before do
distributor1.update!(name: "Alpha Farm")
distributor2.update!(name: "Beta Market")
end
it "filters distributors by name query" do
get "/admin/reports/search_distributors", params: base_params.merge(q: "Alpha")
json_response = response.parsed_body
expect(json_response["results"].pluck("label")).to eq(["Alpha Farm"])
expect(json_response["pagination"]["more"]).to be false
end
context "with more than 30 distributors" do
before { create_list(:distributor_enterprise, 35) }
it "returns first page with 30 results and more flag as true" do
get "/admin/reports/search_distributors", params: base_params.merge(page: 1)
json_response = response.parsed_body
expect(json_response["results"].length).to eq(30)
expect(json_response["pagination"]["more"]).to be true
end
it "returns remaining results on subsequent pages with more flag as false" do
get "/admin/reports/search_distributors", params: base_params.merge(page: 2)
json_response = response.parsed_body
expect(json_response["results"].length).to be > 0
expect(json_response["pagination"]["more"]).to be false
end
end
end
end
context "when user is not an admin" do
before do
login_as distributor1.users.first
create_adjustment(orderA1, enterprise_fee1, 5.0)
create_adjustment(orderA2, enterprise_fee2, 3.0)
end
describe "GET /admin/reports/search_enterprise_fees" do
it "returns only enterprise fees for user's managed enterprises" do
get "/admin/reports/search_enterprise_fees", params: base_params
expect(response).to have_http_status(:ok)
json_response = response.parsed_body
expect(json_response["results"].pluck("label")).to eq(['Delivery Fee'])
expect(json_response["pagination"]["more"]).to be false
end
end
describe "GET /admin/reports/search_enterprise_fee_owners" do
it "returns only enterprise fee owners for user's managed enterprises" do
get "/admin/reports/search_enterprise_fee_owners", params: base_params
expect(response).to have_http_status(:ok)
json_response = response.parsed_body
expect(json_response["results"].pluck("label")).to eq([distributor1.name])
expect(json_response["pagination"]["more"]).to be false
end
end
describe "GET /admin/reports/search_order_customers" do
it "returns only customers from user's managed enterprises" do
customer1 = create(:customer, email: "alice@example.com", enterprise: distributor1)
customer2 = create(:customer, email: "bob@example.com", enterprise: distributor1)
orderA1.update!(customer: customer1)
orderA2.update!(customer: customer2)
get "/admin/reports/search_order_customers", params: base_params
json_response = response.parsed_body
expect(json_response["results"].pluck("label")).to eq(["alice@example.com"])
expect(json_response["pagination"]["more"]).to be false
end
end
describe "GET /admin/reports/search_order_cycles" do
it "returns only order cycles accessible to user's managed enterprises" do
ocA.update!(name: "Winter Market")
ocB.update!(name: "Summer Market")
create(:simple_order_cycle, name: 'Autumn Market', coordinator: coordinator1,
distributors: [distributor2],
suppliers: [supplier1, supplier2, supplier3],
variants: [product2.variants.first])
get "/admin/reports/search_order_cycles", params: base_params
json_response = response.parsed_body
expect(json_response["results"].pluck("label")).to eq(["Summer Market", "Winter Market"])
expect(json_response["pagination"]["more"]).to be false
end
end
describe "GET /admin/reports/search_distributors" do
it "returns only user's managed distributors" do
get "/admin/reports/search_distributors", params: base_params
json_response = response.parsed_body
expect(json_response["results"].pluck("label")).to eq([distributor1.name])
expect(json_response["pagination"]["more"]).to be false
end
end
end
end

View File

@@ -38,8 +38,8 @@ RSpec.shared_examples "attribute changes - tax total changes" do |boolean, type,
create(:order_with_taxes, product_price: 110, tax_rate_amount: 0.1,
included_in_price: included_boolean)
.tap do |order|
order.create_tax_charge!
order.update_shipping_fees!
order.create_tax_charge!
order.update_shipping_fees!
end
end

View File

@@ -92,7 +92,9 @@ RSpec::Matchers.define :have_select2 do |id, options = {}|
end
def selected_option_is(from, text)
find("#{from} a.select2-choice", text:)
within find(from) do
find("a.select2-choice").text == text
end
end
def with_select2_open(from)

View File

@@ -144,7 +144,7 @@ module StripeStubs
customer_id = options[:customer_id] || "cus_A123"
{ status: 200,
body: JSON.generate(id: customer_id,
sources: { data: [{ id: customer_id }] }) }
sources: { data: [id: customer_id] }) }
end
def payment_successful_refund_mock

View File

@@ -69,6 +69,8 @@ module WebHelper
.find(:css, '.select2-drop-active .select2-result-label',
text: options[:select_text] || value)
.click
expect(page).to have_select2 options[:from], selected: options[:select_text] || value
end
def open_select2(selector)

View File

@@ -152,7 +152,7 @@ RSpec.describe '
click_link 'Adjustments'
page.find('tr', text: 'Extra Adjustment').find('a.icon-edit').click
expect(page).to have_select2 :adjustment_tax_category_id, selected: "None"
expect(page).to have_select2 :adjustment_tax_category_id, selected: []
# When I edit the adjustment, setting a tax rate
select2_select 'GST', from: :adjustment_tax_category_id

View File

@@ -1044,10 +1044,10 @@ RSpec.describe '
page.driver
.dismiss_modal :confirm,
text: "Unsaved changes exist and will be lost if you continue." do
within "tr#li_#{li1.id}" do
fill_in "quantity", with: (li1.quantity + 1)
find("a.edit-order").click
end
within "tr#li_#{li1.id}" do
fill_in "quantity", with: (li1.quantity + 1)
find("a.edit-order").click
end
end
# So we save the changes

View File

@@ -20,11 +20,7 @@ RSpec.describe '
click_link 'Payment Methods'
click_link 'New Payment Method'
expect(page).to have_select2 "payment_method_type", selected: "Choose..."
fill_in 'payment_method_name', with: 'Cheque payment method'
cash_name = "Cash/EFT/etc. (payments for which automatic validation is not required)"
select2_select cash_name, from: "payment_method_type"
check "payment_method_distributor_ids_#{@distributors[0].id}"
click_button 'Create'
@@ -247,9 +243,6 @@ RSpec.describe '
it "creates payment methods" do
visit spree.new_admin_payment_method_path
fill_in 'payment_method_name', with: 'Cheque payment method'
cash_name = "Cash/EFT/etc. (payments for which automatic validation is not required)"
select2_select cash_name, from: "payment_method_type"
expect(page).to have_field 'payment_method_description'
expect(page).to have_select 'payment_method_display_on'

View File

@@ -296,8 +296,11 @@ RSpec.describe "Enterprise Summary Fee with Tax Report By Producer" do
end
it "should filter by distributor and order cycle" do
tomselect_multiselect distributor.name, from: 'q[distributor_id_in][]'
tomselect_multiselect order_cycle.name, from: 'q[order_cycle_id_in][]'
page.find("#s2id_autogen1").click
find('li', text: distributor.name).click # selects Distributor
page.find("#s2id_q_order_cycle_id_in").click
find('li', text: order_cycle.name).click
run_report
expect(page.find("table.report__table thead tr")).to have_content(table_header)
@@ -452,6 +455,9 @@ RSpec.describe "Enterprise Summary Fee with Tax Report By Producer" do
}
context "filtering" do
let(:fee_name_selector){ "#s2id_q_enterprise_fee_id_in" }
let(:fee_owner_selector){ "#s2id_q_enterprise_fee_owner_id_in" }
let(:summary_row_after_filtering_by_fee_name){
[cost_of_produce1, "TOTAL", "120.0", "4.8", "124.8"].join(" ")
}
@@ -465,8 +471,11 @@ RSpec.describe "Enterprise Summary Fee with Tax Report By Producer" do
end
it "should filter by distributor and order cycle" do
tomselect_multiselect distributor.name, from: 'q[distributor_id_in][]'
tomselect_multiselect order_cycle3.name, from: 'q[order_cycle_id_in][]'
page.find("#s2id_autogen1").click
find('li', text: distributor.name).click # selects Distributor
page.find("#s2id_q_order_cycle_id_in").click
find('li', text: order_cycle3.name).click
run_report
expect(page.find("table.report__table thead tr")).to have_content(table_header)
@@ -495,7 +504,8 @@ RSpec.describe "Enterprise Summary Fee with Tax Report By Producer" do
end
it "should filter by producer" do
tomselect_multiselect supplier2.name, from: 'supplier_id_in[]'
page.find("#s2id_supplier_id_in").click
find('li', text: supplier2.name).click
run_report
expect(page.find("table.report__table thead tr")).to have_content(table_header)
@@ -518,7 +528,8 @@ RSpec.describe "Enterprise Summary Fee with Tax Report By Producer" do
end
it "should filter by fee name" do
tomselect_multiselect supplier_fees.name, from: 'q[enterprise_fee_id_in][]'
page.find(fee_name_selector).click
find('li', text: supplier_fees.name).click
run_report
@@ -546,7 +557,8 @@ RSpec.describe "Enterprise Summary Fee with Tax Report By Producer" do
end
it "should filter by fee owner" do
tomselect_multiselect supplier.name, from: 'q[enterprise_fee_owner_id_in][]'
page.find(fee_owner_selector).click
find('li', text: supplier.name).click
run_report
expect(page.find("table.report__table thead tr")).to have_content(table_header)

View File

@@ -360,10 +360,10 @@ RSpec.describe "As a consumer, I want to checkout my order" do
# And fake the payment status to avoid user interaction.
allow_any_instance_of(Taler::Client)
.to receive(:fetch_order) do
payment = Spree::Payment.last
url = payment_gateways_confirm_taler_path(payment_id: payment.id)
payment = Spree::Payment.last
url = payment_gateways_confirm_taler_path(payment_id: payment.id)
{ "order_status_url" => url, "order_status" => "paid" }
{ "order_status_url" => url, "order_status" => "paid" }
end
end

View File

@@ -4005,9 +4005,9 @@ hasown@^2.0.2:
function-bind "^1.1.2"
hotkeys-js@*:
version "4.0.0"
resolved "https://registry.yarnpkg.com/hotkeys-js/-/hotkeys-js-4.0.0.tgz#75336c0ac610ad384d286c61c519909dcd4bdf6b"
integrity sha512-gIoeqMWYqPIItc4HaseVbtTRpEpBbeufZMUcoWtN62JZdDq3KadS1ijN6wpaDjTzRK7PjT3QOPUcx+yNT0rrZQ==
version "3.13.15"
resolved "https://registry.yarnpkg.com/hotkeys-js/-/hotkeys-js-3.13.15.tgz#2d394bd6bd78857d4b24dc86bdba2fa1cf7012fc"
integrity sha512-gHh8a/cPTCpanraePpjRxyIlxDFrIhYqjuh01UHWEwDpglJKCnvLW8kqSx5gQtOuSsJogNZXLhOdbSExpgUiqg==
hpack.js@^2.1.6:
version "2.1.6"
@@ -7064,9 +7064,9 @@ toidentifier@1.0.1, toidentifier@~1.0.1:
integrity sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==
tom-select@*:
version "2.5.1"
resolved "https://registry.yarnpkg.com/tom-select/-/tom-select-2.5.1.tgz#8c8d3f11e5c1780b5f26c9e90f4e650842ff9596"
integrity sha512-63D5/Qf6bb6kLSgksEuas/60oawDcuUHrD90jZofeOpF6bkQFYriKrvtpJBQQ4xIA5dUGcjhBbk/yrlfOQsy3g==
version "2.4.6"
resolved "https://registry.yarnpkg.com/tom-select/-/tom-select-2.4.6.tgz#23acdfc09ee235eb752706d418c9c9ae6ccf67f0"
integrity sha512-Hhqi15AiTl0+FjaHVTXvUkF3t7x4W5LXUHxLYlzp7r8bcIgGJyz9M+3ZvrHdTRvEmV4EmNyJPbHJJnZOjr5Iig==
dependencies:
"@orchidjs/sifter" "^1.1.0"
"@orchidjs/unicode-variants" "^1.1.2"
@@ -7376,9 +7376,9 @@ webpack-sources@^3.3.3:
integrity sha512-yd1RBzSGanHkitROoPFd6qsrxt+oFhg/129YzheDGqeustzX0vTZJZsSsQjVQC4yzBQ56K55XU8gaNCtIzOnTg==
webpack@^5.104.0:
version "5.105.1"
resolved "https://registry.yarnpkg.com/webpack/-/webpack-5.105.1.tgz#c05cb3621196c76fa3b3a9bea446d14616b83778"
integrity sha512-Gdj3X74CLJJ8zy4URmK42W7wTZUJrqL+z8nyGEr4dTN0kb3nVs+ZvjbTOqRYPD7qX4tUmwyHL9Q9K6T1seW6Yw==
version "5.105.0"
resolved "https://registry.yarnpkg.com/webpack/-/webpack-5.105.0.tgz#38b5e6c5db8cbe81debbd16e089335ada05ea23a"
integrity sha512-gX/dMkRQc7QOMzgTe6KsYFM7DxeIONQSui1s0n/0xht36HvrgbxtM1xBlgx596NbpHuQU8P7QpKwrZYwUX48nw==
dependencies:
"@types/eslint-scope" "^3.7.7"
"@types/estree" "^1.0.8"