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.
Files changed (60) hide show
  1. checksums.yaml +4 -4
  2. data/app/mailers/spree/base_mailer.rb +2 -0
  3. data/app/models/concerns/spree/default_price.rb +108 -6
  4. data/app/models/concerns/spree/stores/markets.rb +4 -2
  5. data/app/models/spree/exports/product_translations.rb +45 -0
  6. data/app/models/spree/import_schemas/product_translations.rb +14 -0
  7. data/app/models/spree/imports/product_translations.rb +17 -0
  8. data/app/models/spree/market.rb +1 -0
  9. data/app/models/spree/metafield.rb +1 -1
  10. data/app/models/spree/metafield_definition.rb +10 -0
  11. data/app/models/spree/option_type.rb +20 -2
  12. data/app/models/spree/option_value.rb +17 -1
  13. data/app/models/spree/order.rb +22 -2
  14. data/app/models/spree/price.rb +1 -1
  15. data/app/models/spree/product.rb +40 -13
  16. data/app/models/spree/search_provider/meilisearch.rb +41 -8
  17. data/app/models/spree/stock_location.rb +1 -1
  18. data/app/models/spree/stock_movement.rb +2 -1
  19. data/app/models/spree/variant.rb +13 -11
  20. data/app/presenters/spree/csv/product_translation_presenter.rb +31 -0
  21. data/app/presenters/spree/csv/product_variant_presenter.rb +16 -3
  22. data/app/presenters/spree/search_provider/product_presenter.rb +8 -13
  23. data/app/services/spree/cart/create.rb +1 -0
  24. data/app/services/spree/carts/create.rb +1 -0
  25. data/app/services/spree/carts/update.rb +21 -0
  26. data/app/services/spree/imports/row_processors/product_translation.rb +45 -0
  27. data/app/services/spree/imports/row_processors/product_variant.rb +11 -3
  28. data/app/services/spree/products/prepare_nested_attributes.rb +18 -1
  29. data/app/services/spree/sample_data/loader.rb +31 -20
  30. data/app/services/spree/seeds/all.rb +22 -20
  31. data/app/views/spree/shared/_mailer_line_item.html.erb +2 -2
  32. data/config/locales/en.yml +12 -0
  33. data/db/migrate/20250217171018_create_action_text_video_embeds.rb +11 -0
  34. data/db/migrate/20260402000001_add_kind_to_spree_option_types.rb +13 -0
  35. data/db/migrate/20260402000002_add_color_code_to_spree_option_values.rb +5 -0
  36. data/db/migrate/20260403000000_add_market_to_spree_orders.rb +5 -0
  37. data/db/sample_data/markets.rb +30 -0
  38. data/db/sample_data/metafield_definitions.rb +2 -2
  39. data/db/sample_data/options.rb +4 -0
  40. data/db/sample_data/orders.rb +2 -2
  41. data/db/sample_data/product_translations.csv +75 -0
  42. data/db/sample_data/products.csv +212 -1083
  43. data/lib/spree/core/configuration.rb +1 -0
  44. data/lib/spree/core/controller_helpers/order.rb +12 -10
  45. data/lib/spree/core/engine.rb +2 -0
  46. data/lib/spree/core/permission_configuration.rb +12 -0
  47. data/lib/spree/core/pricing/resolver.rb +5 -3
  48. data/lib/spree/core/version.rb +1 -1
  49. data/lib/spree/permitted_attributes.rb +3 -3
  50. data/lib/spree/testing_support/authorization_helpers.rb +16 -16
  51. data/lib/spree/testing_support/factories/import_factory.rb +12 -0
  52. data/lib/spree/testing_support/factories/options_factory.rb +11 -0
  53. data/lib/spree/testing_support/factories/product_factory.rb +12 -2
  54. data/lib/spree/testing_support/factories/variant_factory.rb +8 -2
  55. data/lib/tasks/markets.rake +5 -2
  56. data/lib/tasks/search.rake +3 -3
  57. data/lib/tasks/variants.rake +2 -2
  58. data/spec/fixtures/files/product_translations_import.csv +3 -0
  59. metadata +26 -7
  60. /data/lib/tasks/{images.rake → media.rake} +0 -0
