spree_core 5.4.2 → 5.5.0.rc1
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/helpers/spree/base_helper.rb +0 -82
- data/app/helpers/spree/currency_helper.rb +0 -12
- data/app/helpers/spree/products_helper.rb +0 -8
- data/app/jobs/spree/base_job.rb +18 -0
- data/app/jobs/spree/events/subscriber_job.rb +3 -2
- data/app/jobs/spree/exports/generate_job.rb +11 -0
- data/app/jobs/spree/images/save_from_url_job.rb +23 -8
- data/app/jobs/spree/imports/assign_tags_job.rb +11 -0
- data/app/jobs/spree/imports/base_job.rb +15 -0
- data/app/jobs/spree/imports/create_categories_job.rb +37 -0
- data/app/jobs/spree/imports/create_rows_job.rb +1 -3
- data/app/jobs/spree/imports/process_group_job.rb +8 -6
- data/app/jobs/spree/imports/process_rows_job.rb +1 -3
- data/app/jobs/spree/media/migrate_product_assets_job.rb +83 -0
- data/app/jobs/spree/products/refresh_metrics_job.rb +15 -4
- data/app/jobs/spree/reports/generate_job.rb +11 -0
- data/app/jobs/spree/search_provider/index_job.rb +5 -1
- data/app/jobs/spree/search_provider/remove_job.rb +4 -0
- data/app/jobs/spree/stock_reservations/expire_job.rb +11 -0
- data/app/models/concerns/spree/calculated_adjustments.rb +34 -1
- data/app/models/concerns/spree/display_on.rb +31 -0
- data/app/models/concerns/spree/metafields.rb +167 -5
- data/app/models/concerns/spree/preference_schema.rb +191 -0
- data/app/models/concerns/spree/prefixed_id.rb +94 -11
- data/app/models/concerns/spree/product_scopes.rb +36 -17
- data/app/models/concerns/spree/publishable.rb +1 -1
- data/app/models/concerns/spree/ransackable_attributes.rb +5 -1
- data/app/models/concerns/spree/search_indexable.rb +8 -7
- data/app/models/concerns/spree/searchable.rb +11 -2
- data/app/models/concerns/spree/stores/channels.rb +20 -0
- data/app/models/concerns/spree/stores/markets.rb +21 -5
- data/app/models/concerns/spree/typed_associations.rb +120 -0
- data/app/models/concerns/spree/user_methods.rb +71 -12
- data/app/models/spree/ability.rb +4 -117
- data/app/models/spree/api_key.rb +53 -0
- data/app/models/spree/asset.rb +37 -14
- data/app/models/spree/authentication/strategy_registry.rb +72 -0
- data/app/models/spree/base.rb +18 -1
- data/app/models/spree/channel.rb +159 -0
- data/app/models/spree/country.rb +2 -0
- data/app/models/spree/current.rb +5 -1
- data/app/models/spree/custom_field.rb +9 -0
- data/app/models/spree/custom_field_definition.rb +7 -0
- data/app/models/spree/customer_group.rb +8 -2
- data/app/models/spree/event.rb +6 -6
- data/app/models/spree/export.rb +32 -5
- data/app/models/spree/exports/product_translations.rb +1 -1
- data/app/models/spree/gateway/bogus.rb +6 -1
- data/app/models/spree/gateway.rb +25 -0
- data/app/models/spree/gift_card.rb +1 -1
- data/app/models/spree/gift_card_batch.rb +4 -1
- data/app/models/spree/import.rb +5 -0
- data/app/models/spree/import_row.rb +12 -0
- data/app/models/spree/line_item.rb +7 -2
- data/app/models/spree/market.rb +57 -1
- data/app/models/spree/metafield.rb +38 -0
- data/app/models/spree/metafield_definition.rb +29 -6
- data/app/models/spree/metafields/json.rb +10 -0
- data/app/models/spree/newsletter_subscriber.rb +19 -3
- data/app/models/spree/option_type.rb +48 -7
- data/app/models/spree/order/checkout.rb +3 -3
- data/app/models/spree/order.rb +102 -6
- data/app/models/spree/order_approval.rb +19 -0
- data/app/models/spree/order_cancellation.rb +19 -0
- data/app/models/spree/order_inventory.rb +24 -2
- data/app/models/spree/order_routing/has_strategy_preference.rb +28 -0
- data/app/models/spree/order_routing/rules/default_location.rb +16 -0
- data/app/models/spree/order_routing/rules/minimize_splits.rb +45 -0
- data/app/models/spree/order_routing/rules/preferred_location.rb +22 -0
- data/app/models/spree/order_routing/strategy/base.rb +47 -0
- data/app/models/spree/order_routing/strategy/legacy.rb +33 -0
- data/app/models/spree/order_routing/strategy/reducer.rb +68 -0
- data/app/models/spree/order_routing/strategy/rules.rb +81 -0
- data/app/models/spree/order_routing_rule.rb +75 -0
- data/app/models/spree/payment_setup_sessions/bogus.rb +4 -0
- data/app/models/spree/permission_sets/configuration_management.rb +16 -0
- data/app/models/spree/permission_sets/product_display.rb +2 -0
- data/app/models/spree/permission_sets/product_management.rb +2 -0
- data/app/models/spree/price.rb +14 -1
- data/app/models/spree/price_list.rb +129 -17
- data/app/models/spree/price_rule.rb +11 -1
- data/app/models/spree/price_rules/customer_group_rule.rb +15 -1
- data/app/models/spree/price_rules/market_rule.rb +16 -1
- data/app/models/spree/price_rules/user_rule.rb +21 -2
- data/app/models/spree/product/channels.rb +149 -0
- data/app/models/spree/product/legacy_multi_store_support.rb +40 -0
- data/app/models/spree/product/slugs.rb +1 -1
- data/app/models/spree/product.rb +172 -31
- data/app/models/spree/product_publication.rb +43 -0
- data/app/models/spree/promotion/actions/create_adjustment.rb +4 -0
- data/app/models/spree/promotion/actions/create_item_adjustments.rb +4 -0
- data/app/models/spree/promotion/actions/create_line_items.rb +32 -14
- data/app/models/spree/promotion/rules/country.rb +40 -18
- data/app/models/spree/promotion/rules/customer_group.rb +10 -1
- data/app/models/spree/promotion/rules/product.rb +4 -0
- data/app/models/spree/promotion/rules/taxon.rb +24 -1
- data/app/models/spree/promotion/rules/user.rb +21 -0
- data/app/models/spree/promotion/rules/user_logged_in.rb +6 -0
- data/app/models/spree/promotion.rb +22 -1
- data/app/models/spree/promotion_action.rb +17 -11
- data/app/models/spree/promotion_rule.rb +17 -18
- data/app/models/spree/search_provider/meilisearch.rb +12 -2
- data/app/models/spree/shipment.rb +10 -4
- data/app/models/spree/stock/availability_validator.rb +1 -1
- data/app/models/spree/stock/quantifier.rb +89 -9
- data/app/models/spree/stock_item.rb +36 -0
- data/app/models/spree/stock_location.rb +52 -0
- data/app/models/spree/stock_reservation.rb +38 -0
- data/app/models/spree/stock_reservations/insufficient_stock_error.rb +12 -0
- data/app/models/spree/store.rb +18 -72
- data/app/models/spree/store_credit.rb +0 -8
- data/app/models/spree/store_product.rb +11 -23
- data/app/models/spree/subscriber.rb +12 -12
- data/app/models/spree/taxon.rb +0 -5
- data/app/models/spree/user_identity.rb +1 -2
- data/app/models/spree/variant.rb +132 -18
- data/app/models/spree/variant_media.rb +46 -0
- data/app/models/spree/webhook_delivery.rb +1 -1
- data/app/models/spree/webhook_endpoint.rb +24 -0
- data/app/models/spree/wished_item.rb +0 -13
- data/app/presenters/spree/csv/formula_sanitizer.rb +28 -0
- data/app/presenters/spree/csv/product_variant_presenter.rb +23 -3
- data/app/presenters/spree/search_provider/product_presenter.rb +11 -4
- data/app/presenters/spree/variant_presenter.rb +4 -3
- data/app/services/spree/addresses/update.rb +6 -8
- data/app/services/spree/cart/add_item.rb +10 -0
- data/app/services/spree/cart/empty.rb +2 -0
- data/app/services/spree/cart/remove_line_item.rb +10 -0
- data/app/services/spree/cart/remove_out_of_stock_items.rb +1 -1
- data/app/services/spree/cart/set_quantity.rb +10 -0
- data/app/services/spree/carts/complete.rb +1 -0
- data/app/services/spree/carts/create.rb +1 -0
- data/app/services/spree/carts/update.rb +18 -2
- data/app/services/spree/carts/upsert_items.rb +6 -6
- data/app/services/spree/credit_cards/destroy.rb +1 -1
- data/app/services/spree/imports/row_processors/customer.rb +4 -1
- data/app/services/spree/imports/row_processors/product_variant.rb +95 -57
- data/app/services/spree/newsletter/link_user.rb +53 -0
- data/app/services/spree/newsletter/subscribe.rb +31 -9
- data/app/services/spree/orders/approve.rb +27 -6
- data/app/services/spree/orders/build_shipments.rb +29 -0
- data/app/services/spree/orders/cancel.rb +34 -3
- data/app/services/spree/orders/complete.rb +53 -0
- data/app/services/spree/orders/create.rb +156 -0
- data/app/services/spree/orders/update.rb +51 -0
- data/app/services/spree/orders/upsert_items.rb +70 -0
- data/app/services/spree/payments/handle_webhook.rb +3 -10
- data/app/services/spree/prices/bulk_upsert.rb +201 -0
- data/app/services/spree/products/duplicator.rb +1 -1
- data/app/services/spree/products/prepare_nested_attributes.rb +2 -30
- data/app/services/spree/sample_data/loader.rb +30 -0
- data/app/services/spree/stock_reservations/extend.rb +19 -0
- data/app/services/spree/stock_reservations/release.rb +12 -0
- data/app/services/spree/stock_reservations/reserve.rb +103 -0
- data/app/services/spree/taxons/remove_products.rb +7 -1
- data/app/subscribers/spree/event_log_subscriber.rb +1 -1
- data/app/subscribers/spree/product_metrics_subscriber.rb +3 -7
- data/app/views/spree/invitation_mailer/invitation_email.html.erb +4 -0
- data/config/locales/en.yml +35 -10
- data/config/routes.rb +9 -0
- data/db/migrate/20260429000001_create_spree_order_cancellations.rb +25 -0
- data/db/migrate/20260429000002_create_spree_order_approvals.rb +22 -0
- data/db/migrate/20260429000003_add_status_to_spree_orders.rb +6 -0
- data/db/migrate/20260429000004_add_scopes_to_spree_api_keys.rb +11 -0
- data/db/migrate/20260501000001_create_spree_stock_reservations.rb +19 -0
- data/db/migrate/20260504103113_add_type_to_spree_payment_setup_sessions.rb +6 -0
- data/db/migrate/20260507162651_create_spree_variant_media.rb +23 -0
- data/db/migrate/20260508175303_add_pickup_to_spree_stock_locations.rb +12 -0
- data/db/migrate/20260508204040_create_spree_channels.rb +18 -0
- data/db/migrate/20260508204041_create_spree_order_routing_rules.rb +18 -0
- data/db/migrate/20260508204042_add_preferred_stock_location_to_spree_orders.rb +5 -0
- data/db/migrate/20260508204043_add_channel_id_to_spree_orders.rb +10 -0
- data/db/migrate/20260511000001_backfill_status_on_spree_orders.rb +57 -0
- data/db/migrate/20260515000001_add_store_id_to_spree_newsletter_subscribers.rb +25 -0
- data/db/migrate/20260529000001_add_unique_index_to_spree_price_rules.rb +41 -0
- data/db/migrate/20260529000002_add_unique_index_to_spree_promotion_rules.rb +37 -0
- data/db/migrate/20260601000001_create_spree_product_publications.rb +14 -0
- data/db/migrate/20260601000002_add_store_id_to_spree_products.rb +16 -0
- data/db/migrate/20260602000001_add_default_to_spree_channels.rb +14 -0
- data/db/sample_data/channels.rb +12 -0
- data/db/sample_data/orders.rb +1 -1
- data/db/sample_data/products.csv +212 -212
- data/lib/generators/spree/api_resource/api_resource_generator.rb +353 -0
- data/lib/generators/spree/api_resource/templates/admin_controller.rb.tt +23 -0
- data/lib/generators/spree/api_resource/templates/admin_controller_spec.rb.tt +59 -0
- data/lib/generators/spree/api_resource/templates/admin_serializer.rb.tt +11 -0
- data/lib/generators/spree/api_resource/templates/factory.rb.tt +26 -0
- data/lib/generators/spree/api_resource/templates/store_aliased_serializer.rb.tt +12 -0
- data/lib/generators/spree/api_resource/templates/store_controller.rb.tt +31 -0
- data/lib/generators/spree/api_resource/templates/store_controller_spec.rb.tt +61 -0
- data/lib/generators/spree/api_resource/templates/store_serializer.rb.tt +14 -0
- data/lib/generators/spree/controller_decorator/controller_decorator_generator.rb +66 -0
- data/lib/generators/spree/controller_decorator/templates/controller_decorator.rb.tt +25 -0
- data/lib/generators/spree/model/model_generator.rb +73 -7
- data/lib/generators/spree/model/templates/create_table_migration.rb.tt +40 -0
- data/lib/generators/spree/model/templates/model.rb.tt +28 -2
- data/lib/spree/core/configuration.rb +7 -0
- data/lib/spree/core/controller_helpers/auth.rb +0 -12
- data/lib/spree/core/controller_helpers/currency.rb +0 -17
- data/lib/spree/core/controller_helpers/order.rb +0 -19
- data/lib/spree/core/dependencies.rb +5 -2
- data/lib/spree/core/engine.rb +54 -7
- data/lib/spree/core/permission_configuration.rb +15 -0
- data/lib/spree/core/preferences/masking.rb +47 -0
- data/lib/spree/core/preferences/preferable_class_methods.rb +7 -1
- data/lib/spree/core/version.rb +1 -1
- data/lib/spree/core.rb +56 -5
- data/lib/spree/events/adapters/active_support_notifications.rb +1 -1
- data/lib/spree/events/adapters/base.rb +3 -3
- data/lib/spree/events/registry.rb +1 -1
- data/lib/spree/events.rb +1 -1
- data/lib/spree/permitted_attributes.rb +9 -7
- data/lib/spree/testing_support/factories/address_factory.rb +16 -9
- data/lib/spree/testing_support/factories/api_key_factory.rb +1 -0
- data/lib/spree/testing_support/factories/channel_factory.rb +8 -0
- data/lib/spree/testing_support/factories/line_item_factory.rb +2 -8
- data/lib/spree/testing_support/factories/newsletter_subscriber_factory.rb +2 -0
- data/lib/spree/testing_support/factories/product_factory.rb +16 -7
- data/lib/spree/testing_support/factories/product_publication_factory.rb +6 -0
- data/lib/spree/testing_support/factories/refresh_token_factory.rb +15 -0
- data/lib/spree/testing_support/factories/stock_location_factory.rb +2 -2
- data/lib/spree/testing_support/factories/stock_reservation_factory.rb +31 -0
- data/lib/spree/testing_support/factories/variant_factory.rb +3 -3
- data/lib/spree/testing_support/order_walkthrough.rb +1 -1
- data/lib/spree/testing_support/store.rb +10 -0
- data/lib/spree/upgrades/5_4_to_5_5/manifest.yml +53 -0
- data/lib/tasks/channels.rake +94 -0
- data/lib/tasks/core.rake +1 -0
- data/lib/tasks/media.rake +27 -0
- data/lib/tasks/products.rake +4 -6
- data/lib/tasks/publications.rake +60 -0
- data/lib/tasks/upgrade.rake +211 -0
- metadata +86 -18
- data/app/finders/spree/variants/visible_finder.rb +0 -23
- data/app/paginators/spree/shared/paginate.rb +0 -30
- data/app/presenters/spree/filters/price_presenter.rb +0 -23
- data/app/presenters/spree/filters/price_range_presenter.rb +0 -30
- data/app/presenters/spree/filters/quantified_price_range_presenter.rb +0 -45
- data/app/presenters/spree/product_summary_presenter.rb +0 -27
- data/app/presenters/spree/variants/options_presenter.rb +0 -82
- data/app/services/spree/classifications/reposition.rb +0 -23
- data/app/sorters/spree/orders/sort.rb +0 -10
- data/lib/spree/core/controller_helpers/common.rb +0 -14
- data/lib/spree/core/token_generator.rb +0 -23
- data/lib/spree/database_type_utilities.rb +0 -22
- data/lib/spree/testing_support/bar_ability.rb +0 -14
- data/lib/spree/testing_support/factories/store_product_factory.rb +0 -6
|
@@ -189,14 +189,6 @@ module Spree
|
|
|
189
189
|
"#{id}-SC-#{Time.now.utc.strftime('%Y%m%d%H%M%S%6N')}"
|
|
190
190
|
end
|
|
191
191
|
|
|
192
|
-
class << self
|
|
193
|
-
def default_created_by
|
|
194
|
-
Spree::Deprecation.warn('StoreCredit#default_created_by is deprecated and will be removed in Spree 5.5. Please use store.users.first instead.')
|
|
195
|
-
|
|
196
|
-
Spree::Store.current.users.first
|
|
197
|
-
end
|
|
198
|
-
end
|
|
199
|
-
|
|
200
192
|
private
|
|
201
193
|
|
|
202
194
|
def create_credit_record(amount, action_attributes = {})
|
|
@@ -1,29 +1,17 @@
|
|
|
1
1
|
module Spree
|
|
2
|
+
# Thin AR wrapper over the legacy +spree_products_stores+ join table.
|
|
3
|
+
# Pre-5.5 core used this table to attach products to stores; 5.5+ moved
|
|
4
|
+
# that responsibility onto +Spree::Product#store_id+ + +ProductPublication+.
|
|
5
|
+
#
|
|
6
|
+
# The model exists only to power the 5.4 → 5.5 backfill rake task
|
|
7
|
+
# (+spree:upgrade:populate_publications+). Host apps upgrading from 5.4
|
|
8
|
+
# still have the table; after the backfill runs, +spree_multi_store+ (for
|
|
9
|
+
# multi-store catalogs) keeps the table around, and single-store
|
|
10
|
+
# installations may drop it.
|
|
2
11
|
class StoreProduct < Spree.base_class
|
|
3
|
-
has_prefix_id :sp
|
|
4
|
-
|
|
5
12
|
self.table_name = 'spree_products_stores'
|
|
6
13
|
|
|
7
|
-
belongs_to :
|
|
8
|
-
belongs_to :
|
|
9
|
-
|
|
10
|
-
validates :store, :product, presence: true
|
|
11
|
-
validates :store_id, uniqueness: { scope: :product_id }
|
|
12
|
-
|
|
13
|
-
def refresh_metrics!
|
|
14
|
-
return if product.nil?
|
|
15
|
-
|
|
16
|
-
completed_order_ids = product.completed_orders.where(store_id: store_id).select(:id)
|
|
17
|
-
variant_ids = product.variants_including_master.ids
|
|
18
|
-
|
|
19
|
-
line_items = Spree::LineItem.joins(:order)
|
|
20
|
-
.where(spree_orders: { id: completed_order_ids })
|
|
21
|
-
.where(variant_id: variant_ids)
|
|
22
|
-
|
|
23
|
-
update!(
|
|
24
|
-
units_sold_count: line_items.sum(:quantity),
|
|
25
|
-
revenue: line_items.sum(:pre_tax_amount)
|
|
26
|
-
)
|
|
27
|
-
end
|
|
14
|
+
belongs_to :product, class_name: 'Spree::Product'
|
|
15
|
+
belongs_to :store, class_name: 'Spree::Store'
|
|
28
16
|
end
|
|
29
17
|
end
|
|
@@ -9,7 +9,7 @@ module Spree
|
|
|
9
9
|
#
|
|
10
10
|
# @example Basic subscriber
|
|
11
11
|
# class OrderCompletedNotifier < Spree::Subscriber
|
|
12
|
-
# subscribes_to 'order.
|
|
12
|
+
# subscribes_to 'order.completed'
|
|
13
13
|
#
|
|
14
14
|
# def call(event)
|
|
15
15
|
# order_id = event.payload['id']
|
|
@@ -19,7 +19,7 @@ module Spree
|
|
|
19
19
|
#
|
|
20
20
|
# @example Multi-event subscriber
|
|
21
21
|
# class OrderAuditLogger < Spree::Subscriber
|
|
22
|
-
# subscribes_to 'order.
|
|
22
|
+
# subscribes_to 'order.completed', 'order.canceled', 'order.resumed'
|
|
23
23
|
#
|
|
24
24
|
# def call(event)
|
|
25
25
|
# AuditLog.create!(
|
|
@@ -41,11 +41,11 @@ module Spree
|
|
|
41
41
|
#
|
|
42
42
|
# @example Subscriber with method routing
|
|
43
43
|
# class PaymentSubscriber < Spree::Subscriber
|
|
44
|
-
# subscribes_to 'payment.
|
|
44
|
+
# subscribes_to 'payment.completed', 'payment.voided', 'refund.created'
|
|
45
45
|
#
|
|
46
|
-
# on 'payment.
|
|
47
|
-
# on 'payment.
|
|
48
|
-
# on '
|
|
46
|
+
# on 'payment.completed', :handle_complete
|
|
47
|
+
# on 'payment.voided', :handle_void
|
|
48
|
+
# on 'refund.created', :handle_refund
|
|
49
49
|
#
|
|
50
50
|
# private
|
|
51
51
|
#
|
|
@@ -64,7 +64,7 @@ module Spree
|
|
|
64
64
|
#
|
|
65
65
|
# @example Synchronous subscriber (runs immediately, not via ActiveJob)
|
|
66
66
|
# class CriticalOrderHandler < Spree::Subscriber
|
|
67
|
-
# subscribes_to 'order.
|
|
67
|
+
# subscribes_to 'order.completed', async: false
|
|
68
68
|
#
|
|
69
69
|
# def call(event)
|
|
70
70
|
# # This runs synchronously
|
|
@@ -81,10 +81,10 @@ module Spree
|
|
|
81
81
|
# @return [void]
|
|
82
82
|
#
|
|
83
83
|
# @example
|
|
84
|
-
# subscribes_to 'order.
|
|
85
|
-
# subscribes_to 'order.
|
|
84
|
+
# subscribes_to 'order.completed'
|
|
85
|
+
# subscribes_to 'order.completed', 'order.canceled'
|
|
86
86
|
# subscribes_to 'order.*'
|
|
87
|
-
# subscribes_to 'order.
|
|
87
|
+
# subscribes_to 'order.completed', async: false
|
|
88
88
|
#
|
|
89
89
|
def subscribes_to(*patterns, **options)
|
|
90
90
|
@subscription_patterns ||= []
|
|
@@ -102,8 +102,8 @@ module Spree
|
|
|
102
102
|
# @return [void]
|
|
103
103
|
#
|
|
104
104
|
# @example
|
|
105
|
-
# on 'payment.
|
|
106
|
-
# on 'payment.
|
|
105
|
+
# on 'payment.completed', :handle_complete
|
|
106
|
+
# on 'payment.voided', :handle_void
|
|
107
107
|
#
|
|
108
108
|
def on(pattern, method_name)
|
|
109
109
|
@event_handlers ||= {}
|
data/app/models/spree/taxon.rb
CHANGED
|
@@ -340,11 +340,6 @@ module Spree
|
|
|
340
340
|
end
|
|
341
341
|
end
|
|
342
342
|
|
|
343
|
-
def active_products
|
|
344
|
-
Spree::Deprecation.warn('active_products is deprecated and will be removed in Spree 5.5. Please use taxon.products.active instead.')
|
|
345
|
-
products.active
|
|
346
|
-
end
|
|
347
|
-
|
|
348
343
|
def regenerate_pretty_name_and_permalink
|
|
349
344
|
Mobility.with_locale(nil) do
|
|
350
345
|
update_columns(pretty_name: generate_pretty_name, permalink: generate_slug, updated_at: Time.current)
|
|
@@ -9,8 +9,7 @@ module Spree
|
|
|
9
9
|
|
|
10
10
|
validates :provider, inclusion: {
|
|
11
11
|
in: ->(_record) {
|
|
12
|
-
|
|
13
|
-
(config.store_authentication_strategies.keys + config.admin_authentication_strategies.keys).uniq.map(&:to_s)
|
|
12
|
+
(Spree.store_authentication_strategies.keys + Spree.admin_authentication_strategies.keys).uniq.map(&:to_s)
|
|
14
13
|
}
|
|
15
14
|
}
|
|
16
15
|
|
data/app/models/spree/variant.rb
CHANGED
|
@@ -8,6 +8,7 @@ module Spree
|
|
|
8
8
|
include Spree::MemoizedData
|
|
9
9
|
include Spree::Metafields
|
|
10
10
|
include Spree::Metadata
|
|
11
|
+
include Spree::Searchable
|
|
11
12
|
include Spree::Variant::Webhooks
|
|
12
13
|
|
|
13
14
|
publishes_lifecycle_events
|
|
@@ -36,31 +37,43 @@ module Spree
|
|
|
36
37
|
with_options inverse_of: :variant do
|
|
37
38
|
has_many :inventory_units
|
|
38
39
|
has_many :line_items
|
|
39
|
-
has_many :stock_items, dependent: :destroy
|
|
40
|
+
has_many :stock_items, dependent: :destroy, autosave: true
|
|
40
41
|
end
|
|
41
42
|
|
|
42
43
|
has_many :orders, through: :line_items
|
|
43
44
|
with_options through: :stock_items do
|
|
44
45
|
has_many :stock_locations
|
|
45
46
|
has_many :stock_movements
|
|
47
|
+
has_many :stock_reservations
|
|
46
48
|
end
|
|
47
49
|
|
|
48
50
|
has_many :option_value_variants, class_name: 'Spree::OptionValueVariant'
|
|
49
51
|
has_many :option_values, through: :option_value_variants, dependent: :destroy, class_name: 'Spree::OptionValue'
|
|
50
52
|
|
|
51
53
|
has_many :images, -> { order(:position) }, as: :viewable, dependent: :destroy, class_name: 'Spree::Asset'
|
|
54
|
+
|
|
55
|
+
has_many :variant_media, class_name: 'Spree::VariantMedia', dependent: :destroy
|
|
56
|
+
# Order through the asset's product-level position so a variant's gallery
|
|
57
|
+
# follows whatever ordering the merchant set on the product. There's no
|
|
58
|
+
# per-variant reordering — link/unlink only.
|
|
59
|
+
has_many :associated_media,
|
|
60
|
+
-> { order(Spree::Asset.arel_table[:position].asc) },
|
|
61
|
+
through: :variant_media, source: :asset, class_name: 'Spree::Asset'
|
|
62
|
+
|
|
52
63
|
belongs_to :primary_media, class_name: 'Spree::Asset', optional: true, foreign_key: :primary_media_id
|
|
53
64
|
|
|
54
65
|
has_many :prices,
|
|
55
66
|
class_name: 'Spree::Price',
|
|
56
67
|
dependent: :destroy,
|
|
57
|
-
inverse_of: :variant
|
|
68
|
+
inverse_of: :variant,
|
|
69
|
+
autosave: true
|
|
58
70
|
|
|
59
71
|
has_many :wished_items, dependent: :destroy
|
|
60
72
|
|
|
61
73
|
has_many :digitals
|
|
62
74
|
|
|
63
75
|
before_validation :set_cost_currency
|
|
76
|
+
before_validation :apply_pending_options, if: :pending_options?
|
|
64
77
|
|
|
65
78
|
validate :check_price, if: -> { Spree::Config.enable_legacy_default_price }
|
|
66
79
|
|
|
@@ -128,11 +141,29 @@ module Spree
|
|
|
128
141
|
|
|
129
142
|
scope :with_digital_assets, -> { joins(:digitals) }
|
|
130
143
|
|
|
131
|
-
|
|
132
|
-
|
|
144
|
+
# Free-text variant search: SKU, parent product name, and any
|
|
145
|
+
# option-value presentation (e.g. "Red", "XL"). The 3-char floor
|
|
146
|
+
# keeps single-letter queries from triggering a full scan.
|
|
147
|
+
def self.search(query)
|
|
148
|
+
return none if query.blank? || query.length < 3
|
|
133
149
|
|
|
134
|
-
|
|
135
|
-
|
|
150
|
+
conditions = [
|
|
151
|
+
search_condition(self, :sku, query),
|
|
152
|
+
search_condition(Spree::OptionValue, :presentation, query),
|
|
153
|
+
]
|
|
154
|
+
|
|
155
|
+
if Spree.use_translations?
|
|
156
|
+
translation_table = Product::Translation.arel_table.alias(Product.translation_table_alias)
|
|
157
|
+
sanitized = sanitize_query_for_search(query)
|
|
158
|
+
conditions << translation_table[:name].lower.matches("%#{sanitized}%", '\\')
|
|
159
|
+
else
|
|
160
|
+
conditions << search_condition(Spree::Product, :name, query)
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
relation = joins(:product).left_joins(:option_values)
|
|
164
|
+
relation = relation.join_translation_table(Product) if Spree.use_translations?
|
|
165
|
+
relation.where(conditions.reduce(:or)).distinct
|
|
166
|
+
end
|
|
136
167
|
|
|
137
168
|
# Backward compatibility alias — remove in Spree 6.0
|
|
138
169
|
scope :multi_search, ->(*args) { search(*args) }
|
|
@@ -166,8 +197,8 @@ module Spree
|
|
|
166
197
|
|
|
167
198
|
self.whitelisted_ransackable_associations = %w[option_values product tax_category prices default_price]
|
|
168
199
|
self.whitelisted_ransackable_attributes = %w[weight depth width height sku discontinue_on is_master cost_price cost_currency track_inventory
|
|
169
|
-
deleted_at]
|
|
170
|
-
self.whitelisted_ransackable_scopes = %i(product_name_or_sku_cont search_by_product_name_or_sku)
|
|
200
|
+
deleted_at product_id]
|
|
201
|
+
self.whitelisted_ransackable_scopes = %i(product_name_or_sku_cont search_by_product_name_or_sku search)
|
|
171
202
|
|
|
172
203
|
def self.product_name_or_sku_cont(query)
|
|
173
204
|
sanitized_query = ActiveRecord::Base.sanitize_sql_like(query.to_s.downcase.strip)
|
|
@@ -260,16 +291,21 @@ module Spree
|
|
|
260
291
|
end
|
|
261
292
|
|
|
262
293
|
# Returns the variant's media gallery.
|
|
263
|
-
#
|
|
294
|
+
# Prefers product-level media linked via variant_media (5.5+) — these reuse
|
|
295
|
+
# a single blob across variants. Falls back to direct variant images for
|
|
296
|
+
# legacy uploads.
|
|
264
297
|
# @return [ActiveRecord::Relation]
|
|
265
298
|
def gallery_media
|
|
299
|
+
return associated_media if has_associated_media?
|
|
300
|
+
|
|
266
301
|
images
|
|
267
302
|
end
|
|
268
303
|
|
|
269
|
-
# Returns true if the variant has media.
|
|
270
|
-
# Uses loaded
|
|
304
|
+
# Returns true if the variant has media (linked product-level or direct images).
|
|
305
|
+
# Uses loaded associations when available, otherwise falls back to counter cache.
|
|
271
306
|
# @return [Boolean]
|
|
272
307
|
def has_media?
|
|
308
|
+
return true if has_associated_media?
|
|
273
309
|
return images.any? if images.loaded?
|
|
274
310
|
|
|
275
311
|
media_count.positive?
|
|
@@ -277,6 +313,13 @@ module Spree
|
|
|
277
313
|
|
|
278
314
|
alias has_images? has_media?
|
|
279
315
|
|
|
316
|
+
# @return [Boolean] true if any product-level media is linked to this variant
|
|
317
|
+
def has_associated_media?
|
|
318
|
+
return variant_media.any? if variant_media.loaded?
|
|
319
|
+
|
|
320
|
+
variant_media.exists?
|
|
321
|
+
end
|
|
322
|
+
|
|
280
323
|
# @deprecated Use #primary_media instead.
|
|
281
324
|
def default_image
|
|
282
325
|
Spree::Deprecation.warn('Spree::Variant#default_image is deprecated and will be removed in Spree 6.0. Please use Spree::Variant#primary_media instead.')
|
|
@@ -285,8 +328,10 @@ module Spree
|
|
|
285
328
|
|
|
286
329
|
# Updates primary_media_id to the first media item by position.
|
|
287
330
|
# Called when media is added, removed, or reordered.
|
|
331
|
+
# Uses gallery_media so product-level assets linked via VariantMedia are
|
|
332
|
+
# considered alongside legacy variant-pinned images.
|
|
288
333
|
def update_thumbnail!
|
|
289
|
-
first_media =
|
|
334
|
+
first_media = gallery_media.first
|
|
290
335
|
update_column(:primary_media_id, first_media&.id)
|
|
291
336
|
end
|
|
292
337
|
|
|
@@ -330,6 +375,11 @@ module Spree
|
|
|
330
375
|
# @param options [Array<Hash>] the options to set
|
|
331
376
|
# @return [void]
|
|
332
377
|
def options=(options = {})
|
|
378
|
+
if product.nil?
|
|
379
|
+
@pending_options = options
|
|
380
|
+
return
|
|
381
|
+
end
|
|
382
|
+
|
|
333
383
|
options.each do |option|
|
|
334
384
|
next if option[:name].blank? || option[:value].blank?
|
|
335
385
|
|
|
@@ -438,6 +488,51 @@ module Spree
|
|
|
438
488
|
price_in(currency).try(:compare_at_amount)
|
|
439
489
|
end
|
|
440
490
|
|
|
491
|
+
# Syncs base prices from an array of hashes.
|
|
492
|
+
# Upserts prices for listed currencies, removes base prices for unlisted currencies.
|
|
493
|
+
# On new records, builds prices in memory (saved when variant is saved).
|
|
494
|
+
# On persisted records, saves prices immediately and removes unlisted currencies.
|
|
495
|
+
# An empty array clears every base price — distinguished from `nil` (no
|
|
496
|
+
# change requested), which falls through to the default ActiveRecord setter.
|
|
497
|
+
# @param prices_params [Array<Hash>, nil] array of { currency:, amount:, compare_at_amount: }
|
|
498
|
+
# @return [void]
|
|
499
|
+
def prices=(prices_params)
|
|
500
|
+
return super if prices_params.nil? || prices_params.first.is_a?(Spree::Price)
|
|
501
|
+
|
|
502
|
+
currencies_in_payload = []
|
|
503
|
+
|
|
504
|
+
prices_params.each do |price_data|
|
|
505
|
+
price_data = price_data.to_h.with_indifferent_access
|
|
506
|
+
currencies_in_payload << price_data[:currency]
|
|
507
|
+
set_price(price_data[:currency], price_data[:amount], price_data[:compare_at_amount])
|
|
508
|
+
end
|
|
509
|
+
|
|
510
|
+
# Remove base prices for currencies not in the payload (including the
|
|
511
|
+
# `prices_params == []` case, which clears every base price).
|
|
512
|
+
prices.base_prices.where.not(currency: currencies_in_payload).destroy_all if persisted?
|
|
513
|
+
end
|
|
514
|
+
|
|
515
|
+
# Syncs stock items from an array of hashes.
|
|
516
|
+
# Upserts stock for listed locations, soft-deletes stock items for unlisted locations.
|
|
517
|
+
# On new records, defers to after_create callback.
|
|
518
|
+
# @param stock_items_params [Array<Hash>] array of { stock_location_id:, count_on_hand:, backorderable: }
|
|
519
|
+
# @return [void]
|
|
520
|
+
def stock_items=(stock_items_params)
|
|
521
|
+
return super if stock_items_params.blank? || stock_items_params.first.is_a?(Spree::StockItem)
|
|
522
|
+
|
|
523
|
+
location_ids_in_payload = []
|
|
524
|
+
|
|
525
|
+
stock_items_params.each do |stock_data|
|
|
526
|
+
stock_data = stock_data.to_h.with_indifferent_access
|
|
527
|
+
location = Spree::StockLocation.find_by_param(stock_data[:stock_location_id])
|
|
528
|
+
location_ids_in_payload << location.id
|
|
529
|
+
set_stock(stock_data[:count_on_hand], stock_data[:backorderable], location)
|
|
530
|
+
end
|
|
531
|
+
|
|
532
|
+
# Soft-delete stock items for locations not in the payload
|
|
533
|
+
stock_items.where.not(stock_location_id: location_ids_in_payload).destroy_all if persisted?
|
|
534
|
+
end
|
|
535
|
+
|
|
441
536
|
# Sets the base price (global price, not for a price list) for the given currency.
|
|
442
537
|
# @param currency [String] the currency to set the price for
|
|
443
538
|
# @param amount [BigDecimal] the amount to set
|
|
@@ -465,16 +560,18 @@ module Spree
|
|
|
465
560
|
Spree::Pricing::Resolver.new(context).resolve
|
|
466
561
|
end
|
|
467
562
|
|
|
468
|
-
# Sets the stock for the variant
|
|
563
|
+
# Sets the stock for the variant at a given location.
|
|
564
|
+
# Mirrors set_price: find-or-initialize, set attrs, save only if persisted.
|
|
469
565
|
# @param count_on_hand [Integer] the count on hand
|
|
470
566
|
# @param backorderable [Boolean] the backorderable flag
|
|
471
|
-
# @param stock_location [Spree::StockLocation] the stock location to
|
|
567
|
+
# @param stock_location [Spree::StockLocation] the stock location (defaults to store default)
|
|
472
568
|
# @return [void]
|
|
473
|
-
def set_stock(count_on_hand, backorderable = nil)
|
|
474
|
-
|
|
569
|
+
def set_stock(count_on_hand, backorderable = nil, stock_location = nil)
|
|
570
|
+
stock_location ||= default_stock_location
|
|
571
|
+
stock_item = stock_items.find_or_initialize_by(stock_location: stock_location)
|
|
475
572
|
stock_item.count_on_hand = count_on_hand
|
|
476
573
|
stock_item.backorderable = backorderable if backorderable.present?
|
|
477
|
-
stock_item.save!
|
|
574
|
+
stock_item.save! if persisted?
|
|
478
575
|
end
|
|
479
576
|
|
|
480
577
|
def default_stock_location
|
|
@@ -532,7 +629,7 @@ module Spree
|
|
|
532
629
|
@on_sale ||= price_in(currency)&.discounted?
|
|
533
630
|
end
|
|
534
631
|
|
|
535
|
-
delegate :total_on_hand, :can_supply?, to: :quantifier
|
|
632
|
+
delegate :total_on_hand, :available_stock, :reserved_quantity, :can_supply?, to: :quantifier
|
|
536
633
|
|
|
537
634
|
alias is_backorderable? backorderable?
|
|
538
635
|
|
|
@@ -585,6 +682,23 @@ module Spree
|
|
|
585
682
|
|
|
586
683
|
private
|
|
587
684
|
|
|
685
|
+
def pending_options?
|
|
686
|
+
@pending_options.present?
|
|
687
|
+
end
|
|
688
|
+
|
|
689
|
+
def apply_pending_options
|
|
690
|
+
return unless @pending_options
|
|
691
|
+
|
|
692
|
+
options_to_apply = @pending_options
|
|
693
|
+
@pending_options = nil
|
|
694
|
+
|
|
695
|
+
options_to_apply.each do |option|
|
|
696
|
+
next if option[:name].blank? || option[:value].blank?
|
|
697
|
+
|
|
698
|
+
set_option_value(option[:name], option[:value], option[:position])
|
|
699
|
+
end
|
|
700
|
+
end
|
|
701
|
+
|
|
588
702
|
def ensure_not_in_complete_orders
|
|
589
703
|
if orders.complete.any?
|
|
590
704
|
errors.add(:base, :cannot_destroy_if_attached_to_line_items)
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
module Spree
|
|
2
|
+
# FK column is `media_id` (not `asset_id`) to match the 6.0 rename
|
|
3
|
+
# Spree::Asset → Spree::Media. The `:asset` association name follows the
|
|
4
|
+
# current parent class; in 6.0 it renames to `:media` without a column change.
|
|
5
|
+
class VariantMedia < Spree.base_class
|
|
6
|
+
self.table_name = 'spree_variant_media'
|
|
7
|
+
|
|
8
|
+
belongs_to :variant, class_name: 'Spree::Variant', touch: true
|
|
9
|
+
belongs_to :asset, class_name: 'Spree::Asset', foreign_key: :media_id, inverse_of: :variant_media
|
|
10
|
+
|
|
11
|
+
validates :variant, :asset, presence: true
|
|
12
|
+
validates :media_id, uniqueness: { scope: :variant_id }
|
|
13
|
+
validate :asset_belongs_to_variant_product
|
|
14
|
+
|
|
15
|
+
after_commit :refresh_variant_thumbnail, on: %i[create destroy]
|
|
16
|
+
|
|
17
|
+
# Resolves an array of variant identifiers (prefixed ids or raw ids) to the
|
|
18
|
+
# numeric ids of variants that belong to `product`. Anything else — bad
|
|
19
|
+
# prefix, foreign product, garbage — is dropped. This is the security
|
|
20
|
+
# boundary used by Spree::Asset#variant_ids=, so callers (forms, API params)
|
|
21
|
+
# can't link assets to variants from another product.
|
|
22
|
+
def self.resolve_variant_ids(product, variant_ids)
|
|
23
|
+
ids = Array(variant_ids).reject(&:blank?)
|
|
24
|
+
return [] if ids.empty?
|
|
25
|
+
|
|
26
|
+
product.variants.filter_map do |variant|
|
|
27
|
+
token = variant.id.to_s
|
|
28
|
+
prefixed = variant.prefixed_id
|
|
29
|
+
variant.id if ids.any? { |id| id.to_s == token || id == prefixed }
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
private
|
|
34
|
+
|
|
35
|
+
def asset_belongs_to_variant_product
|
|
36
|
+
return if asset.blank? || variant.blank?
|
|
37
|
+
return if asset.product&.id == variant.product_id
|
|
38
|
+
|
|
39
|
+
errors.add(:asset, 'must belong to the same product as the variant')
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def refresh_variant_thumbnail
|
|
43
|
+
variant&.update_thumbnail!
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
end
|
|
@@ -19,7 +19,7 @@ module Spree
|
|
|
19
19
|
scope :for_event, ->(event_name) { where(event_name: event_name) }
|
|
20
20
|
|
|
21
21
|
# Ransack configuration
|
|
22
|
-
self.whitelisted_ransackable_attributes = %w[event_name response_code execution_time success delivered_at]
|
|
22
|
+
self.whitelisted_ransackable_attributes = %w[event_name response_code execution_time success delivered_at created_at]
|
|
23
23
|
|
|
24
24
|
# Check if the delivery was successful
|
|
25
25
|
#
|
|
@@ -22,6 +22,12 @@ module Spree
|
|
|
22
22
|
validate :url_must_not_resolve_to_private_ip, if: -> { !Rails.env.development? && url.present? && url_changed? }
|
|
23
23
|
|
|
24
24
|
before_create :generate_secret_key
|
|
25
|
+
after_create { @reveal_secret_in_response = true }
|
|
26
|
+
# Re-enabling via a direct `update(active: true)` (e.g., the dashboard's
|
|
27
|
+
# edit form) must also clear the auto-disable bookkeeping so the endpoint
|
|
28
|
+
# rejoins the `enabled` scope. `#enable!` handles this too, but we can't
|
|
29
|
+
# rely on every call site using it.
|
|
30
|
+
before_save :clear_disabled_state_when_reactivated
|
|
25
31
|
|
|
26
32
|
self.whitelisted_ransackable_attributes = %w[name url active]
|
|
27
33
|
|
|
@@ -29,6 +35,17 @@ module Spree
|
|
|
29
35
|
scope :inactive, -> { where(active: false) }
|
|
30
36
|
scope :enabled, -> { active.where(disabled_at: nil) }
|
|
31
37
|
|
|
38
|
+
# Returns the plaintext `secret_key` only on the create response.
|
|
39
|
+
#
|
|
40
|
+
# `@reveal_secret_in_response` is set by the `after_create` callback above
|
|
41
|
+
# — a per-instance flag, not derived from `previous_changes`, so a reload
|
|
42
|
+
# or any subsequent save can't accidentally re-expose the secret.
|
|
43
|
+
#
|
|
44
|
+
# @return [String, nil]
|
|
45
|
+
def secret_key_for_response
|
|
46
|
+
@reveal_secret_in_response ? secret_key : nil
|
|
47
|
+
end
|
|
48
|
+
|
|
32
49
|
# Number of consecutive failed deliveries before auto-disabling
|
|
33
50
|
AUTO_DISABLE_THRESHOLD = 15
|
|
34
51
|
|
|
@@ -128,6 +145,13 @@ module Spree
|
|
|
128
145
|
self.secret_key ||= SecureRandom.hex(32)
|
|
129
146
|
end
|
|
130
147
|
|
|
148
|
+
def clear_disabled_state_when_reactivated
|
|
149
|
+
return unless will_save_change_to_active? && active
|
|
150
|
+
|
|
151
|
+
self.disabled_at = nil
|
|
152
|
+
self.disabled_reason = nil
|
|
153
|
+
end
|
|
154
|
+
|
|
131
155
|
def url_must_not_resolve_to_private_ip
|
|
132
156
|
uri = URI.parse(url)
|
|
133
157
|
blacklist = SsrfFilter::IPV4_BLACKLIST + SsrfFilter::IPV6_BLACKLIST
|
|
@@ -16,19 +16,6 @@ module Spree
|
|
|
16
16
|
validates :variant, uniqueness: { scope: [:wishlist] }
|
|
17
17
|
validates :quantity, numericality: { only_integer: true, greater_than: 0 }
|
|
18
18
|
|
|
19
|
-
# This is a workaround to allow the variant_id to be set with a prefixed ID
|
|
20
|
-
# in the API.
|
|
21
|
-
#
|
|
22
|
-
# @param id [String] the prefixed ID of the variant
|
|
23
|
-
def variant_id=(id)
|
|
24
|
-
if id.to_s.include?('_')
|
|
25
|
-
decoded = Spree::Variant.decode_prefixed_id(id)
|
|
26
|
-
super(decoded)
|
|
27
|
-
else
|
|
28
|
-
super(id)
|
|
29
|
-
end
|
|
30
|
-
end
|
|
31
|
-
|
|
32
19
|
def price(currency)
|
|
33
20
|
variant.amount_in(currency[:currency])
|
|
34
21
|
end
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
module Spree
|
|
2
|
+
module CSV
|
|
3
|
+
# Neutralizes CSV formula injection (CWE-1236 / OWASP "CSV Injection")
|
|
4
|
+
# by prefixing cells that would otherwise be evaluated as a formula
|
|
5
|
+
# when the exported file is opened in Excel, Google Sheets, LibreOffice,
|
|
6
|
+
# or Numbers.
|
|
7
|
+
#
|
|
8
|
+
# The leading apostrophe is the OWASP-recommended marker — spreadsheets
|
|
9
|
+
# render the cell as plain text without displaying the apostrophe.
|
|
10
|
+
module FormulaSanitizer
|
|
11
|
+
TRIGGERS = ["=", "+", "-", "@", "\t", "\r", "\n"].freeze
|
|
12
|
+
|
|
13
|
+
module_function
|
|
14
|
+
|
|
15
|
+
def cell(value)
|
|
16
|
+
return value unless value.is_a?(String)
|
|
17
|
+
return value if value.empty?
|
|
18
|
+
return value unless TRIGGERS.include?(value[0])
|
|
19
|
+
|
|
20
|
+
"'#{value}"
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def row(values)
|
|
24
|
+
values.map { |v| cell(v) }
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
|
@@ -53,7 +53,7 @@ module Spree
|
|
|
53
53
|
@index = index
|
|
54
54
|
@properties = properties
|
|
55
55
|
@taxons = taxons
|
|
56
|
-
@store = store || product.
|
|
56
|
+
@store = store || product.store
|
|
57
57
|
@currency = currency || @store.default_currency
|
|
58
58
|
@price_only = @currency != @store.default_currency
|
|
59
59
|
@metafields = metafields
|
|
@@ -100,8 +100,8 @@ module Spree
|
|
|
100
100
|
variant.dimensions_unit,
|
|
101
101
|
variant.weight,
|
|
102
102
|
variant.weight_unit,
|
|
103
|
-
|
|
104
|
-
(variant.discontinue_on ||
|
|
103
|
+
publication_available_on&.strftime('%Y-%m-%d %H:%M:%S'),
|
|
104
|
+
(variant.discontinue_on || publication_discontinue_on)&.strftime('%Y-%m-%d %H:%M:%S'),
|
|
105
105
|
variant.track_inventory?,
|
|
106
106
|
total_on_hand == BigDecimal::INFINITY ? '∞' : total_on_hand,
|
|
107
107
|
variant.backorderable?,
|
|
@@ -137,6 +137,26 @@ module Spree
|
|
|
137
137
|
|
|
138
138
|
private
|
|
139
139
|
|
|
140
|
+
# Default-channel publication for the export's store. 5.5 transitional:
|
|
141
|
+
# fall back to the legacy Product columns when the publication dates are
|
|
142
|
+
# NULL (pre-backfill). 6.0 drops the Product-column fallback.
|
|
143
|
+
def default_publication
|
|
144
|
+
return @default_publication if defined?(@default_publication)
|
|
145
|
+
|
|
146
|
+
channel_id = store&.default_channel&.id
|
|
147
|
+
@default_publication = channel_id && product.product_publications.find do |p|
|
|
148
|
+
p.store_id == store.id && p.channel_id == channel_id
|
|
149
|
+
end
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
def publication_available_on
|
|
153
|
+
default_publication&.published_at || product.available_on
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
def publication_discontinue_on
|
|
157
|
+
default_publication&.unpublished_at || product.discontinue_on
|
|
158
|
+
end
|
|
159
|
+
|
|
140
160
|
def price_only_row
|
|
141
161
|
csv = Array.new(CSV_HEADERS.size)
|
|
142
162
|
csv[CSV_HEADERS.index('sku')] = variant.sku
|
|
@@ -58,7 +58,8 @@ module Spree
|
|
|
58
58
|
status: product.status,
|
|
59
59
|
sku: product.sku,
|
|
60
60
|
in_stock: product.in_stock?,
|
|
61
|
-
store_ids: product.
|
|
61
|
+
store_ids: Array(product.store_id).map(&:to_s),
|
|
62
|
+
channel_ids: channel_ids_for_store,
|
|
62
63
|
discontinue_on: product.discontinue_on&.to_i || 0,
|
|
63
64
|
category_ids: category_ids_with_ancestors,
|
|
64
65
|
category_names: product.taxons.map { |t| translated(t, :name, fallback_locale) },
|
|
@@ -67,7 +68,7 @@ module Spree
|
|
|
67
68
|
option_value_ids: variant_option_value_ids,
|
|
68
69
|
option_values: variant_option_values_data.map { |ov| translated(ov, :presentation, fallback_locale) }.uniq,
|
|
69
70
|
tags: product.tag_list || [],
|
|
70
|
-
units_sold_count: product.
|
|
71
|
+
units_sold_count: product.units_sold_count || 0,
|
|
71
72
|
available_on: product.available_on&.iso8601,
|
|
72
73
|
created_at: product.created_at&.iso8601,
|
|
73
74
|
updated_at: product.updated_at&.iso8601
|
|
@@ -107,8 +108,14 @@ module Spree
|
|
|
107
108
|
@compare_at_cache[currency]
|
|
108
109
|
end
|
|
109
110
|
|
|
110
|
-
|
|
111
|
-
|
|
111
|
+
def channel_ids_for_store
|
|
112
|
+
@channel_ids_for_store ||= product.product_publications
|
|
113
|
+
.joins(:channel)
|
|
114
|
+
.where(spree_channels: { store_id: store.id })
|
|
115
|
+
.pluck(:channel_id)
|
|
116
|
+
.map(&:to_s)
|
|
117
|
+
end
|
|
118
|
+
|
|
112
119
|
def category_ids_with_ancestors
|
|
113
120
|
@category_ids_with_ancestors ||= product.taxons.flat_map { |t|
|
|
114
121
|
t.self_and_ancestors.map(&:prefixed_id)
|
|
@@ -16,10 +16,11 @@ module Spree
|
|
|
16
16
|
|
|
17
17
|
def call
|
|
18
18
|
@variants.map do |variant|
|
|
19
|
+
price = variant.price_in(current_currency)
|
|
19
20
|
{
|
|
20
|
-
display_price:
|
|
21
|
-
price:
|
|
22
|
-
display_compare_at_price:
|
|
21
|
+
display_price: price.display_price_including_vat_for(current_price_options).to_html,
|
|
22
|
+
price: price,
|
|
23
|
+
display_compare_at_price: price.display_compare_at_price_including_vat_for(current_price_options).to_html,
|
|
23
24
|
should_display_compare_at_price: should_display_compare_at_price?(variant),
|
|
24
25
|
is_product_available_in_currency: @is_product_available_in_currency,
|
|
25
26
|
backorderable: backorderable?(variant),
|