spree_core 6.0.0.rc1 → 6.0.0.rc3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/README.md +3 -3
- data/app/jobs/spree/search_provider/index_job.rb +4 -0
- data/app/jobs/spree/uploads/purge_unattached_job.rb +49 -0
- data/app/mailers/spree/base_mailer.rb +4 -3
- data/app/models/concerns/spree/admin_user_methods.rb +2 -0
- data/app/models/concerns/spree/calculated_adjustments.rb +41 -13
- data/app/models/concerns/spree/calculator/currency_amounts.rb +1 -1
- data/app/models/concerns/spree/calculator/tiers.rb +1 -14
- data/app/models/concerns/spree/canonical_decimals.rb +61 -0
- data/app/models/concerns/spree/customer_methods.rb +8 -4
- data/app/models/concerns/spree/derived_number.rb +11 -13
- data/app/models/concerns/spree/preference_schema/json_schema.rb +139 -0
- data/app/models/concerns/spree/preference_schema.rb +59 -21
- data/app/models/concerns/spree/product_scopes.rb +5 -1
- data/app/models/concerns/spree/purchase/payment_processing.rb +1 -1
- data/app/models/concerns/spree/purchase/purchase_order.rb +7 -2
- data/app/models/concerns/spree/purchase/store_credits.rb +36 -1
- data/app/models/concerns/spree/purchase/totals.rb +1 -8
- data/app/models/concerns/spree/ransackable_attributes.rb +46 -0
- data/app/models/concerns/spree/signed_id_attachments.rb +54 -0
- data/app/models/concerns/spree/stores/email_branding.rb +2 -1
- data/app/models/concerns/spree/stores/markets.rb +5 -2
- data/app/models/concerns/spree/typed_adjustment_line.rb +4 -5
- data/app/models/concerns/spree/typed_associations.rb +6 -18
- data/app/models/concerns/spree/vat_price_calculation.rb +5 -6
- data/app/models/spree/address.rb +1 -1
- data/app/models/spree/adjusters/largest_remainder.rb +14 -1
- data/app/models/spree/adjusters/promotion.rb +3 -4
- data/app/models/spree/allowed_origin.rb +3 -0
- data/app/models/spree/base.rb +29 -0
- data/app/models/spree/calculator/flat_percent_item_total.rb +1 -1
- data/app/models/spree/calculator/flat_rate.rb +2 -2
- data/app/models/spree/calculator/flexi_rate.rb +3 -3
- data/app/models/spree/calculator/percent_on_line_item.rb +1 -1
- data/app/models/spree/calculator/price_sack.rb +4 -4
- data/app/models/spree/calculator/shipping/digital_delivery.rb +2 -2
- data/app/models/spree/calculator/shipping/flat_percent_item_total.rb +6 -4
- data/app/models/spree/calculator/shipping/flat_rate.rb +4 -4
- data/app/models/spree/calculator/shipping/flexi_rate.rb +3 -3
- data/app/models/spree/calculator/shipping/per_item.rb +2 -2
- data/app/models/spree/calculator/shipping/price_sack.rb +4 -4
- data/app/models/spree/calculator/tiered_flat_rate.rb +2 -2
- data/app/models/spree/calculator/tiered_percent.rb +1 -1
- data/app/models/spree/catalog.rb +1 -0
- data/app/models/spree/catalog_price.rb +0 -7
- data/app/models/spree/catalog_price_tier.rb +0 -8
- data/app/models/spree/category.rb +3 -1
- data/app/models/spree/channel/gating.rb +2 -1
- data/app/models/spree/channel.rb +2 -0
- data/app/models/spree/claim.rb +1 -0
- data/app/models/spree/collection.rb +2 -0
- data/app/models/spree/commission_rate.rb +5 -1
- data/app/models/spree/commission_rules/category_rule.rb +2 -5
- data/app/models/spree/commission_rules/item_total_rule.rb +2 -2
- data/app/models/spree/commission_rules/seller_rule.rb +2 -5
- data/app/models/spree/company.rb +1 -0
- data/app/models/spree/custom_field_definition.rb +11 -12
- data/app/models/spree/customer_group.rb +1 -0
- data/app/models/spree/data_request.rb +1 -0
- data/app/models/spree/delivery_method.rb +6 -1
- data/app/models/spree/delivery_method_rules/channel_rule.rb +2 -5
- data/app/models/spree/delivery_method_rules/item_total_rule.rb +2 -2
- data/app/models/spree/delivery_profile.rb +1 -0
- data/app/models/spree/delivery_rate.rb +3 -3
- data/app/models/spree/delivery_zone.rb +1 -1
- data/app/models/spree/exchange.rb +1 -0
- data/app/models/spree/exports/report.rb +2 -2
- data/app/models/spree/file_upload.rb +155 -0
- data/app/models/spree/fulfillment.rb +13 -3
- data/app/models/spree/fulfillment_group.rb +6 -1
- data/app/models/spree/gateway/bogus.rb +2 -0
- data/app/models/spree/gateway.rb +1 -1
- data/app/models/spree/gift_card.rb +6 -7
- data/app/models/spree/gift_card_batch.rb +3 -4
- data/app/models/spree/import.rb +3 -0
- data/app/models/spree/integration.rb +1 -1
- data/app/models/spree/line_item.rb +2 -2
- data/app/models/spree/market.rb +7 -2
- data/app/models/spree/media.rb +1 -9
- data/app/models/spree/newsletter_subscriber.rb +1 -0
- data/app/models/spree/option_type.rb +2 -1
- data/app/models/spree/option_value.rb +2 -1
- data/app/models/spree/order.rb +5 -5
- data/app/models/spree/order_group.rb +7 -2
- data/app/models/spree/package_type.rb +1 -0
- data/app/models/spree/payment/gateway_options.rb +1 -1
- data/app/models/spree/payment.rb +3 -10
- data/app/models/spree/payment_method/check.rb +2 -0
- data/app/models/spree/payment_method/store_credit.rb +11 -9
- data/app/models/spree/payment_method.rb +42 -2
- data/app/models/spree/policy.rb +1 -0
- data/app/models/spree/price.rb +17 -14
- data/app/models/spree/price_adjustment_tier.rb +4 -0
- data/app/models/spree/price_list.rb +13 -7
- data/app/models/spree/price_rules/channel_rule.rb +2 -5
- data/app/models/spree/price_rules/customer_group_rule.rb +2 -5
- data/app/models/spree/price_rules/market_rule.rb +2 -5
- data/app/models/spree/price_rules/user_rule.rb +1 -3
- data/app/models/spree/product/channels.rb +9 -2
- data/app/models/spree/product.rb +23 -23
- data/app/models/spree/product_type.rb +1 -0
- data/app/models/spree/promotion/actions/create_line_items.rb +1 -1
- data/app/models/spree/promotion/rules/category.rb +1 -1
- data/app/models/spree/promotion/rules/channel.rb +2 -5
- data/app/models/spree/promotion/rules/country.rb +3 -4
- data/app/models/spree/promotion/rules/currency.rb +1 -1
- data/app/models/spree/promotion/rules/customer_group.rb +2 -5
- data/app/models/spree/promotion/rules/item_total.rb +2 -2
- data/app/models/spree/promotion/rules/market.rb +2 -5
- data/app/models/spree/promotion/rules/option_value.rb +2 -3
- data/app/models/spree/promotion/rules/product.rb +1 -1
- data/app/models/spree/promotion.rb +1 -0
- data/app/models/spree/purchase_order.rb +2 -0
- data/app/models/spree/refund.rb +13 -15
- data/app/models/spree/return.rb +1 -0
- data/app/models/spree/saved_report.rb +1 -0
- data/app/models/spree/search_provider/base.rb +4 -0
- data/app/models/spree/seller.rb +8 -1
- data/app/models/spree/seller_payout.rb +1 -0
- data/app/models/spree/seller_requirement.rb +1 -0
- data/app/models/spree/seller_requirement_submission.rb +1 -0
- data/app/models/spree/seller_transfer.rb +1 -0
- data/app/models/spree/shipping_label.rb +1 -0
- data/app/models/spree/stock/estimator.rb +1 -1
- data/app/models/spree/stock_level.rb +2 -0
- data/app/models/spree/stock_location.rb +1 -0
- data/app/models/spree/stock_transfer.rb +2 -0
- data/app/models/spree/store.rb +37 -12
- data/app/models/spree/store_credit.rb +5 -4
- data/app/models/spree/supplier.rb +1 -0
- data/app/models/spree/tax_category.rb +1 -0
- data/app/models/spree/tax_exemption_certificate.rb +1 -0
- data/app/models/spree/tax_provider/internal.rb +12 -9
- data/app/models/spree/tax_rate.rb +29 -6
- data/app/models/spree/taxon.rb +1 -1
- data/app/models/spree/update_status.rb +25 -0
- data/app/models/spree/variant.rb +6 -9
- data/app/models/spree/webhook_delivery.rb +1 -0
- data/app/models/spree/webhook_endpoint.rb +36 -0
- data/app/presenters/spree/csv/order_line_item_presenter.rb +10 -6
- data/app/presenters/spree/csv/price_list_price_presenter.rb +3 -8
- data/app/presenters/spree/csv/product_variant_presenter.rb +17 -8
- data/app/presenters/spree/csv/purchase_order_item_presenter.rb +1 -3
- data/app/services/spree/carts/update.rb +3 -3
- data/app/services/spree/imports/row_processors/product_variant.rb +12 -0
- data/app/services/spree/locales.rb +39 -5
- data/app/services/spree/orders/create.rb +3 -3
- data/app/services/spree/orders/discounts/create.rb +8 -8
- data/app/services/spree/prices/bulk_upsert.rb +32 -25
- data/app/services/spree/seeds/stores.rb +1 -1
- data/app/services/spree/stores/provision_defaults.rb +13 -16
- data/app/services/spree/update_check.rb +160 -0
- data/app/services/spree/uploads/assign_stores.rb +92 -0
- data/app/validators/spree/attachment_content_type_validator.rb +19 -2
- data/app/validators/spree/locale_codes_validator.rb +22 -0
- data/app/workflows/spree/fulfillments/create.rb +2 -2
- data/app/workflows/spree/payments/capture.rb +20 -5
- data/app/workflows/spree/payments/process.rb +3 -1
- data/app/workflows/spree/price_lists/update.rb +4 -4
- data/app/workflows/spree/purchase_orders/create.rb +3 -1
- data/app/workflows/spree/shipping_labels/record.rb +2 -2
- data/app/workflows/spree/stock_transfers/create.rb +3 -1
- data/config/currency_exponents.json +180 -0
- data/config/locales/content_locales.rb +9 -0
- data/config/locales/en.yml +10 -2
- data/db/migrate/20261009000001_change_money_columns_to_decimal_19_4.rb +116 -0
- data/db/migrate/20261010000001_add_store_id_to_active_storage_blobs.rb +12 -0
- data/lib/generators/spree/dummy/templates/rails/test.rb +21 -2
- data/lib/spree/core/configuration.rb +18 -0
- data/lib/spree/core/engine.rb +26 -0
- data/lib/spree/core/preferences/invalid_preferences.rb +18 -0
- data/lib/spree/core/preferences/json_conversion.rb +5 -1
- data/lib/spree/core/preferences/masking.rb +51 -1
- data/lib/spree/core/preferences/preferable.rb +212 -26
- data/lib/spree/core/preferences/preferable_class_methods.rb +121 -4
- data/lib/spree/core/ransack_configuration.rb +13 -2
- data/lib/spree/core/version.rb +1 -1
- data/lib/spree/core.rb +6 -3
- data/lib/spree/iso_data.rb +7 -0
- data/lib/spree/localized_number.rb +6 -0
- data/lib/spree/migrations.rb +4 -1
- data/lib/spree/money.rb +196 -15
- data/lib/spree/reporting/adapters/base.rb +7 -6
- data/lib/spree/reporting/registry.rb +13 -0
- data/lib/spree/testing_support/factories/product_factory.rb +9 -0
- data/lib/spree/upgrades/5_6_to_6_0/manifest.yml +22 -1
- data/lib/spree/uploads.rb +101 -0
- data/lib/tasks/markets.rake +2 -1
- data/lib/tasks/migrate_users_to_customers.rake +23 -18
- data/lib/tasks/money.rake +46 -0
- data/lib/tasks/seller_sample_data.rake +2 -2
- data/lib/tasks/store_settings.rake +13 -6
- data/lib/tasks/typed_adjustments_migration.rake +1 -1
- data/lib/tasks/uploads.rake +19 -0
- metadata +35 -6
- data/app/jobs/spree/products/touch_categories_job.rb +0 -15
- data/app/jobs/spree/products/touch_taxons_job.rb +0 -13
|
@@ -20,7 +20,8 @@ module Spree
|
|
|
20
20
|
.group("#{Variant.table_name}.product_id")
|
|
21
21
|
.to_sql
|
|
22
22
|
|
|
23
|
-
|
|
23
|
+
# Above any price decimal(19,4) holds, so an unpriced product sorts last.
|
|
24
|
+
price_expr = Arel.sql('COALESCE(price_agg.agg_price, 1000000000000000)')
|
|
24
25
|
|
|
25
26
|
joins("LEFT JOIN (#{price_agg_sql}) AS price_agg ON price_agg.product_id = #{Product.table_name}.id").
|
|
26
27
|
select("#{Product.table_name}.*").
|
|
@@ -109,6 +110,9 @@ module Spree
|
|
|
109
110
|
#
|
|
110
111
|
# Spree::Product.in_taxon(taxon).count(distinct: true)
|
|
111
112
|
scope :in_taxon, ->(taxon) {
|
|
113
|
+
taxon = Spree::Category.find_by_prefix_id(taxon) if taxon.is_a?(String)
|
|
114
|
+
next none if taxon.nil?
|
|
115
|
+
|
|
112
116
|
joins(:product_categories).
|
|
113
117
|
where("#{Spree::ProductCategory.table_name}.category_id" => taxon.cached_self_and_descendants_ids).distinct
|
|
114
118
|
}
|
|
@@ -17,8 +17,8 @@ module Spree
|
|
|
17
17
|
MAX_PO_DOCUMENT_SIZE = 10.megabytes
|
|
18
18
|
|
|
19
19
|
# What a purchase order plausibly arrives as: an export from the buyer's
|
|
20
|
-
# procurement system, a scan, or a photo of
|
|
21
|
-
#
|
|
20
|
+
# procurement system (often a spreadsheet or CSV), a scan, or a photo of
|
|
21
|
+
# one.
|
|
22
22
|
PO_DOCUMENT_CONTENT_TYPES = %w[
|
|
23
23
|
application/pdf
|
|
24
24
|
image/jpeg
|
|
@@ -27,6 +27,10 @@ module Spree
|
|
|
27
27
|
image/webp
|
|
28
28
|
application/msword
|
|
29
29
|
application/vnd.openxmlformats-officedocument.wordprocessingml.document
|
|
30
|
+
application/vnd.ms-excel
|
|
31
|
+
application/vnd.openxmlformats-officedocument.spreadsheetml.sheet
|
|
32
|
+
application/vnd.oasis.opendocument.spreadsheet
|
|
33
|
+
text/csv
|
|
30
34
|
].freeze
|
|
31
35
|
|
|
32
36
|
included do
|
|
@@ -36,6 +40,7 @@ module Spree
|
|
|
36
40
|
# internal cost codes, and is served only through an authenticated
|
|
37
41
|
# download action.
|
|
38
42
|
has_one_attached :po_document, service: Spree.private_storage_service_name
|
|
43
|
+
signed_id_attachments :po_document
|
|
39
44
|
|
|
40
45
|
validates :po_document,
|
|
41
46
|
size: { less_than_or_equal_to: MAX_PO_DOCUMENT_SIZE },
|
|
@@ -58,7 +58,7 @@ module Spree
|
|
|
58
58
|
|
|
59
59
|
# @return [BigDecimal]
|
|
60
60
|
def total_applied_store_credit
|
|
61
|
-
if payments
|
|
61
|
+
if read_association_in_memory?(:payments)
|
|
62
62
|
payments.
|
|
63
63
|
find_all(&:store_credit?).
|
|
64
64
|
reject(&:has_invalid_status?).
|
|
@@ -68,10 +68,32 @@ module Spree
|
|
|
68
68
|
end
|
|
69
69
|
end
|
|
70
70
|
|
|
71
|
+
# Balance still to collect, never negative. Store credit already applied
|
|
72
|
+
# but not yet captured is taken off; captured credit is a completed
|
|
73
|
+
# payment, so the balance has counted it already.
|
|
74
|
+
#
|
|
75
|
+
# @return [BigDecimal]
|
|
76
|
+
def amount_due
|
|
77
|
+
[outstanding_balance - uncaptured_store_credit, 0].max
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
# What store credit paid, apart from gift cards. A gift card is redeemed
|
|
81
|
+
# as store credit, but it is reported on its own as +gift_card_total+, so
|
|
82
|
+
# a summary showing both rows deducts each amount once.
|
|
83
|
+
#
|
|
84
|
+
# @return [BigDecimal]
|
|
85
|
+
def store_credit_total
|
|
86
|
+
total_applied_store_credit - gift_card_total
|
|
87
|
+
end
|
|
88
|
+
|
|
71
89
|
def using_store_credit?
|
|
72
90
|
total_applied_store_credit > 0
|
|
73
91
|
end
|
|
74
92
|
|
|
93
|
+
def display_store_credit_total
|
|
94
|
+
Spree::Money.new(-store_credit_total, currency: currency)
|
|
95
|
+
end
|
|
96
|
+
|
|
75
97
|
def display_total_applicable_store_credit
|
|
76
98
|
Spree::Money.new(-total_applicable_store_credit, currency: currency)
|
|
77
99
|
end
|
|
@@ -95,6 +117,19 @@ module Spree
|
|
|
95
117
|
def display_total_minus_store_credits
|
|
96
118
|
Spree::Money.new(total_minus_store_credits, currency: currency)
|
|
97
119
|
end
|
|
120
|
+
|
|
121
|
+
private
|
|
122
|
+
|
|
123
|
+
# @return [BigDecimal]
|
|
124
|
+
def uncaptured_store_credit
|
|
125
|
+
if read_association_in_memory?(:payments)
|
|
126
|
+
payments.
|
|
127
|
+
select { |payment| payment.store_credit? && !payment.has_invalid_status? && !payment.completed? }.
|
|
128
|
+
sum(BigDecimal('0'), &:amount)
|
|
129
|
+
else
|
|
130
|
+
payments.store_credits.valid.where.not(status: 'completed').sum(:amount)
|
|
131
|
+
end
|
|
132
|
+
end
|
|
98
133
|
end
|
|
99
134
|
end
|
|
100
135
|
end
|
|
@@ -39,7 +39,7 @@ module Spree
|
|
|
39
39
|
|
|
40
40
|
line_items.sum(BigDecimal('0')) do |line_item|
|
|
41
41
|
gifted_quantity = excluded_gift_promo_actions.sum { |action| action.gifted_quantity_of(line_item) }
|
|
42
|
-
line_item.price * [gifted_quantity, line_item.quantity].min
|
|
42
|
+
Spree::Money::Rounding.to_currency(line_item.price * [gifted_quantity, line_item.quantity].min, line_item.currency)
|
|
43
43
|
end
|
|
44
44
|
end
|
|
45
45
|
|
|
@@ -75,13 +75,6 @@ module Spree
|
|
|
75
75
|
outstanding_balance != 0
|
|
76
76
|
end
|
|
77
77
|
|
|
78
|
-
# Balance still to collect after applied store credit, never negative.
|
|
79
|
-
#
|
|
80
|
-
# @return [BigDecimal]
|
|
81
|
-
def amount_due
|
|
82
|
-
[outstanding_balance - total_applied_store_credit, 0].max
|
|
83
|
-
end
|
|
84
|
-
|
|
85
78
|
# @return [Boolean]
|
|
86
79
|
def paid?
|
|
87
80
|
total.positive? && payment_total >= total
|
|
@@ -11,6 +11,12 @@ module Spree::RansackableAttributes
|
|
|
11
11
|
class_attribute :whitelisted_ransackable_attributes
|
|
12
12
|
class_attribute :whitelisted_ransackable_scopes
|
|
13
13
|
|
|
14
|
+
# `{ 'price_between' => %w[decimal decimal], 'in_categories' => { list: 'id' } }`
|
|
15
|
+
# — the arguments a ransackable scope takes, which Ruby cannot report for
|
|
16
|
+
# a `scope` lambda. A scope taking none is `'boolean'`; an undeclared one
|
|
17
|
+
# is published as taking one text value.
|
|
18
|
+
class_attribute :ransackable_scope_types, default: {}
|
|
19
|
+
|
|
14
20
|
# Associations the Store API may filter through. Empty by default: every
|
|
15
21
|
# hop is a join into data the storefront was never meant to query, and a
|
|
16
22
|
# join back to the model it came from lets one filter chain them forever.
|
|
@@ -21,6 +27,9 @@ module Spree::RansackableAttributes
|
|
|
21
27
|
class_attribute :private_ransackable_attributes, default: {}
|
|
22
28
|
class_attribute :private_ransackable_scopes, default: {}
|
|
23
29
|
|
|
30
|
+
# Attributes the `search` filter matches, set with {.search_by}.
|
|
31
|
+
class_attribute :search_attributes, default: [], instance_accessor: false
|
|
32
|
+
|
|
24
33
|
class_attribute :default_ransackable_attributes
|
|
25
34
|
# `position` is included so any `acts_as_list` model is sortable by it
|
|
26
35
|
# without each subclass having to opt in. Ransack ignores attributes
|
|
@@ -48,10 +57,47 @@ module Spree::RansackableAttributes
|
|
|
48
57
|
def self.ransackable_scopes(auth_object = nil)
|
|
49
58
|
base = (whitelisted_ransackable_scopes || []).map(&:to_s)
|
|
50
59
|
base |= Spree.ransack.custom_scopes_for(self).map(&:to_s)
|
|
60
|
+
base |= %w[search] if search_attributes.any? && search_attributes.all? { |path| ransackable_path?(path, auth_object) }
|
|
51
61
|
return base unless Spree::RansackableAttributes.restricted_audience?(auth_object)
|
|
52
62
|
|
|
53
63
|
base - Array(private_ransackable_scopes[auth_object.to_sym])
|
|
54
64
|
end
|
|
65
|
+
|
|
66
|
+
# Declares the `search` filter (`q[search]=term`): a case-insensitive
|
|
67
|
+
# partial match on any of the given attributes, which may reach through
|
|
68
|
+
# associations (`variant_sku`). It runs as Ransack's `_cont` on those
|
|
69
|
+
# attributes, so translated names match in the current locale. An
|
|
70
|
+
# audience that may not filter on every one of them does not get it.
|
|
71
|
+
#
|
|
72
|
+
# search_by :name, :code
|
|
73
|
+
#
|
|
74
|
+
# @param attributes [Array<Symbol, String>]
|
|
75
|
+
def self.search_by(*attributes)
|
|
76
|
+
self.search_attributes = attributes.map(&:to_s).freeze
|
|
77
|
+
scope :search, ->(query) {
|
|
78
|
+
query.blank? ? all : ransack("#{search_attributes.join('_or_')}_cont" => query.to_s.strip).result
|
|
79
|
+
}
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
# Whether an attribute, or one reached through associations
|
|
83
|
+
# (`variant_product_name`), is filterable by this audience.
|
|
84
|
+
#
|
|
85
|
+
# @param path [String]
|
|
86
|
+
# @param auth_object [Symbol, nil]
|
|
87
|
+
# @return [Boolean]
|
|
88
|
+
def self.ransackable_path?(path, auth_object = nil, depth = 0)
|
|
89
|
+
return true if ransackable_attributes(auth_object).include?(path)
|
|
90
|
+
return false if depth >= 2
|
|
91
|
+
|
|
92
|
+
ransackable_associations(auth_object).any? do |name|
|
|
93
|
+
next false unless path.start_with?("#{name}_")
|
|
94
|
+
|
|
95
|
+
reflection = reflect_on_association(name.to_sym)
|
|
96
|
+
next false if reflection.nil? || reflection.polymorphic? || !reflection.klass.respond_to?(:ransackable_path?)
|
|
97
|
+
|
|
98
|
+
reflection.klass.ransackable_path?(path.delete_prefix("#{name}_"), auth_object, depth + 1)
|
|
99
|
+
end
|
|
100
|
+
end
|
|
55
101
|
end
|
|
56
102
|
|
|
57
103
|
# @param auth_object [Symbol, nil] the Ransack auth object a search ran with
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
module Spree
|
|
2
|
+
# Gives attachment slots their API write name, `<slot>_signed_id`
|
|
3
|
+
# (docs/plans/6.0-uploads-and-file-ownership.md).
|
|
4
|
+
#
|
|
5
|
+
# signed_id_attachments :logo, :mailer_logo
|
|
6
|
+
# store.update(logo_signed_id: upload.signed_id) # attaches
|
|
7
|
+
# store.update(logo_signed_id: nil) # removes
|
|
8
|
+
#
|
|
9
|
+
# A declared slot also refuses a file stored with the other visibility:
|
|
10
|
+
# attaching never moves a file between storage services, so a public upload
|
|
11
|
+
# attached to a private slot would stay readable at a public URL.
|
|
12
|
+
module SignedIdAttachments
|
|
13
|
+
extend ActiveSupport::Concern
|
|
14
|
+
|
|
15
|
+
included do
|
|
16
|
+
class_attribute :signed_id_attachment_slots, instance_writer: false, default: []
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
class_methods do
|
|
20
|
+
# @param slots [Array<Symbol>] `has_one_attached` slot names
|
|
21
|
+
# @return [void]
|
|
22
|
+
def signed_id_attachments(*slots)
|
|
23
|
+
validate :signed_id_attachments_on_their_storage if signed_id_attachment_slots.empty?
|
|
24
|
+
self.signed_id_attachment_slots += slots
|
|
25
|
+
|
|
26
|
+
slots.each do |slot|
|
|
27
|
+
define_method(:"#{slot}_signed_id=") { |signed_id| public_send(:"#{slot}=", signed_id) }
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
# The name the API reports +attribute+'s errors under: `logo_signed_id`
|
|
32
|
+
# for the `logo` slot, so a client reads them back under the field it sent.
|
|
33
|
+
#
|
|
34
|
+
# @param attribute [Symbol, String]
|
|
35
|
+
# @return [String, nil] nil when +attribute+ is not a declared slot
|
|
36
|
+
def signed_id_attribute_for(attribute)
|
|
37
|
+
"#{attribute}_signed_id" if signed_id_attachment_slots.include?(attribute.to_sym)
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
private
|
|
42
|
+
|
|
43
|
+
def signed_id_attachments_on_their_storage
|
|
44
|
+
signed_id_attachment_slots.each do |slot|
|
|
45
|
+
blob = attachment_changes[slot.to_s].try(:blob)
|
|
46
|
+
expected_service = self.class.reflect_on_attachment(slot)&.options&.dig(:service_name)
|
|
47
|
+
next if blob.nil? || expected_service.nil? || blob.service_name.to_s == expected_service.to_s
|
|
48
|
+
|
|
49
|
+
visibility = expected_service.to_s == Spree.private_storage_service_name.to_s ? 'private' : 'public'
|
|
50
|
+
errors.add(slot, :wrong_visibility, message: I18n.t('spree.upload_wrong_visibility', visibility: visibility))
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
end
|
|
@@ -6,7 +6,8 @@ module Spree
|
|
|
6
6
|
extend ActiveSupport::Concern
|
|
7
7
|
|
|
8
8
|
included do
|
|
9
|
-
Spree::Emails::Branding.
|
|
9
|
+
Spree::Emails::Branding::COLORS.each { |name| preference :"email_#{name}", :string, format: :color }
|
|
10
|
+
preference :email_font, :string, choices: -> { Spree::Emails::Branding::FONTS.keys }
|
|
10
11
|
|
|
11
12
|
validate :email_branding_valid
|
|
12
13
|
end
|
|
@@ -7,6 +7,9 @@ module Spree
|
|
|
7
7
|
has_many :markets, class_name: 'Spree::Market', dependent: :destroy
|
|
8
8
|
has_one :default_market, -> { default }, class_name: 'Spree::Market'
|
|
9
9
|
|
|
10
|
+
normalizes :default_locale, with: ->(value) { value.to_s.strip.presence }
|
|
11
|
+
validates_with Spree::LocaleCodesValidator, attributes: %i[default_locale supported_locales]
|
|
12
|
+
|
|
10
13
|
after_create :ensure_default_market
|
|
11
14
|
after_update :sync_default_market_settings
|
|
12
15
|
end
|
|
@@ -145,7 +148,7 @@ module Spree
|
|
|
145
148
|
markets.create!(
|
|
146
149
|
name: country.name,
|
|
147
150
|
currency: read_attribute(:default_currency) || iso_country&.currency_code || 'USD',
|
|
148
|
-
default_locale: read_attribute(:default_locale) || iso_country&.languages_official&.first || 'en',
|
|
151
|
+
default_locale: read_attribute(:default_locale) || Spree::Locales.listed(iso_country&.languages_official&.first) || 'en',
|
|
149
152
|
supported_locales: read_attribute(:supported_locales),
|
|
150
153
|
default: true,
|
|
151
154
|
bootstrap_default: true,
|
|
@@ -190,7 +193,7 @@ module Spree
|
|
|
190
193
|
end
|
|
191
194
|
|
|
192
195
|
def legacy_supported_locales_list
|
|
193
|
-
(read_attribute(:supported_locales).to_s.split(',') << default_locale).
|
|
196
|
+
(read_attribute(:supported_locales).to_s.split(',').map(&:strip) << default_locale).compact_blank.uniq.sort
|
|
194
197
|
end
|
|
195
198
|
end
|
|
196
199
|
end
|
|
@@ -1,11 +1,14 @@
|
|
|
1
1
|
module Spree
|
|
2
2
|
# Shared shape of the typed adjustment rows (TaxLine, Discount, Fee):
|
|
3
3
|
# dual-FK cart/order owner (exactly one), consolidated +metadata+ JSON
|
|
4
|
-
# column, money display and
|
|
4
|
+
# column, money display and strict amount parsing.
|
|
5
5
|
module TypedAdjustmentLine
|
|
6
6
|
extend ActiveSupport::Concern
|
|
7
7
|
|
|
8
8
|
included do
|
|
9
|
+
include Spree::CanonicalDecimals
|
|
10
|
+
|
|
11
|
+
canonical_decimals :amount
|
|
9
12
|
attribute :metadata, default: -> { {} }
|
|
10
13
|
|
|
11
14
|
belongs_to :order, class_name: 'Spree::Order', optional: true
|
|
@@ -38,10 +41,6 @@ module Spree
|
|
|
38
41
|
end
|
|
39
42
|
end
|
|
40
43
|
|
|
41
|
-
def amount=(amount)
|
|
42
|
-
self[:amount] = Spree::LocalizedNumber.parse(amount)
|
|
43
|
-
end
|
|
44
|
-
|
|
45
44
|
def currency
|
|
46
45
|
owner&.currency
|
|
47
46
|
end
|
|
@@ -53,7 +53,7 @@ module Spree
|
|
|
53
53
|
|
|
54
54
|
def reconcile_typed_association(association, rows)
|
|
55
55
|
collection = public_send(association)
|
|
56
|
-
kept_ids = rows.filter_map { |row| save_typed_association_row(collection, row) }
|
|
56
|
+
kept_ids = rows.each_with_index.filter_map { |row, index| save_typed_association_row(collection, row, "/#{association.to_s.split('_').last}/#{index}") }
|
|
57
57
|
if kept_ids.any? || rows.empty?
|
|
58
58
|
collection.where.not(id: kept_ids).destroy_all
|
|
59
59
|
# `where.not(...).destroy_all` deletes the dropped rows from the DB but
|
|
@@ -64,7 +64,8 @@ module Spree
|
|
|
64
64
|
end
|
|
65
65
|
end
|
|
66
66
|
|
|
67
|
-
|
|
67
|
+
# @param pointer [String] where the row sits in the request, e.g. `/rules/1`
|
|
68
|
+
def save_typed_association_row(collection, row, pointer)
|
|
68
69
|
record = find_or_build_typed_association_row(collection, row)
|
|
69
70
|
return nil unless record
|
|
70
71
|
|
|
@@ -78,12 +79,10 @@ module Spree
|
|
|
78
79
|
deferred_ids, scalar_attrs = attrs.partition { |k, _| record.new_record? && k.to_s.end_with?('_ids') }
|
|
79
80
|
record.assign_attributes(scalar_attrs.to_h) if scalar_attrs.any?
|
|
80
81
|
|
|
81
|
-
preferences
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
record.set_preference(key.to_sym, decode_preference_value(key, value))
|
|
82
|
+
record.assign_preferences(preferences, pointer: "#{pointer}/preferences") if preferences
|
|
83
|
+
if calculator.present? && record.respond_to?(:assign_calculator_attributes)
|
|
84
|
+
record.assign_calculator_attributes(calculator, pointer: "#{pointer}/calculator")
|
|
85
85
|
end
|
|
86
|
-
record.assign_calculator_attributes(calculator) if calculator.present? && record.respond_to?(:assign_calculator_attributes)
|
|
87
86
|
|
|
88
87
|
# Always save — `record.changed?` doesn't reflect preferences
|
|
89
88
|
# (serialized hash) or calculator association changes.
|
|
@@ -101,17 +100,6 @@ module Spree
|
|
|
101
100
|
raise
|
|
102
101
|
end
|
|
103
102
|
|
|
104
|
-
# Decode `*_ids` array preferences (`customer_group_ids`, `user_ids`,
|
|
105
|
-
# …) from prefixed strings to raw PKs. Plain-scalar / non-id
|
|
106
|
-
# preferences pass through unchanged.
|
|
107
|
-
def decode_preference_value(key, value)
|
|
108
|
-
return value unless key.to_s.end_with?('_ids') && value.is_a?(Array)
|
|
109
|
-
|
|
110
|
-
value.map do |v|
|
|
111
|
-
Spree::PrefixedId.prefixed_id?(v) ? Spree::PrefixedId.decode_prefixed_id(v) : v
|
|
112
|
-
end
|
|
113
|
-
end
|
|
114
|
-
|
|
115
103
|
def find_or_build_typed_association_row(collection, row)
|
|
116
104
|
if row[:id].present?
|
|
117
105
|
id = Spree::PrefixedId.prefixed_id?(row[:id]) ? Spree::PrefixedId.decode_prefixed_id(row[:id]) : row[:id]
|
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
module Spree
|
|
2
2
|
# Restates a VAT-inclusive price for a customer in another country: the
|
|
3
3
|
# home-country VAT comes out, the destination's goes on.
|
|
4
|
+
#
|
|
5
|
+
# The including class answers +currency+, which the restated amount is
|
|
6
|
+
# rounded to.
|
|
4
7
|
module VatPriceCalculation
|
|
5
8
|
# Cheapest test first: a domestic sale needs no restatement whatever the
|
|
6
9
|
# provider is, so the provider lookup (a constantize and an instantiation) is
|
|
@@ -11,7 +14,7 @@ module Spree
|
|
|
11
14
|
return amount if amount.nil? || !outside_default_vat_zone?(price_options)
|
|
12
15
|
return amount unless restatement_available?(price_options)
|
|
13
16
|
|
|
14
|
-
|
|
17
|
+
Spree::Money::Rounding.to_currency(add_foreign_vat_for(amount, price_options), currency)
|
|
15
18
|
end
|
|
16
19
|
|
|
17
20
|
private
|
|
@@ -66,7 +69,7 @@ module Spree
|
|
|
66
69
|
lookup[:country] = country_from(price_options)
|
|
67
70
|
end
|
|
68
71
|
|
|
69
|
-
Spree::TaxRate.included_tax_amount_for(lookup).
|
|
72
|
+
Spree::TaxRate.included_tax_amount_for(lookup).to_d
|
|
70
73
|
end
|
|
71
74
|
|
|
72
75
|
# +current_price_options+ is documented as an override point, so an app may
|
|
@@ -92,9 +95,5 @@ module Spree
|
|
|
92
95
|
|
|
93
96
|
@default_tax_country = Spree::Current.store&.default_country
|
|
94
97
|
end
|
|
95
|
-
|
|
96
|
-
def round_to_two_places(amount)
|
|
97
|
-
Spree::Money::Rounding.quantize(amount, 2)
|
|
98
|
-
end
|
|
99
98
|
end
|
|
100
99
|
end
|
data/app/models/spree/address.rb
CHANGED
|
@@ -206,7 +206,7 @@ module Spree
|
|
|
206
206
|
scope.first
|
|
207
207
|
end
|
|
208
208
|
|
|
209
|
-
self.whitelisted_ransackable_attributes = ADDRESS_FIELDS + %w[country_code state_code]
|
|
209
|
+
self.whitelisted_ransackable_attributes = ADDRESS_FIELDS - %w[country state] + %w[country_code state_code]
|
|
210
210
|
self.whitelisted_ransackable_associations = %w[owner]
|
|
211
211
|
|
|
212
212
|
def self.required_fields
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
module Spree
|
|
2
2
|
module Adjusters
|
|
3
|
-
# Largest-remainder apportionment of an integer
|
|
3
|
+
# Largest-remainder apportionment of an integer total, in the currency's
|
|
4
|
+
# minor units, across weights.
|
|
4
5
|
# Shared by promotion order-level distribution and manual admin discounts
|
|
5
6
|
# so both split identically (sum of shares always equals the total).
|
|
6
7
|
module LargestRemainder
|
|
@@ -19,6 +20,18 @@ module Spree
|
|
|
19
20
|
end
|
|
20
21
|
shares
|
|
21
22
|
end
|
|
23
|
+
|
|
24
|
+
# Splits an amount across weights in the currency's minor units, so the
|
|
25
|
+
# shares are amounts that sum exactly to the one given.
|
|
26
|
+
#
|
|
27
|
+
# @param amount [BigDecimal]
|
|
28
|
+
# @param weights [Array<Numeric>] proportional bases (sum must be > 0)
|
|
29
|
+
# @param currency [String]
|
|
30
|
+
# @return [Array<BigDecimal>]
|
|
31
|
+
def apportion(amount, weights, currency)
|
|
32
|
+
units = Spree::Money::Rounding.to_minor_units(amount, currency)
|
|
33
|
+
largest_remainder_shares(units, weights).map { |share| Spree::Money::Rounding.from_minor_units(share, currency) }
|
|
34
|
+
end
|
|
22
35
|
end
|
|
23
36
|
end
|
|
24
37
|
end
|
|
@@ -140,7 +140,7 @@ module Spree
|
|
|
140
140
|
end
|
|
141
141
|
end
|
|
142
142
|
|
|
143
|
-
# Largest-remainder distribution (in
|
|
143
|
+
# Largest-remainder distribution (in minor units) of the order-wide winner over
|
|
144
144
|
# each line's remaining discounted base, so shares always sum exactly to
|
|
145
145
|
# the promotion amount and no line goes below zero.
|
|
146
146
|
def distribute_over_line_items(chosen)
|
|
@@ -149,11 +149,10 @@ module Spree
|
|
|
149
149
|
bases_sum = bases.sum
|
|
150
150
|
return if bases_sum <= 0
|
|
151
151
|
|
|
152
|
-
|
|
153
|
-
shares = Spree::Adjusters::LargestRemainder.largest_remainder_shares(total_cents.round, bases)
|
|
152
|
+
shares = Spree::Adjusters::LargestRemainder.apportion([chosen[:amount].abs, bases_sum].min, bases, order.currency)
|
|
154
153
|
|
|
155
154
|
line_items.each_with_index do |line_item, index|
|
|
156
|
-
amount = -
|
|
155
|
+
amount = -shares[index]
|
|
157
156
|
next if amount.zero?
|
|
158
157
|
|
|
159
158
|
persist_discount(line_item, chosen, amount)
|
|
@@ -10,6 +10,9 @@ module Spree
|
|
|
10
10
|
|
|
11
11
|
include Spree::SingleStoreResource
|
|
12
12
|
|
|
13
|
+
self.whitelisted_ransackable_attributes = %w[origin]
|
|
14
|
+
search_by :origin
|
|
15
|
+
|
|
13
16
|
|
|
14
17
|
validates :origin, presence: true
|
|
15
18
|
validates :origin, uniqueness: { scope: [:store_id, *spree_base_uniqueness_scope] }
|
data/app/models/spree/base.rb
CHANGED
|
@@ -9,6 +9,7 @@ class Spree::Base < ApplicationRecord
|
|
|
9
9
|
include Spree::HasNumber
|
|
10
10
|
include Spree::HasIsoGeography
|
|
11
11
|
include Spree::TypedAssociations
|
|
12
|
+
include Spree::SignedIdAttachments
|
|
12
13
|
include Spree::ValidatesStoreUniqueness
|
|
13
14
|
|
|
14
15
|
# Extra writable attributes contributed by extensions, appended to the v3
|
|
@@ -68,6 +69,18 @@ class Spree::Base < ApplicationRecord
|
|
|
68
69
|
true
|
|
69
70
|
end
|
|
70
71
|
|
|
72
|
+
# Whether to answer from an association's rows rather than a query of its
|
|
73
|
+
# own (a SUM, an EXISTS). True when the rows are loaded already, or when
|
|
74
|
+
# this record came from a list loaded with +preload_associations_lazily+:
|
|
75
|
+
# loading them then fetches the rows for the whole list in one query, where
|
|
76
|
+
# an aggregate would run once per record.
|
|
77
|
+
#
|
|
78
|
+
# @param name [Symbol] the association
|
|
79
|
+
# @return [Boolean]
|
|
80
|
+
def read_association_in_memory?(name)
|
|
81
|
+
association(name).loaded? || lazy_preload_context.present?
|
|
82
|
+
end
|
|
83
|
+
|
|
71
84
|
# @see Spree.mysql? — the single definition; this reads it so a model can
|
|
72
85
|
# branch without reaching for the connection itself.
|
|
73
86
|
def mysql_adapter?
|
|
@@ -151,6 +164,22 @@ class Spree::Base < ApplicationRecord
|
|
|
151
164
|
end
|
|
152
165
|
end
|
|
153
166
|
|
|
167
|
+
# How a filter on one of this model's columns turns the short name a client
|
|
168
|
+
# sends (`purchase_order`) into the class name the column stores, or nil
|
|
169
|
+
# when the column does not store class names. Covers the STI column of a
|
|
170
|
+
# typed family and polymorphic `*_type` columns; a model with another such
|
|
171
|
+
# column overrides this.
|
|
172
|
+
#
|
|
173
|
+
# @param attribute [String]
|
|
174
|
+
# @return [Proc, nil] short name → stored class name
|
|
175
|
+
def self.api_type_resolver(attribute)
|
|
176
|
+
if attribute == inheritance_column && api_type_registry.any?
|
|
177
|
+
->(api_type) { class_name_for_api_type(api_type) }
|
|
178
|
+
elsif reflect_on_all_associations(:belongs_to).any? { |reflection| reflection.polymorphic? && reflection.foreign_type == attribute }
|
|
179
|
+
->(api_type) { Spree::Base.polymorphic_type_for(api_type) }
|
|
180
|
+
end
|
|
181
|
+
end
|
|
182
|
+
|
|
154
183
|
# Shorthand for a *polymorphic* `*_type` column (`owner_type`,
|
|
155
184
|
# `viewable_type`, …), where the value names an arbitrary model rather than a
|
|
156
185
|
# subclass of the serialized one — so `api_type_for`'s registry lookup does
|
|
@@ -7,7 +7,7 @@ module Spree
|
|
|
7
7
|
end
|
|
8
8
|
|
|
9
9
|
def compute(object)
|
|
10
|
-
computed_amount = (object.amount * preferred_flat_percent / 100
|
|
10
|
+
computed_amount = Spree::Money::Rounding.to_currency(object.amount * preferred_flat_percent / 100, object.try(:currency))
|
|
11
11
|
|
|
12
12
|
# We don't want to cause the promotion adjustments to push the order into a negative total.
|
|
13
13
|
if computed_amount > object.amount
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
module Spree
|
|
2
2
|
class Calculator::FlatRate < Calculator
|
|
3
|
-
preference :amount, :
|
|
4
|
-
preference :currency, :string, default: -> { Spree::Store.default.default_currency }
|
|
3
|
+
preference :amount, :money, default: 0
|
|
4
|
+
preference :currency, :string, format: :currency, default: -> { Spree::Store.default.default_currency }
|
|
5
5
|
preference :apply_only_on_full_priced_items, :boolean, default: false
|
|
6
6
|
|
|
7
7
|
def self.description
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
module Spree
|
|
2
2
|
class Calculator::FlexiRate < Calculator
|
|
3
|
-
preference :first_item, :
|
|
4
|
-
preference :additional_item, :
|
|
3
|
+
preference :first_item, :money, default: 0.0
|
|
4
|
+
preference :additional_item, :money, default: 0.0
|
|
5
5
|
preference :max_items, :integer, default: 0
|
|
6
|
-
preference :currency, :string, default: -> { Spree::Store.default.default_currency }
|
|
6
|
+
preference :currency, :string, format: :currency, default: -> { Spree::Store.default.default_currency }
|
|
7
7
|
preference :apply_only_on_full_priced_items, :boolean, default: false
|
|
8
8
|
|
|
9
9
|
def self.description
|
|
@@ -11,7 +11,7 @@ module Spree
|
|
|
11
11
|
def compute(object)
|
|
12
12
|
return 0 if preferred_apply_only_on_full_priced_items && object.variant.compare_at_amount_in(object.currency).present?
|
|
13
13
|
|
|
14
|
-
computed_amount = (object.amount * preferred_percent / 100
|
|
14
|
+
computed_amount = Spree::Money::Rounding.to_currency(object.amount * preferred_percent / 100, object.currency)
|
|
15
15
|
|
|
16
16
|
# We don't want to cause the promotion adjustments to push the order into a negative total.
|
|
17
17
|
if computed_amount > object.amount
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
module Spree
|
|
2
2
|
class Calculator::PriceSack < Calculator
|
|
3
|
-
preference :minimal_amount, :
|
|
4
|
-
preference :normal_amount, :
|
|
5
|
-
preference :discount_amount, :
|
|
6
|
-
preference :currency, :string, default: -> { Spree::Store.default.default_currency }
|
|
3
|
+
preference :minimal_amount, :money, default: 0
|
|
4
|
+
preference :normal_amount, :money, default: 0
|
|
5
|
+
preference :discount_amount, :money, default: 0
|
|
6
|
+
preference :currency, :string, format: :currency, default: -> { Spree::Store.default.default_currency }
|
|
7
7
|
|
|
8
8
|
def self.description
|
|
9
9
|
I18n.t('spree.price_sack')
|
|
@@ -3,8 +3,8 @@ module Spree
|
|
|
3
3
|
class DigitalDelivery < ShippingCalculator
|
|
4
4
|
include Spree::Calculator::CurrencyAmounts
|
|
5
5
|
|
|
6
|
-
preference :amount, :
|
|
7
|
-
preference :currency, :string, default: -> { Spree::Store.default.default_currency }
|
|
6
|
+
preference :amount, :money, default: 0
|
|
7
|
+
preference :currency, :string, format: :currency, default: -> { Spree::Store.default.default_currency }
|
|
8
8
|
|
|
9
9
|
def self.description
|
|
10
10
|
I18n.t('spree.digital.digital_delivery')
|
|
@@ -8,12 +8,14 @@ module Spree
|
|
|
8
8
|
end
|
|
9
9
|
|
|
10
10
|
def compute_package(package)
|
|
11
|
-
compute_from_price(total(package.contents))
|
|
11
|
+
compute_from_price(total(package.contents), package.currency)
|
|
12
12
|
end
|
|
13
13
|
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
14
|
+
# @param price [BigDecimal]
|
|
15
|
+
# @param currency [String, nil] the amount is rounded to its minor unit
|
|
16
|
+
# @return [BigDecimal]
|
|
17
|
+
def compute_from_price(price, currency = nil)
|
|
18
|
+
Spree::Money::Rounding.to_currency(price * BigDecimal(preferred_flat_percent.to_s) / 100, currency)
|
|
17
19
|
end
|
|
18
20
|
end
|
|
19
21
|
end
|