@@ -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 = ['quantity']
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?
@@ -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
- with_options numericality: { greater_than_or_equal_to: 0, allow_nil: true } do
70
- validates :cost_price
71
- validates :price
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 if compare_at_amount.present?
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 (has_default_price? && default_price.valid?) || prices.any?
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
- if price.nil?
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
- self.price = product.default_variant.price
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
@@ -35,10 +35,15 @@ module Spree
35
35
 
36
36
  private
37
37
 
38
+ # Build a document for a given locale and currency
39
+ # @param locale [String] the locale to build the document for
40
+ # @param currency [String] the currency to build the document for
41
+ # @param fallback_locale [String] the fallback locale to use if the product has no translation for the given locale
42
+ # @return [Hash] the document
38
43
  def build_document(locale, currency, fallback_locale)
39
44
  {
40
45
  # Composite ID: product + locale + currency
41
- prefixed_id: "#{product.prefixed_id}_#{locale}_#{currency}",
46
+ id: "#{product.prefixed_id}_#{locale}_#{currency}",
42
47
  product_id: product.prefixed_id,
43
48
  locale: locale.to_s,
44
49
  currency: currency,
@@ -53,7 +58,7 @@ module Spree
53
58
  status: product.status,
54
59
  sku: product.sku,
55
60
  in_stock: product.in_stock?,
56
- store_ids: cached_store_ids,
61
+ store_ids: product.store_ids.map(&:to_s),
57
62
  discontinue_on: product.discontinue_on&.to_i || 0,
58
63
  category_ids: category_ids_with_ancestors,
59
64
  category_names: product.taxons.map { |t| translated(t, :name, fallback_locale) },
@@ -62,8 +67,7 @@ module Spree
62
67
  option_value_ids: variant_option_value_ids,
63
68
  option_values: variant_option_values_data.map { |ov| translated(ov, :presentation, fallback_locale) }.uniq,
64
69
  tags: product.tag_list || [],
65
- thumbnail_url: product.primary_media&.url(:large),
66
- units_sold_count: cached_units_sold_count,
70
+ units_sold_count: product.store_products.find { |sp| sp.store_id == store.id }&.units_sold_count || 0,
67
71
  available_on: product.available_on&.iso8601,
68
72
  created_at: product.created_at&.iso8601,
69
73
  updated_at: product.updated_at&.iso8601
@@ -111,15 +115,6 @@ module Spree
111
115
  }.uniq
112
116
  end
113
117
 
114
- # Memoized — avoids N+1 when called per document
115
- def cached_store_ids
116
- @cached_store_ids ||= product.store_ids.map(&:to_s)
117
- end
118
-
119
- def cached_units_sold_count
120
- @cached_units_sold_count ||= product.store_products.detect { |sp| sp.store_id == store.id }&.units_sold_count || 0
121
- end
122
-
123
118
  def variant_option_value_ids
124
119
  variant_option_values_data.map(&:prefixed_id).uniq
125
120
  end
@@ -11,6 +11,7 @@ module Spree
11
11
 
