spree_core 5.4.0.rc6 → 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/models/concerns/spree/default_price.rb +108 -6
- 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/metafield.rb +1 -1
- data/app/models/spree/metafield_definition.rb +10 -0
- data/app/models/spree/option_type.rb +9 -1
- data/app/models/spree/option_value.rb +9 -1
- data/app/models/spree/order.rb +1 -1
- data/app/models/spree/price.rb +1 -1
- data/app/models/spree/product.rb +40 -13
- 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/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 +13 -0
- data/app/views/spree/shared/_mailer_line_item.html.erb +2 -2
- data/config/locales/en.yml +1 -0
- data/db/sample_data/markets.rb +30 -0
- data/db/sample_data/product_translations.csv +75 -0
- data/db/sample_data/products.csv +88 -1
- 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/testing_support/authorization_helpers.rb +16 -16
- data/lib/spree/testing_support/factories/import_factory.rb +12 -0
- data/lib/spree/testing_support/factories/product_factory.rb +12 -2
- data/lib/spree/testing_support/factories/variant_factory.rb +8 -2
- data/spec/fixtures/files/product_translations_import.csv +3 -0
- metadata +12 -4
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
|
|
@@ -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
|
|
@@ -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
|
|
@@ -35,7 +35,15 @@ module Spree
|
|
|
35
35
|
has_many :prototypes, through: :option_type_prototypes, class_name: 'Spree::Prototype'
|
|
36
36
|
|
|
37
37
|
# 5.5 API naming bridge (DB column rename in 6.0)
|
|
38
|
-
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
|
|
39
47
|
|
|
40
48
|
#
|
|
41
49
|
# Validations
|
|
@@ -34,7 +34,15 @@ module Spree
|
|
|
34
34
|
has_many :products, through: :variants, class_name: 'Spree::Product'
|
|
35
35
|
|
|
36
36
|
# 5.5 API naming bridge (DB column rename in 6.0)
|
|
37
|
-
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
|
|
38
46
|
|
|
39
47
|
#
|
|
40
48
|
# Validations
|
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!
|
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
|
|
|
@@ -31,7 +31,8 @@ module Spree
|
|
|
31
31
|
delegate :variant, :variant_id, to: :stock_item, allow_nil: true
|
|
32
32
|
delegate :product, to: :variant
|
|
33
33
|
|
|
34
|
-
self.whitelisted_ransackable_attributes = [
|
|
34
|
+
self.whitelisted_ransackable_attributes = %w[quantity action created_at stock_item_id originator_type]
|
|
35
|
+
self.whitelisted_ransackable_associations = %w[stock_item]
|
|
35
36
|
|
|
36
37
|
def readonly?
|
|
37
38
|
persisted?
|
data/app/models/spree/variant.rb
CHANGED
|
@@ -62,14 +62,13 @@ module Spree
|
|
|
62
62
|
|
|
63
63
|
before_validation :set_cost_currency
|
|
64
64
|
|
|
65
|
-
validate :check_price
|
|
65
|
+
validate :check_price, if: -> { Spree::Config.enable_legacy_default_price }
|
|
66
66
|
|
|
67
67
|
validates :option_value_variants, presence: true, unless: :is_master?
|
|
68
68
|
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
end
|
|
69
|
+
validates :cost_price, numericality: { greater_than_or_equal_to: 0, allow_nil: true }
|
|
70
|
+
validates :price, numericality: { greater_than_or_equal_to: 0, allow_nil: true },
|
|
71
|
+
if: -> { Spree::Config.enable_legacy_default_price }
|
|
73
72
|
validates :sku, uniqueness: { conditions: -> { where(deleted_at: nil) }, case_sensitive: false, scope: spree_base_uniqueness_scope },
|
|
74
73
|
allow_blank: true, unless: :disable_sku_validation?
|
|
75
74
|
|
|
@@ -447,8 +446,8 @@ module Spree
|
|
|
447
446
|
def set_price(currency, amount, compare_at_amount = nil)
|
|
448
447
|
price = prices.base_prices.find_or_initialize_by(currency: currency)
|
|
449
448
|
price.amount = amount
|
|
450
|
-
price.compare_at_amount = compare_at_amount
|
|
451
|
-
price.save!
|
|
449
|
+
price.compare_at_amount = compare_at_amount
|
|
450
|
+
price.save! if persisted?
|
|
452
451
|
end
|
|
453
452
|
|
|
454
453
|
# Returns the price for the given context or options.
|
|
@@ -607,18 +606,21 @@ module Spree
|
|
|
607
606
|
|
|
608
607
|
# Ensures a new variant takes the product master price when price is not supplied
|
|
609
608
|
def check_price
|
|
610
|
-
return if
|
|
609
|
+
return if prices.any?
|
|
611
610
|
|
|
612
611
|
infer_price_from_default_variant_if_needed
|
|
613
|
-
self.currency = Spree::Store.default.default_currency if price.present? && currency.nil?
|
|
614
612
|
end
|
|
615
613
|
|
|
616
614
|
def infer_price_from_default_variant_if_needed
|
|
617
|
-
|
|
615
|
+
default_currency = Spree::Store.default.default_currency
|
|
616
|
+
current_price = price_in(default_currency).amount
|
|
617
|
+
|
|
618
|
+
if current_price.nil?
|
|
618
619
|
return errors.add(:base, :no_master_variant_found_to_infer_price) unless product&.master
|
|
619
620
|
|
|
620
621
|
# At this point, master can have or have no price, so let's use price from the default variant
|
|
621
|
-
|
|
622
|
+
inferred_price = product.default_variant.price_in(default_currency).amount
|
|
623
|
+
set_price(default_currency, inferred_price) if inferred_price.present?
|
|
622
624
|
end
|
|
623
625
|
end
|
|
624
626
|
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
module Spree
|
|
2
|
+
module CSV
|
|
3
|
+
class ProductTranslationPresenter
|
|
4
|
+
CSV_HEADERS = %w[
|
|
5
|
+
slug
|
|
6
|
+
locale
|
|
7
|
+
name
|
|
8
|
+
description
|
|
9
|
+
meta_title
|
|
10
|
+
meta_description
|
|
11
|
+
].freeze
|
|
12
|
+
|
|
13
|
+
TRANSLATABLE_FIELDS = %i[name description meta_title meta_description].freeze
|
|
14
|
+
|
|
15
|
+
def initialize(product, locale)
|
|
16
|
+
@product = product
|
|
17
|
+
@locale = locale.to_s
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
attr_reader :product, :locale
|
|
21
|
+
|
|
22
|
+
def call
|
|
23
|
+
[
|
|
24
|
+
product.slug,
|
|
25
|
+
locale,
|
|
26
|
+
*TRANSLATABLE_FIELDS.map { |field| product.get_field_with_locale(locale, field) }
|
|
27
|
+
]
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
|
@@ -47,18 +47,19 @@ module Spree
|
|
|
47
47
|
'category3',
|
|
48
48
|
].freeze
|
|
49
49
|
|
|
50
|
-
def initialize(product, variant, index = 0, properties = [], taxons = [], store = nil, metafields = [])
|
|
50
|
+
def initialize(product, variant, index = 0, properties = [], taxons = [], store = nil, metafields = [], currency = nil)
|
|
51
51
|
@product = product
|
|
52
52
|
@variant = variant
|
|
53
53
|
@index = index
|
|
54
54
|
@properties = properties
|
|
55
55
|
@taxons = taxons
|
|
56
56
|
@store = store || product.stores.first
|
|
57
|
-
@currency = @store.default_currency
|
|
57
|
+
@currency = currency || @store.default_currency
|
|
58
|
+
@price_only = @currency != @store.default_currency
|
|
58
59
|
@metafields = metafields
|
|
59
60
|
end
|
|
60
61
|
|
|
61
|
-
attr_accessor :product, :variant, :index, :properties, :taxons, :store, :currency, :metafields
|
|
62
|
+
attr_accessor :product, :variant, :index, :properties, :taxons, :store, :currency, :price_only, :metafields
|
|
62
63
|
|
|
63
64
|
##
|
|
64
65
|
# Generates an array representing a CSV row of product variant data.
|
|
@@ -72,6 +73,8 @@ module Spree
|
|
|
72
73
|
#
|
|
73
74
|
# @return [Array] An array containing the combined product and variant CSV data.
|
|
74
75
|
def call
|
|
76
|
+
return price_only_row if price_only
|
|
77
|
+
|
|
75
78
|
total_on_hand = variant.total_on_hand
|
|
76
79
|
|
|
77
80
|
csv = [
|
|
@@ -134,6 +137,16 @@ module Spree
|
|
|
134
137
|
|
|
135
138
|
private
|
|
136
139
|
|
|
140
|
+
def price_only_row
|
|
141
|
+
csv = Array.new(CSV_HEADERS.size)
|
|
142
|
+
csv[CSV_HEADERS.index('sku')] = variant.sku
|
|
143
|
+
csv[CSV_HEADERS.index('slug')] = product.slug
|
|
144
|
+
csv[CSV_HEADERS.index('price')] = variant.amount_in(currency)&.to_f
|
|
145
|
+
csv[CSV_HEADERS.index('compare_at_price')] = variant.compare_at_amount_in(currency)&.to_f
|
|
146
|
+
csv[CSV_HEADERS.index('currency')] = currency
|
|
147
|
+
csv
|
|
148
|
+
end
|
|
149
|
+
|
|
137
150
|
def image_url_options
|
|
138
151
|
{ variant: :xlarge }
|
|
139
152
|
end
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
module Spree
|
|
2
|
+
module Imports
|
|
3
|
+
module RowProcessors
|
|
4
|
+
class ProductTranslation < Base
|
|
5
|
+
TRANSLATABLE_FIELDS = %w[name description meta_title meta_description].freeze
|
|
6
|
+
|
|
7
|
+
def initialize(row)
|
|
8
|
+
super
|
|
9
|
+
@store = row.store
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
attr_reader :store
|
|
13
|
+
|
|
14
|
+
def process!
|
|
15
|
+
locale = attributes['locale'].to_s.strip
|
|
16
|
+
raise ArgumentError, 'Locale is required' if locale.blank?
|
|
17
|
+
|
|
18
|
+
slug = attributes['slug'].to_s.strip
|
|
19
|
+
raise ArgumentError, 'Slug is required' if slug.blank?
|
|
20
|
+
|
|
21
|
+
product = product_scope.find_by!(slug: slug)
|
|
22
|
+
|
|
23
|
+
translation_attrs = TRANSLATABLE_FIELDS.each_with_object({}) do |field, hash|
|
|
24
|
+
value = attributes[field]
|
|
25
|
+
hash[field.to_sym] = value.to_s.strip if value.present?
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
return product if translation_attrs.empty?
|
|
29
|
+
|
|
30
|
+
Mobility.with_locale(locale) do
|
|
31
|
+
product.update!(translation_attrs)
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
product
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
private
|
|
38
|
+
|
|
39
|
+
def product_scope
|
|
40
|
+
Spree::Product.accessible_by(import.current_ability, :manage)
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
end
|
|
@@ -32,7 +32,7 @@ module Spree
|
|
|
32
32
|
variant.width = attributes['width'] if attributes['width'].present?
|
|
33
33
|
variant.depth = attributes['depth'] if attributes['depth'].present?
|
|
34
34
|
variant.track_inventory = attributes['track_inventory'] if attributes['track_inventory'].present?
|
|
35
|
-
variant.option_value_variants = prepare_option_value_variants
|
|
35
|
+
variant.option_value_variants = prepare_option_value_variants if options.any?
|
|
36
36
|
|
|
37
37
|
if attributes['tax_category'].present?
|
|
38
38
|
tax_category = prepare_tax_category
|
|
@@ -68,7 +68,7 @@ module Spree
|
|
|
68
68
|
|
|
69
69
|
product = assign_attributes_to_product(product)
|
|
70
70
|
product.save!
|
|
71
|
-
handle_metafields(product)
|
|
71
|
+
handle_metafields(product) if has_product_attributes?
|
|
72
72
|
product
|
|
73
73
|
else
|
|
74
74
|
# For non-master variants, only look up the product
|
|
@@ -105,7 +105,11 @@ module Spree
|
|
|
105
105
|
shipping_category = prepare_shipping_category
|
|
106
106
|
product.shipping_category = shipping_category if shipping_category.present?
|
|
107
107
|
end
|
|
108
|
-
|
|
108
|
+
|
|
109
|
+
taxons = prepare_taxons
|
|
110
|
+
# Full product rows (with name/status/description) clear taxons when categories are blank.
|
|
111
|
+
# Price-only rows (no product attributes) never touch taxons.
|
|
112
|
+
product.taxons = taxons if taxons.any? || has_product_attributes?
|
|
109
113
|
end
|
|
110
114
|
|
|
111
115
|
product
|
|
@@ -197,6 +201,10 @@ module Spree
|
|
|
197
201
|
end
|
|
198
202
|
end
|
|
199
203
|
|
|
204
|
+
def has_product_attributes?
|
|
205
|
+
%w[name status description category1 category2 category3].any? { |key| attributes[key].present? }
|
|
206
|
+
end
|
|
207
|
+
|
|
200
208
|
def handle_metafields(product)
|
|
201
209
|
return unless product.class.included_modules.include?(Spree::Metafields)
|
|
202
210
|
|
|
@@ -33,8 +33,11 @@ module Spree
|
|
|
33
33
|
existing_variant = variant_params[:id].presence && @product.variants.find_by(id: variant_params[:id])
|
|
34
34
|
variants_to_remove.delete(variant_params[:id]) if variant_params[:id].present?
|
|
35
35
|
|
|
36
|
+
variant_params.delete(:price) # remove legacy price param
|
|
37
|
+
|
|
36
38
|
if can_update_prices?
|
|
37
|
-
|
|
39
|
+
backfill_price_ids!(variant_params, existing_variant)
|
|
40
|
+
|
|
38
41
|
variant_params[:prices_attributes]&.each do |price_key, price_params|
|
|
39
42
|
variant_params[:prices_attributes][price_key]['_destroy'] = '1' if price_params[:amount].blank?
|
|
40
43
|
end
|
|
@@ -95,6 +98,20 @@ module Spree
|
|
|
95
98
|
|
|
96
99
|
delegate :can?, :cannot?, to: :ability
|
|
97
100
|
|
|
101
|
+
# Backfill IDs for prices_attributes entries that reference existing prices
|
|
102
|
+
# so that ActiveRecord updates them instead of inserting duplicates
|
|
103
|
+
def backfill_price_ids!(variant_params, existing_variant)
|
|
104
|
+
return unless existing_variant && variant_params[:prices_attributes]
|
|
105
|
+
|
|
106
|
+
variant_params[:prices_attributes].each do |_key, price_params|
|
|
107
|
+
next if price_params[:id].present?
|
|
108
|
+
next if price_params[:currency].blank?
|
|
109
|
+
|
|
110
|
+
existing_price = existing_variant.prices.base_prices.find_by(currency: price_params[:currency])
|
|
111
|
+
price_params[:id] = existing_price.id if existing_price
|
|
112
|
+
end
|
|
113
|
+
end
|
|
114
|
+
|
|
98
115
|
def product_option_types_params
|
|
99
116
|
@product_option_types_params ||= {}
|
|
100
117
|
end
|