spree_core 5.4.0.rc5 → 5.4.0.rc7
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/app/mailers/spree/base_mailer.rb +2 -0
- data/app/models/concerns/spree/default_price.rb +108 -6
- data/app/models/concerns/spree/stores/markets.rb +4 -2
- data/app/models/spree/exports/product_translations.rb +45 -0
- data/app/models/spree/import_schemas/product_translations.rb +14 -0
- data/app/models/spree/imports/product_translations.rb +17 -0
- data/app/models/spree/market.rb +1 -0
- data/app/models/spree/metafield.rb +1 -1
- data/app/models/spree/metafield_definition.rb +10 -0
- data/app/models/spree/option_type.rb +20 -2
- data/app/models/spree/option_value.rb +17 -1
- data/app/models/spree/order.rb +22 -2
- data/app/models/spree/price.rb +1 -1
- data/app/models/spree/product.rb +40 -13
- data/app/models/spree/search_provider/meilisearch.rb +41 -8
- data/app/models/spree/stock_location.rb +1 -1
- data/app/models/spree/stock_movement.rb +2 -1
- data/app/models/spree/variant.rb +13 -11
- data/app/presenters/spree/csv/product_translation_presenter.rb +31 -0
- data/app/presenters/spree/csv/product_variant_presenter.rb +16 -3
- data/app/presenters/spree/search_provider/product_presenter.rb +8 -13
- data/app/services/spree/cart/create.rb +1 -0
- data/app/services/spree/carts/create.rb +1 -0
- data/app/services/spree/carts/update.rb +21 -0
- data/app/services/spree/imports/row_processors/product_translation.rb +45 -0
- data/app/services/spree/imports/row_processors/product_variant.rb +11 -3
- data/app/services/spree/products/prepare_nested_attributes.rb +18 -1
- data/app/services/spree/sample_data/loader.rb +31 -20
- data/app/services/spree/seeds/all.rb +22 -20
- data/app/views/spree/shared/_mailer_line_item.html.erb +2 -2
- data/config/locales/en.yml +12 -0
- data/db/migrate/20250217171018_create_action_text_video_embeds.rb +11 -0
- data/db/migrate/20260402000001_add_kind_to_spree_option_types.rb +13 -0
- data/db/migrate/20260402000002_add_color_code_to_spree_option_values.rb +5 -0
- data/db/migrate/20260403000000_add_market_to_spree_orders.rb +5 -0
- data/db/sample_data/markets.rb +30 -0
- data/db/sample_data/metafield_definitions.rb +2 -2
- data/db/sample_data/options.rb +4 -0
- data/db/sample_data/orders.rb +2 -2
- data/db/sample_data/product_translations.csv +75 -0
- data/db/sample_data/products.csv +212 -1083
- data/lib/spree/core/configuration.rb +1 -0
- data/lib/spree/core/controller_helpers/order.rb +12 -10
- data/lib/spree/core/engine.rb +2 -0
- data/lib/spree/core/permission_configuration.rb +12 -0
- data/lib/spree/core/pricing/resolver.rb +5 -3
- data/lib/spree/core/version.rb +1 -1
- data/lib/spree/permitted_attributes.rb +3 -3
- data/lib/spree/testing_support/authorization_helpers.rb +16 -16
- data/lib/spree/testing_support/factories/import_factory.rb +12 -0
- data/lib/spree/testing_support/factories/options_factory.rb +11 -0
- data/lib/spree/testing_support/factories/product_factory.rb +12 -2
- data/lib/spree/testing_support/factories/variant_factory.rb +8 -2
- data/lib/tasks/markets.rake +5 -2
- data/lib/tasks/search.rake +3 -3
- data/lib/tasks/variants.rake +2 -2
- data/spec/fixtures/files/product_translations_import.csv +3 -0
- metadata +26 -7
- /data/lib/tasks/{images.rake → media.rake} +0 -0
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 07eb5e06c1dbe2e9d89b69a3768a376fcf5d23f0be28bb15bd4e9fac1ce0bcaf
|
|
4
|
+
data.tar.gz: 99e243c978fb95cfd04e4b8ee42cc42b63f3580493939b4271be7992006c2bbb
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 2967ba8a3825d98180adb93f0a025370c6aa6aee76520cf5400be56bfb6328b63f121434065f2acf8b600099f53136b067becec17c0f1523078dedd95cc4ccd5
|
|
7
|
+
data.tar.gz: ca2d9d12300bd8a765b3130cf86fd857d8c8b0ad7cc413f895956a806a642d5ef44c20983b08d227de1fff80b9bcfd68995173061fc60849ebe5dab185631ffb
|
|
@@ -2,24 +2,126 @@ module Spree
|
|
|
2
2
|
module DefaultPrice
|
|
3
3
|
extend ActiveSupport::Concern
|
|
4
4
|
|
|
5
|
+
DEPRECATION_MSG = 'Spree::DefaultPrice is deprecated and will be removed in Spree 6.0. ' \
|
|
6
|
+
'Use variant.set_price(currency, amount) and variant.price_in(currency) instead.'
|
|
7
|
+
|
|
5
8
|
included do
|
|
6
9
|
has_one :default_price,
|
|
7
10
|
-> { with_deleted.where(currency: Spree::Store.default.default_currency) },
|
|
8
11
|
class_name: 'Spree::Price',
|
|
9
12
|
dependent: :destroy
|
|
10
13
|
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
+
after_save :save_default_price, if: -> { Spree::Config.enable_legacy_default_price }
|
|
15
|
+
|
|
16
|
+
def price
|
|
17
|
+
Spree::Deprecation.warn(Spree::DefaultPrice::DEPRECATION_MSG)
|
|
18
|
+
if Spree::Config.enable_legacy_default_price
|
|
19
|
+
find_or_build_default_price.price
|
|
20
|
+
else
|
|
21
|
+
price_in(Spree::Store.default.default_currency).amount
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def price=(value)
|
|
26
|
+
Spree::Deprecation.warn(Spree::DefaultPrice::DEPRECATION_MSG)
|
|
27
|
+
if Spree::Config.enable_legacy_default_price
|
|
28
|
+
find_or_build_default_price.price = value
|
|
29
|
+
else
|
|
30
|
+
set_price(Spree::Store.default.default_currency, value)
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def currency
|
|
35
|
+
Spree::Deprecation.warn(Spree::DefaultPrice::DEPRECATION_MSG)
|
|
36
|
+
if Spree::Config.enable_legacy_default_price
|
|
37
|
+
find_or_build_default_price.currency
|
|
38
|
+
else
|
|
39
|
+
Spree::Store.default.default_currency
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def currency=(value)
|
|
44
|
+
Spree::Deprecation.warn(Spree::DefaultPrice::DEPRECATION_MSG)
|
|
45
|
+
if Spree::Config.enable_legacy_default_price
|
|
46
|
+
find_or_build_default_price.currency = value
|
|
47
|
+
end
|
|
48
|
+
# no-op when legacy is disabled — currency is determined by the store
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def display_price
|
|
52
|
+
Spree::Deprecation.warn(Spree::DefaultPrice::DEPRECATION_MSG)
|
|
53
|
+
if Spree::Config.enable_legacy_default_price
|
|
54
|
+
find_or_build_default_price.display_price
|
|
55
|
+
else
|
|
56
|
+
price_in(Spree::Store.default.default_currency).display_amount
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def display_amount
|
|
61
|
+
Spree::Deprecation.warn(Spree::DefaultPrice::DEPRECATION_MSG)
|
|
62
|
+
if Spree::Config.enable_legacy_default_price
|
|
63
|
+
find_or_build_default_price.display_amount
|
|
64
|
+
else
|
|
65
|
+
price_in(Spree::Store.default.default_currency).display_amount
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def compare_at_price
|
|
70
|
+
Spree::Deprecation.warn(Spree::DefaultPrice::DEPRECATION_MSG)
|
|
71
|
+
if Spree::Config.enable_legacy_default_price
|
|
72
|
+
find_or_build_default_price.compare_at_price
|
|
73
|
+
else
|
|
74
|
+
price_in(Spree::Store.default.default_currency).compare_at_amount
|
|
75
|
+
end
|
|
76
|
+
end
|
|
14
77
|
|
|
15
|
-
|
|
78
|
+
def compare_at_price=(value)
|
|
79
|
+
Spree::Deprecation.warn(Spree::DefaultPrice::DEPRECATION_MSG)
|
|
80
|
+
if Spree::Config.enable_legacy_default_price
|
|
81
|
+
find_or_build_default_price.compare_at_price = value
|
|
82
|
+
else
|
|
83
|
+
default_currency = Spree::Store.default.default_currency
|
|
84
|
+
price_record = price_in(default_currency)
|
|
85
|
+
price_record.compare_at_amount = value
|
|
86
|
+
price_record.save! if price_record.persisted?
|
|
87
|
+
end
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
def display_compare_at_price
|
|
91
|
+
Spree::Deprecation.warn(Spree::DefaultPrice::DEPRECATION_MSG)
|
|
92
|
+
if Spree::Config.enable_legacy_default_price
|
|
93
|
+
find_or_build_default_price.display_compare_at_price
|
|
94
|
+
else
|
|
95
|
+
price_in(Spree::Store.default.default_currency).display_compare_at_amount
|
|
96
|
+
end
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
def price_including_vat_for(price_options)
|
|
100
|
+
Spree::Deprecation.warn(Spree::DefaultPrice::DEPRECATION_MSG)
|
|
101
|
+
if Spree::Config.enable_legacy_default_price
|
|
102
|
+
find_or_build_default_price.price_including_vat_for(price_options)
|
|
103
|
+
else
|
|
104
|
+
price_in(Spree::Store.default.default_currency).price_including_vat_for(price_options)
|
|
105
|
+
end
|
|
106
|
+
end
|
|
16
107
|
|
|
17
108
|
def has_default_price?
|
|
18
|
-
|
|
109
|
+
Spree::Deprecation.warn(Spree::DefaultPrice::DEPRECATION_MSG)
|
|
110
|
+
if Spree::Config.enable_legacy_default_price
|
|
111
|
+
!default_price.nil?
|
|
112
|
+
else
|
|
113
|
+
prices.base_prices.any? { |p| p.currency == Spree::Store.default.default_currency }
|
|
114
|
+
end
|
|
19
115
|
end
|
|
20
116
|
|
|
21
117
|
def find_or_build_default_price
|
|
22
|
-
|
|
118
|
+
Spree::Deprecation.warn(Spree::DefaultPrice::DEPRECATION_MSG)
|
|
119
|
+
if Spree::Config.enable_legacy_default_price
|
|
120
|
+
default_price || build_default_price
|
|
121
|
+
else
|
|
122
|
+
prices.base_prices.find { |p| p.currency == Spree::Store.default.default_currency } ||
|
|
123
|
+
prices.build(currency: Spree::Store.default.default_currency)
|
|
124
|
+
end
|
|
23
125
|
end
|
|
24
126
|
|
|
25
127
|
private
|
|
@@ -104,12 +104,14 @@ module Spree
|
|
|
104
104
|
end
|
|
105
105
|
end
|
|
106
106
|
|
|
107
|
-
|
|
108
|
-
|
|
107
|
+
# Returns true if the store has markets, false otherwise
|
|
108
|
+
# @return [Boolean]
|
|
109
109
|
def has_markets?
|
|
110
110
|
@has_markets ||= persisted? && (markets.loaded? ? markets.any? : markets.exists?)
|
|
111
111
|
end
|
|
112
112
|
|
|
113
|
+
private
|
|
114
|
+
|
|
113
115
|
def legacy_supported_currencies_list
|
|
114
116
|
([default_currency] + read_attribute(:supported_currencies).to_s.split(',')).uniq.map(&:to_s).map do |code|
|
|
115
117
|
::Money::Currency.find(code.strip)
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
module Spree
|
|
2
|
+
module Exports
|
|
3
|
+
class ProductTranslations < Spree::Export
|
|
4
|
+
def scope_includes
|
|
5
|
+
[]
|
|
6
|
+
end
|
|
7
|
+
|
|
8
|
+
def multi_line_csv?
|
|
9
|
+
true
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def scope
|
|
13
|
+
if search_params.nil?
|
|
14
|
+
super.where.not(status: 'archived')
|
|
15
|
+
else
|
|
16
|
+
super
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def csv_headers
|
|
21
|
+
Spree::CSV::ProductTranslationPresenter::CSV_HEADERS
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def generate_csv
|
|
25
|
+
locales = store.supported_locales_list - [store.default_locale]
|
|
26
|
+
return super if locales.empty?
|
|
27
|
+
|
|
28
|
+
::CSV.open(export_tmp_file_path, 'wb', encoding: 'UTF-8', col_sep: ',', row_sep: "\r\n") do |csv|
|
|
29
|
+
csv << csv_headers
|
|
30
|
+
records_to_export.includes(scope_includes).find_in_batches do |batch|
|
|
31
|
+
batch.each do |product|
|
|
32
|
+
product.to_translation_csv(store, locales).each do |line|
|
|
33
|
+
csv << line
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def model_class
|
|
41
|
+
Spree::Product
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
end
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
module Spree
|
|
2
|
+
module ImportSchemas
|
|
3
|
+
class ProductTranslations < Spree::ImportSchema
|
|
4
|
+
FIELDS = [
|
|
5
|
+
{ name: 'slug', label: 'Slug', required: true },
|
|
6
|
+
{ name: 'locale', label: 'Locale', required: true },
|
|
7
|
+
{ name: 'name', label: 'Name' },
|
|
8
|
+
{ name: 'description', label: 'Description' },
|
|
9
|
+
{ name: 'meta_title', label: 'Meta Title' },
|
|
10
|
+
{ name: 'meta_description', label: 'Meta Description' }
|
|
11
|
+
].freeze
|
|
12
|
+
end
|
|
13
|
+
end
|
|
14
|
+
end
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
module Spree
|
|
2
|
+
module Imports
|
|
3
|
+
class ProductTranslations < Spree::Import
|
|
4
|
+
def row_processor_class
|
|
5
|
+
Spree::Imports::RowProcessors::ProductTranslation
|
|
6
|
+
end
|
|
7
|
+
|
|
8
|
+
def model_class
|
|
9
|
+
Spree::Product
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def self.model_class
|
|
13
|
+
Spree::Product
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
end
|
data/app/models/spree/market.rb
CHANGED
|
@@ -13,6 +13,7 @@ module Spree
|
|
|
13
13
|
belongs_to :store, class_name: 'Spree::Store', touch: true
|
|
14
14
|
has_many :market_countries, class_name: 'Spree::MarketCountry', dependent: :destroy
|
|
15
15
|
has_many :countries, through: :market_countries, class_name: 'Spree::Country'
|
|
16
|
+
has_many :orders, class_name: 'Spree::Order', dependent: :nullify
|
|
16
17
|
|
|
17
18
|
#
|
|
18
19
|
# Validations
|
|
@@ -51,6 +51,16 @@ module Spree
|
|
|
51
51
|
self.whitelisted_ransackable_attributes = %w[key namespace name resource_type display_on]
|
|
52
52
|
self.whitelisted_ransackable_scopes = %w[search multi_search]
|
|
53
53
|
|
|
54
|
+
# 5.5 API naming bridge (DB column rename in 6.0)
|
|
55
|
+
# Aligns with OptionType/OptionValue which also expose `label` for the display name.
|
|
56
|
+
def label
|
|
57
|
+
name
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def label=(value)
|
|
61
|
+
self.name = value
|
|
62
|
+
end
|
|
63
|
+
|
|
54
64
|
# Returns the full key with namespace
|
|
55
65
|
# @return [String] eg. custom.id
|
|
56
66
|
def full_key
|
|
@@ -3,6 +3,7 @@ module Spree
|
|
|
3
3
|
has_prefix_id :opt # Spree-specific: option type
|
|
4
4
|
|
|
5
5
|
COLOR_NAMES = %w[color colour].freeze
|
|
6
|
+
KINDS = %w[dropdown color_swatch buttons].freeze
|
|
6
7
|
|
|
7
8
|
include Spree::ParameterizableName
|
|
8
9
|
include Spree::UniqueName
|
|
@@ -34,18 +35,28 @@ module Spree
|
|
|
34
35
|
has_many :prototypes, through: :option_type_prototypes, class_name: 'Spree::Prototype'
|
|
35
36
|
|
|
36
37
|
# 5.5 API naming bridge (DB column rename in 6.0)
|
|
37
|
-
alias_attribute
|
|
38
|
+
# NOTE: alias_attribute bypasses Mobility's locale-aware reader/writer,
|
|
39
|
+
# so we define explicit delegating methods instead.
|
|
40
|
+
def label(*args)
|
|
41
|
+
presentation(*args)
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def label=(value)
|
|
45
|
+
self.presentation = value
|
|
46
|
+
end
|
|
38
47
|
|
|
39
48
|
#
|
|
40
49
|
# Validations
|
|
41
50
|
#
|
|
42
51
|
validates :presentation, presence: true
|
|
52
|
+
validates :kind, presence: true, inclusion: { in: KINDS }
|
|
43
53
|
|
|
44
54
|
#
|
|
45
55
|
# Scopes
|
|
46
56
|
#
|
|
47
57
|
default_scope { order(:position) }
|
|
48
58
|
scope :colors, -> { where(name: COLOR_NAMES) }
|
|
59
|
+
scope :color_swatches, -> { where(kind: 'color_swatch') }
|
|
49
60
|
scope :filterable, -> { where(filterable: true) }
|
|
50
61
|
|
|
51
62
|
#
|
|
@@ -72,8 +83,15 @@ module Spree
|
|
|
72
83
|
colors.first
|
|
73
84
|
end
|
|
74
85
|
|
|
86
|
+
def color_swatch?
|
|
87
|
+
kind == 'color_swatch'
|
|
88
|
+
end
|
|
89
|
+
|
|
75
90
|
def color?
|
|
76
|
-
|
|
91
|
+
Spree::Deprecation.warn(
|
|
92
|
+
'Spree::OptionType#color? is deprecated. Use #color_swatch? instead. Will be removed in Spree 6.0.'
|
|
93
|
+
)
|
|
94
|
+
color_swatch?
|
|
77
95
|
end
|
|
78
96
|
|
|
79
97
|
private
|
|
@@ -20,6 +20,11 @@ module Spree
|
|
|
20
20
|
acts_as_list scope: :option_type
|
|
21
21
|
self.whitelisted_ransackable_attributes = ['presentation']
|
|
22
22
|
|
|
23
|
+
#
|
|
24
|
+
# Attachments
|
|
25
|
+
#
|
|
26
|
+
has_one_attached :image
|
|
27
|
+
|
|
23
28
|
#
|
|
24
29
|
# Associations
|
|
25
30
|
#
|
|
@@ -29,7 +34,15 @@ module Spree
|
|
|
29
34
|
has_many :products, through: :variants, class_name: 'Spree::Product'
|
|
30
35
|
|
|
31
36
|
# 5.5 API naming bridge (DB column rename in 6.0)
|
|
32
|
-
alias_attribute
|
|
37
|
+
# NOTE: alias_attribute bypasses Mobility's locale-aware reader/writer,
|
|
38
|
+
# so we define explicit delegating methods instead.
|
|
39
|
+
def label(*args)
|
|
40
|
+
presentation(*args)
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def label=(value)
|
|
44
|
+
self.presentation = value
|
|
45
|
+
end
|
|
33
46
|
|
|
34
47
|
#
|
|
35
48
|
# Validations
|
|
@@ -39,6 +52,9 @@ module Spree
|
|
|
39
52
|
validates :presentation
|
|
40
53
|
end
|
|
41
54
|
|
|
55
|
+
validates :color_code, format: { with: /\A#[0-9A-Fa-f]{6}([0-9A-Fa-f]{2})?\z/, message: 'must be a valid hex color (e.g. #FF0000)' },
|
|
56
|
+
allow_blank: true
|
|
57
|
+
|
|
42
58
|
#
|
|
43
59
|
# Scopes
|
|
44
60
|
#
|
data/app/models/spree/order.rb
CHANGED
|
@@ -54,7 +54,7 @@ module Spree
|
|
|
54
54
|
alias display_ship_total display_shipment_total
|
|
55
55
|
alias_attribute :ship_total, :shipment_total
|
|
56
56
|
def amount_due
|
|
57
|
-
outstanding_balance
|
|
57
|
+
[outstanding_balance - total_applied_store_credit, 0].max
|
|
58
58
|
end
|
|
59
59
|
|
|
60
60
|
# Transient warnings populated by remove_out_of_stock_items!
|
|
@@ -140,6 +140,7 @@ module Spree
|
|
|
140
140
|
alias_attribute :shipping_address_id, :ship_address_id
|
|
141
141
|
|
|
142
142
|
belongs_to :store, class_name: 'Spree::Store'
|
|
143
|
+
belongs_to :market, class_name: 'Spree::Market', optional: true
|
|
143
144
|
|
|
144
145
|
with_options dependent: :destroy do
|
|
145
146
|
has_many :state_changes, as: :stateful, class_name: 'Spree::StateChange'
|
|
@@ -181,6 +182,8 @@ module Spree
|
|
|
181
182
|
alias_attribute :fulfillment_status, :shipment_state
|
|
182
183
|
alias_attribute :payment_status, :payment_state
|
|
183
184
|
|
|
185
|
+
delegate :has_markets?, to: :store, prefix: true
|
|
186
|
+
|
|
184
187
|
accepts_nested_attributes_for :line_items
|
|
185
188
|
accepts_nested_attributes_for :bill_address
|
|
186
189
|
accepts_nested_attributes_for :ship_address
|
|
@@ -191,12 +194,14 @@ module Spree
|
|
|
191
194
|
|
|
192
195
|
# Needs to happen before save_permalink is called
|
|
193
196
|
before_validation :ensure_store_presence
|
|
197
|
+
before_validation :ensure_market_presence
|
|
194
198
|
before_validation :ensure_currency_presence
|
|
195
199
|
before_validation :ensure_locale_presence
|
|
200
|
+
before_validation :resolve_market_from_currency, if: -> { persisted? && currency_changed? && !skip_market_resolution }
|
|
196
201
|
|
|
197
202
|
before_validation :clone_billing_address, if: :use_billing?
|
|
198
203
|
before_validation :clone_shipping_address, if: :use_shipping?
|
|
199
|
-
attr_accessor :use_billing, :use_shipping
|
|
204
|
+
attr_accessor :use_billing, :use_shipping, :skip_market_resolution
|
|
200
205
|
|
|
201
206
|
before_create :link_by_email
|
|
202
207
|
before_update :ensure_updated_shipments, :homogenize_line_item_currencies, if: :currency_changed?
|
|
@@ -220,6 +225,7 @@ module Spree
|
|
|
220
225
|
validates :shipment_total, MONEY_VALIDATION
|
|
221
226
|
validates :promo_total, NEGATIVE_MONEY_VALIDATION
|
|
222
227
|
validates :total, MONEY_VALIDATION
|
|
228
|
+
validates :market, presence: true, if: :store_has_markets?
|
|
223
229
|
validate :currency_must_be_supported_by_store
|
|
224
230
|
validate :locale_must_be_supported_by_store
|
|
225
231
|
|
|
@@ -450,6 +456,10 @@ module Spree
|
|
|
450
456
|
self.store ||= Spree::Store.default
|
|
451
457
|
end
|
|
452
458
|
|
|
459
|
+
def ensure_market_presence
|
|
460
|
+
self.market ||= Spree::Current.market || store&.default_market
|
|
461
|
+
end
|
|
462
|
+
|
|
453
463
|
def allow_cancel?
|
|
454
464
|
return false if !completed? || canceled?
|
|
455
465
|
|
|
@@ -1050,6 +1060,16 @@ module Spree
|
|
|
1050
1060
|
end
|
|
1051
1061
|
end
|
|
1052
1062
|
|
|
1063
|
+
# When currency changes, auto-resolve the matching market.
|
|
1064
|
+
# Only applies when the store has markets configured.
|
|
1065
|
+
def resolve_market_from_currency
|
|
1066
|
+
return unless store_has_markets?
|
|
1067
|
+
return if market&.currency == currency
|
|
1068
|
+
|
|
1069
|
+
resolved = store.markets.find_by(currency: currency)
|
|
1070
|
+
self.market = resolved if resolved
|
|
1071
|
+
end
|
|
1072
|
+
|
|
1053
1073
|
def collect_payment_methods
|
|
1054
1074
|
Spree::Deprecation.warn('`Order#collect_payment_methods` is deprecated and will be removed in Spree 5.5. Use `collect_frontend_payment_methods` instead.')
|
|
1055
1075
|
|
data/app/models/spree/price.rb
CHANGED
|
@@ -135,7 +135,7 @@ module Spree
|
|
|
135
135
|
#
|
|
136
136
|
# @return [Boolean]
|
|
137
137
|
def discounted?
|
|
138
|
-
compare_at_amount.to_i.positive? && compare_at_amount > amount
|
|
138
|
+
compare_at_amount.to_i.positive? && amount.present? && compare_at_amount > amount
|
|
139
139
|
end
|
|
140
140
|
|
|
141
141
|
# returns true if the price was discounted
|
data/app/models/spree/product.rb
CHANGED
|
@@ -604,19 +604,40 @@ module Spree
|
|
|
604
604
|
taxons_for_csv.fill(nil, taxons_for_csv.size...3)
|
|
605
605
|
|
|
606
606
|
csv_lines = []
|
|
607
|
+
all_variants = has_variants? ? variants_including_master.to_a : [master]
|
|
608
|
+
default_currency = store.default_currency
|
|
609
|
+
additional_currencies = store.supported_currencies_list.map(&:iso_code) - [default_currency]
|
|
610
|
+
|
|
611
|
+
# Primary rows in the store's default currency
|
|
612
|
+
all_variants.each_with_index do |variant, index|
|
|
613
|
+
csv_lines << Spree::CSV::ProductVariantPresenter.new(self, variant, index, properties_for_csv, taxons_for_csv, store,
|
|
614
|
+
metafields_for_csv).call
|
|
615
|
+
end
|
|
607
616
|
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
617
|
+
# Price-only rows for each additional currency
|
|
618
|
+
additional_currencies.each do |currency|
|
|
619
|
+
all_variants.each do |variant|
|
|
620
|
+
next unless variant.amount_in(currency)
|
|
621
|
+
|
|
622
|
+
csv_lines << Spree::CSV::ProductVariantPresenter.new(self, variant, 0, [], [], store,
|
|
623
|
+
[], currency).call
|
|
612
624
|
end
|
|
613
|
-
else
|
|
614
|
-
csv_lines << Spree::CSV::ProductVariantPresenter.new(self, master, 0, properties_for_csv, taxons_for_csv, store, metafields_for_csv).call
|
|
615
625
|
end
|
|
616
626
|
|
|
617
627
|
csv_lines
|
|
618
628
|
end
|
|
619
629
|
|
|
630
|
+
def to_translation_csv(store = nil, locales = [])
|
|
631
|
+
locales.filter_map do |locale|
|
|
632
|
+
# Only export if at least one field has a translation for this locale
|
|
633
|
+
has_translation = Spree::CSV::ProductTranslationPresenter::TRANSLATABLE_FIELDS.any? do |field|
|
|
634
|
+
get_field_with_locale(locale, field).present?
|
|
635
|
+
end
|
|
636
|
+
|
|
637
|
+
Spree::CSV::ProductTranslationPresenter.new(self, locale).call if has_translation
|
|
638
|
+
end
|
|
639
|
+
end
|
|
640
|
+
|
|
620
641
|
private
|
|
621
642
|
|
|
622
643
|
# Determines which variant should be used for displaying media.
|
|
@@ -652,11 +673,12 @@ module Spree
|
|
|
652
673
|
values = option_values_hash.values
|
|
653
674
|
values = values.inject(values.shift) { |memo, value| memo.product(value).map(&:flatten) }
|
|
654
675
|
|
|
676
|
+
default_currency = stores.first&.default_currency || Spree::Store.default.default_currency
|
|
677
|
+
master_price = master.price_in(default_currency).amount
|
|
678
|
+
|
|
655
679
|
values.each do |ids|
|
|
656
|
-
variants.create(
|
|
657
|
-
|
|
658
|
-
price: master.price
|
|
659
|
-
)
|
|
680
|
+
variant = variants.create!(option_value_ids: ids)
|
|
681
|
+
variant.set_price(default_currency, master_price) if master_price.present?
|
|
660
682
|
end
|
|
661
683
|
save
|
|
662
684
|
end
|
|
@@ -690,6 +712,7 @@ module Spree
|
|
|
690
712
|
master.new_record? ||
|
|
691
713
|
master.changed? ||
|
|
692
714
|
(
|
|
715
|
+
Spree::Config.enable_legacy_default_price &&
|
|
693
716
|
master.default_price &&
|
|
694
717
|
(
|
|
695
718
|
master.default_price.new_record? ||
|
|
@@ -712,9 +735,13 @@ module Spree
|
|
|
712
735
|
# If the master cannot be saved, the Product object will get its errors
|
|
713
736
|
# and will be destroyed
|
|
714
737
|
def validate_master
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
738
|
+
if Spree::Config.enable_legacy_default_price
|
|
739
|
+
# We call master.default_price here to ensure price is initialized.
|
|
740
|
+
# Required to avoid Variant#check_price validation failing on create.
|
|
741
|
+
master.default_price
|
|
742
|
+
end
|
|
743
|
+
|
|
744
|
+
unless master.valid?
|
|
718
745
|
master.errors.map { |error| { field: error.attribute, message: error&.message } }.each do |err|
|
|
719
746
|
next if err[:field].blank? || err[:message].blank?
|
|
720
747
|
|
|
@@ -103,9 +103,10 @@ module Spree
|
|
|
103
103
|
product_prefixed_ids = ms_result['hits'].map { |h| h['product_id'] }.uniq
|
|
104
104
|
raw_ids = product_prefixed_ids.filter_map { |pid| Spree::Product.decode_prefixed_id(pid) }
|
|
105
105
|
|
|
106
|
-
# Intersect with AR scope for security/visibility.
|
|
106
|
+
# Intersect with AR scope for security/visibility, preserving Meilisearch sort order.
|
|
107
107
|
products = if raw_ids.any?
|
|
108
|
-
scope.where(id: raw_ids).reorder(nil)
|
|
108
|
+
records = scope.where(id: raw_ids).reorder(nil).index_by(&:id)
|
|
109
|
+
raw_ids.filter_map { |id| records[id] }
|
|
109
110
|
else
|
|
110
111
|
scope.none
|
|
111
112
|
end
|
|
@@ -123,8 +124,9 @@ module Spree
|
|
|
123
124
|
end
|
|
124
125
|
|
|
125
126
|
def index(product)
|
|
127
|
+
ensure_index_settings_once!
|
|
126
128
|
documents = presenter_class.new(product, store).call
|
|
127
|
-
client.index(index_name).add_documents(documents, '
|
|
129
|
+
client.index(index_name).add_documents(documents, 'id')
|
|
128
130
|
end
|
|
129
131
|
|
|
130
132
|
def remove(product)
|
|
@@ -132,7 +134,9 @@ module Spree
|
|
|
132
134
|
end
|
|
133
135
|
|
|
134
136
|
def index_batch(documents)
|
|
135
|
-
|
|
137
|
+
return if documents.empty?
|
|
138
|
+
|
|
139
|
+
client.index(index_name).add_documents(documents, 'id')
|
|
136
140
|
end
|
|
137
141
|
|
|
138
142
|
# Remove all documents for a product by its prefixed_id (e.g. 'prod_abc')
|
|
@@ -147,21 +151,44 @@ module Spree
|
|
|
147
151
|
scope ||= store.products
|
|
148
152
|
ensure_index_settings!
|
|
149
153
|
|
|
154
|
+
indexed = 0
|
|
150
155
|
scope.reorder(id: :asc)
|
|
151
156
|
.preload_associations_lazily
|
|
152
157
|
.find_in_batches(batch_size: 500) do |batch|
|
|
153
158
|
documents = batch.flat_map { |product| presenter_class.new(product, store).call }
|
|
159
|
+
next if documents.empty?
|
|
160
|
+
|
|
154
161
|
index_batch(documents)
|
|
162
|
+
indexed += documents.size
|
|
163
|
+
|
|
164
|
+
Rails.logger.info { "[Meilisearch] Enqueued #{documents.size} documents (#{indexed} total) for #{index_name}" }
|
|
155
165
|
end
|
|
166
|
+
|
|
167
|
+
Rails.logger.info { "[Meilisearch] Reindex complete: #{indexed} documents enqueued for #{index_name}" }
|
|
168
|
+
indexed
|
|
156
169
|
end
|
|
157
170
|
|
|
158
171
|
# Configure index settings for filtering, sorting, and faceting.
|
|
159
172
|
# Called automatically by reindex, but can be called separately.
|
|
173
|
+
# Waits for all settings tasks to complete before returning so that
|
|
174
|
+
# subsequent add_documents calls use the correct filterable/sortable attributes.
|
|
160
175
|
def ensure_index_settings!
|
|
161
176
|
index = client.index(index_name)
|
|
162
|
-
|
|
163
|
-
index.
|
|
164
|
-
index.
|
|
177
|
+
tasks = []
|
|
178
|
+
tasks << index.update_filterable_attributes(filterable_attributes)
|
|
179
|
+
tasks << index.update_sortable_attributes(sortable_attributes)
|
|
180
|
+
tasks << index.update_searchable_attributes(searchable_attributes)
|
|
181
|
+
tasks.each { |task| task&.await }
|
|
182
|
+
@index_settings_configured = true
|
|
183
|
+
end
|
|
184
|
+
|
|
185
|
+
# Lightweight guard — configures index settings once per provider instance.
|
|
186
|
+
# Meilisearch settings updates are idempotent, so repeated calls are safe
|
|
187
|
+
# but we avoid the overhead by memoizing.
|
|
188
|
+
def ensure_index_settings_once!
|
|
189
|
+
return if @index_settings_configured
|
|
190
|
+
|
|
191
|
+
ensure_index_settings!
|
|
165
192
|
end
|
|
166
193
|
|
|
167
194
|
private
|
|
@@ -359,7 +386,12 @@ module Spree
|
|
|
359
386
|
|
|
360
387
|
ot = ov.option_type
|
|
361
388
|
by_option_type[ot] ||= []
|
|
362
|
-
by_option_type[ot] << {
|
|
389
|
+
by_option_type[ot] << {
|
|
390
|
+
id: ov.prefixed_id, name: ov.name, label: ov.label, position: ov.position,
|
|
391
|
+
color_code: ov.color_code,
|
|
392
|
+
image_url: ov.image.attached? ? Rails.application.routes.url_helpers.cdn_image_url(ov.image) : nil,
|
|
393
|
+
count: count
|
|
394
|
+
}
|
|
363
395
|
end
|
|
364
396
|
|
|
365
397
|
by_option_type.map do |option_type, values|
|
|
@@ -368,6 +400,7 @@ module Spree
|
|
|
368
400
|
type: 'option',
|
|
369
401
|
name: option_type.name,
|
|
370
402
|
label: option_type.label,
|
|
403
|
+
kind: option_type.kind,
|
|
371
404
|
options: values.sort_by { |o| o[:position] }
|
|
372
405
|
}
|
|
373
406
|
end
|
|
@@ -27,7 +27,7 @@ module Spree
|
|
|
27
27
|
after_save :ensure_one_default
|
|
28
28
|
after_update :conditional_touch_records
|
|
29
29
|
|
|
30
|
-
delegate :name, :iso3, :iso, :iso_name, to: :country, prefix: true
|
|
30
|
+
delegate :name, :iso3, :iso, :iso_name, to: :country, prefix: true, allow_nil: true
|
|
31
31
|
|
|
32
32
|
def state_text
|
|
33
33
|
state.try(:abbr) || state.try(:name) || state_name
|