12
12
  default_params = {
13
13
  user: user,
14
+ market: Spree::Current.market,
14
15
  currency: currency || store.default_currency,
15
16
  token: Spree::GenerateToken.new.call(Spree::Order),
16
17
  public_metadata: public_metadata.to_h,
@@ -11,6 +11,7 @@ module Spree
11
11
 
12
12
  cart = store.carts.create!(
13
13
  user: @params.delete(:user),
14
+ market: @params.delete(:market) || Spree::Current.market,
14
15
  currency: @params.delete(:currency) || store.default_currency,
15
16
  locale: @params.delete(:locale) || Spree::Current.locale
16
17
  )
@@ -9,6 +9,7 @@ module Spree
9
9
 
10
10
  ApplicationRecord.transaction do
11
11
  assign_cart_attributes
12
+ clear_shipping_address_if_outside_market
12
13
  assign_address(:shipping_address)
13
14
  assign_address(:billing_address)
14
15
 
@@ -35,6 +36,8 @@ module Spree
35
36
  def assign_cart_attributes
36
37
  cart.email = params[:email] if params[:email].present?
37
38
  cart.customer_note = params[:customer_note] if params.key?(:customer_note)
39
+
40
+ assign_market if params[:market_id].present?
38
41
  cart.currency = params[:currency].upcase if params[:currency].present?
39
42
  cart.locale = params[:locale] if params[:locale].present?
40
43
  cart.metadata = cart.metadata.merge(params[:metadata].to_h) if params[:metadata].present?
@@ -83,6 +86,24 @@ module Spree
83
86
  decoded ? cart.user.addresses.find_by(id: decoded)&.id : nil
84
87
  end
85
88
 
89
+ def assign_market
90
+ market = cart.store.markets.find_by_prefix_id!(params[:market_id])
91
+ cart.market = market
92
+ cart.skip_market_resolution = true
93
+ end
94
+
95
+ # When the market changes, clear the shipping address if its country
96
+ # is not part of the new market. The market dictates which countries
97
+ # are available for checkout.
98
+ def clear_shipping_address_if_outside_market
99
+ return unless cart.market_id_changed? && cart.ship_address&.country
100
+
101
+ unless cart.market.country_ids.include?(cart.ship_address.country_id)
102
+ cart.ship_address = nil
103
+ revert_to_address_state if cart.has_checkout_step?('address')
104
+ end
105
+ end
106
+
86
107
  def revert_to_address_state
87
108
  return if ['cart', 'address'].include?(cart.state)
88
109
 
@@ -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
- product.taxons = prepare_taxons
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
- # If the variant price is nil then mark it for destruction
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
@@ -6,29 +6,38 @@ module Spree
6
6
  def call
7
7
  Spree::Events.disable do
8
8
  without_geocoding do
9
- ensure_seeds_loaded
9
+ ActiveRecord::Base.no_touching do
10
+ ensure_seeds_loaded
10
11
 
11
- puts 'Loading sample configuration data...'
12
- load_configuration_data
12
+ puts 'Loading sample configuration data...'
13
+ load_configuration_data
13
14
 
14
- puts 'Loading sample metafield definitions...'
15
- load_ruby_file('metafield_definitions')
15
+ puts 'Loading sample markets...'
16
+ load_ruby_file('markets')
16
17
 
17
- puts 'Loading sample products...'
18
- load_products
18
+ puts 'Loading sample metafield definitions...'
19
+ load_ruby_file('metafield_definitions')
19
20
 
20
- puts 'Loading sample customers...'
21
- load_customers
21
+ puts 'Loading sample options...'
22
+ load_ruby_file('options')
22
23
 
23
- puts 'Loading sample orders...'
24
- load_ruby_file('orders')
24
+ puts 'Loading sample products...'
25
+ load_products
25
26
 
26
- puts 'Loading sample posts...'
27
- load_ruby_file('posts')
27
+ puts 'Loading sample product translations...'
28
+ load_product_translations
28
29
 
29
- clear_jobs_queue
30
+ puts 'Loading sample customers...'
31
+ load_customers
30
32
 
31
- puts 'Sample data loaded successfully!'
33
+ puts 'Loading sample orders...'
34
+ load_ruby_file('orders')
35
+
36
+ puts 'Loading sample posts...'
37
+ load_ruby_file('posts')
38
+
39
+ puts 'Sample data loaded successfully!'
40
+ end
32
41
  end
33
42
  end
34
43
  end
@@ -58,6 +67,13 @@ module Spree
58
67
  Spree::SampleData::ImportRunner.call(csv_path: csv_path, import_class: Spree::Imports::Products)
59
68
  end
60
69
 
70
+ def load_product_translations
71
+ csv_path = sample_data_path.join('product_translations.csv')
72
+ return unless csv_path.exist?
73
+
74
+ Spree::SampleData::ImportRunner.call(csv_path: csv_path, import_class: Spree::Imports::ProductTranslations)
75
+ end
76
+
61
77
  def load_customers
62
78
  csv_path = sample_data_path.join('customers.csv')
63
79
  Spree::SampleData::ImportRunner.call(csv_path: csv_path, import_class: Spree::Imports::Customers)
@@ -75,11 +91,6 @@ module Spree
75
91
  ensure
76
92
  Spree::Config[:geocode_addresses] = previous
77
93
  end
78
-
79
- def clear_jobs_queue
80
- adapter = ActiveJob::Base.queue_adapter
81
- adapter.enqueued_jobs.clear if adapter.respond_to?(:enqueued_jobs)
82
- end
83
94
  end
84
95
  end
85
96
  end
@@ -5,30 +5,32 @@ module Spree
5
5
 
6
6
  def call
7
7
  Spree::Events.disable do
8
- # GEO
9
- Countries.call
10
- States.call
11
- Zones.call
8
+ ActiveRecord::Base.no_touching do
9
+ # GEO
10
+ Countries.call
11
+ States.call
12
+ Zones.call
12
13
 
13
- # user roles
14
- Roles.call
14
+ # user roles
15
+ Roles.call
15
16
 
16
- # additional data
17
- ReturnsEnvironment.call
18
- ShippingCategories.call
19
- StoreCreditCategories.call
20
- TaxCategories.call
21
- DigitalDelivery.call
17
+ # additional data
18
+ ReturnsEnvironment.call
19
+ ShippingCategories.call
20
+ StoreCreditCategories.call
21
+ TaxCategories.call
22
+ DigitalDelivery.call
22
23
 
23
- # store & stock location
24
- Stores.call
25
- StockLocations.call
26
- AdminUser.call
24
+ # store & stock location
25
+ Stores.call
26
+ StockLocations.call
27
+ AdminUser.call
27
28
 
28
- # add store resources
29
- PaymentMethods.call
30
- ApiKeys.call
31
- AllowedOrigins.call
29
+ # add store resources
30
+ PaymentMethods.call
31
+ ApiKeys.call
32
+ AllowedOrigins.call
33
+ end
32
34
  end
33
35
  end
34
36
  end
@@ -1,9 +1,9 @@
1
1
  <tr>
2
2
  <td class="six sub-columns">
3
3
  <strong>
4
- <%= link_to raw(line_item.variant.product.name), spree_storefront_resource_url(line_item.variant.product) %>
4
+ <%= link_to line_item.variant.product.name, spree_storefront_resource_url(line_item.variant.product) %>
5
5
  </strong>
6
- <%= raw(line_item.variant.options_text) -%>
6
+ <%= line_item.variant.options_text -%>
7
7
  (<%= line_item.variant.sku %>)
8
8
  </td>
9
9
  <td class="six sub-columns last right">
@@ -73,8 +73,12 @@ en:
73
73
  namespace: Namespace
74
74
  spree/option_type:
75
75
  filterable: Filterable
76
+ kind: Kind
76
77
  name: Name
77
78
  presentation: Presentation
79
+ spree/option_value:
80
+ color_code: Color Code
81
+ image: Swatch Image
78
82
  spree/order:
79
83
  checkout_complete: Checkout Complete
80
84
  completed_at: Completed At
@@ -775,6 +779,7 @@ en:
775
779
  approver: Approver
776
780
  are_you_sure: Are you sure?
777
781
  are_you_sure_delete: Are you sure you want to delete this record?
782
+ assets: Media
778
783
  associated_adjustment_closed: The associated adjustment is closed, and will not be recalculated. Do you want to open it?
779
784
  at_symbol: "@"
780
785
  attachments: Attachments
@@ -897,6 +902,7 @@ en:
897
902
  close_sidebar: Close sidebar
898
903
  closed: Closed
899
904
  code: Code
905
+ color_code: Color Code
900
906
  colors: Colors
901
907
  company: Company
902
908
  compare_at_amount: Compare at amount
@@ -1573,6 +1579,11 @@ en:
1573
1579
  option_name: Option name
1574
1580
  option_type: Option Type
1575
1581
  option_type_filterable_info: When an option type is set to Filterable, your storefront visitors are presented with the option to filter a taxon of products based on the option type. A typical example of this would be to filter clothing by size and color.<br><br><b>Please Note:</b> Filters will only be visible in the storefront taxons that contain products with this option type set.
1582
+ option_type_kind_info: Controls how the option is displayed on the storefront. Color swatches show color circles, buttons show clickable chips, and dropdown shows a select menu.
1583
+ option_type_kinds:
1584
+ buttons: Buttons
1585
+ color_swatch: Color Swatch
1586
+ dropdown: Dropdown
1576
1587
  option_type_placeholder: Choose an option type
1577
1588
  option_types: Option Types
1578
1589
  option_value: Option Value
@@ -1826,6 +1837,7 @@ en:
1826
1837
  group: From product group
1827
1838
  manual: Manually choose
1828
1839
  product_sold_out: This item is sold out
1840
+ product_translations: Product Translations
1829
1841
  products: Products
1830
1842
  products_added: Products added
1831
1843
  products_cannot_be_shipped: We cannot ship %{product_names} to your current location at the moment. Please change your shipping address or remove %{product_names} from your cart.
@@ -0,0 +1,11 @@
1
+ class CreateActionTextVideoEmbeds < ActiveRecord::Migration[6.1]
2
+ def change
3
+ create_table :action_text_video_embeds, if_not_exists: true do |t|
4
+ t.string :url, null: false
5
+ t.string :thumbnail_url, null: false
6
+ t.text :raw_html, null: false
7
+
8
+ t.timestamps
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,13 @@
1
+ class AddKindToSpreeOptionTypes < ActiveRecord::Migration[7.2]
2
+ def change
3
+ add_column :spree_option_types, :kind, :string, null: false, default: 'dropdown'
4
+ add_index :spree_option_types, :kind
5
+
6
+ # Backfill: option types named 'color'/'colour' get kind 'color_swatch'
7
+ reversible do |dir|
8
+ dir.up do
9
+ execute "UPDATE spree_option_types SET kind = 'color_swatch' WHERE name IN ('color', 'colour')"
10
+ end
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,5 @@
1
+ class AddColorCodeToSpreeOptionValues < ActiveRecord::Migration[7.2]
2
+ def change
3
+ add_column :spree_option_values, :color_code, :string
4
+ end
5
+ end
@@ -0,0 +1,5 @@
1
+ class AddMarketToSpreeOrders < ActiveRecord::Migration[7.2]
2
+ def change
3
+ add_reference :spree_orders, :market, foreign_key: false, index: true
4
+ end
5
+ end
@@ -0,0 +1,30 @@
1
+ store = Spree::Store.default
2
+
3
+ us = Spree::Country.find_by(iso: 'US')
4
+ ca = Spree::Country.find_by(iso: 'CA')
5
+
6
+ if us
7
+ # Store auto-creates a default market on creation — update it rather than creating a new one
8
+ us_market = store.markets.default.first || store.markets.order(:position).first || store.markets.new
9
+ us_market.name = 'US'
10
+ us_market.currency = 'USD'
11
+ us_market.default_locale = 'en'
12
+ us_market.default = true
13
+ us_market.countries = [us, ca].compact
14
+ us_market.save!
15
+ end
16
+
17
+ eu_zone = Spree::Zone.find_by(name: 'EU_VAT')
18
+
19
+ if eu_zone
20
+ eu_countries = eu_zone.zone_members.where(zoneable_type: 'Spree::Country').map(&:zoneable)
21
+
22
+ if eu_countries.any?
23
+ eu_market = store.markets.find_or_initialize_by(name: 'Europe')
24
+ eu_market.currency = 'EUR'
25
+ eu_market.default_locale = 'de'
26
+ eu_market.supported_locales = 'de,fr,es,it'
27
+ eu_market.countries = eu_countries
28
+ eu_market.save!
29
+ end
30
+ end
@@ -1,6 +1,6 @@
1
- %w[manufacturer material fit].each do |key|
1
+ %w[warranty capacity voltage wattage runtime room_coverage noise_level connectivity].each do |key|
2
2
  Spree::MetafieldDefinition.find_or_create_by!(
3
- namespace: 'properties',
3
+ namespace: 'custom',
4
4
  key: key,
5
5
  resource_type: 'Spree::Product'
6
6
  )
@@ -0,0 +1,4 @@
1
+ color = Spree::OptionType.find_or_initialize_by(name: 'color')
2
+ color.presentation = 'Color'
3
+ color.kind = 'color_swatch'
4
+ color.save!