spree_core 6.0.0.rc4 → 6.0.0.rc6
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/jobs/spree/images/save_from_url_job.rb +24 -5
- data/app/models/concerns/spree/account_deactivation.rb +54 -0
- data/app/models/concerns/spree/calculator/currency_amounts.rb +19 -6
- data/app/models/concerns/spree/derived_number.rb +1 -1
- data/app/models/concerns/spree/has_external_references.rb +24 -16
- data/app/models/concerns/spree/prefixed_id.rb +15 -5
- data/app/models/concerns/spree/publishable.rb +47 -1
- data/app/models/concerns/spree/purchase/addresses.rb +114 -36
- data/app/models/concerns/spree/purchase/digital_items.rb +17 -3
- data/app/models/concerns/spree/purchase/freight.rb +2 -1
- data/app/models/concerns/spree/purchase/payment_processing.rb +7 -1
- data/app/models/concerns/spree/purchase/quantity_rules.rb +2 -0
- data/app/models/concerns/spree/stores/channels.rb +3 -1
- data/app/models/concerns/spree/translatable_resource.rb +6 -1
- data/app/models/concerns/spree/webhook_payload_redaction.rb +1 -0
- data/app/models/spree/ability.rb +8 -0
- data/app/models/spree/address.rb +6 -0
- data/app/models/spree/admin_user.rb +1 -0
- data/app/models/spree/base.rb +33 -1
- data/app/models/spree/calculator/shipping/digital_delivery.rb +3 -1
- data/app/models/spree/calculator/shipping/flat_rate.rb +3 -1
- data/app/models/spree/calculator/shipping/per_item.rb +3 -1
- data/app/models/spree/cart.rb +7 -7
- data/app/models/spree/category.rb +76 -5
- data/app/models/spree/channel.rb +79 -40
- data/app/models/spree/company.rb +46 -3
- data/app/models/spree/customer.rb +1 -1
- data/app/models/spree/customer_group.rb +59 -41
- data/app/models/spree/data_request.rb +34 -1
- data/app/models/spree/delivery_method.rb +18 -8
- data/app/models/spree/delivery_profile.rb +18 -0
- data/app/models/spree/digital_asset.rb +14 -0
- data/app/models/spree/event.rb +20 -5
- data/app/models/spree/fulfillment.rb +8 -1
- data/app/models/spree/fulfillment_changer.rb +3 -1
- data/app/models/spree/fulfillment_item.rb +11 -0
- data/app/models/spree/import.rb +20 -2
- data/app/models/spree/order.rb +1 -1
- data/app/models/spree/order_routing/strategy/rules.rb +4 -1
- data/app/models/spree/price_list.rb +15 -0
- data/app/models/spree/product/channels.rb +16 -8
- data/app/models/spree/product.rb +7 -2
- data/app/models/spree/return.rb +8 -1
- data/app/models/spree/sales_context.rb +162 -0
- data/app/models/spree/search_provider/base.rb +8 -3
- data/app/models/spree/search_provider/database.rb +2 -10
- data/app/models/spree/seller.rb +35 -5
- data/app/models/spree/seller_balance.rb +28 -0
- data/app/models/spree/seller_requirements/delivery_method.rb +20 -2
- data/app/models/spree/stock/coordinator.rb +7 -4
- data/app/models/spree/stock/estimator.rb +6 -0
- data/app/models/spree/stock/inventory_unit_builder.rb +14 -2
- data/app/models/spree/stock/packer.rb +39 -5
- data/app/models/spree/stock/quantifier.rb +16 -0
- data/app/models/spree/stock_location.rb +15 -8
- data/app/models/spree/store.rb +31 -2
- data/app/models/spree/store_credit.rb +4 -4
- data/app/models/spree/tax_line.rb +5 -4
- data/app/models/spree/variant.rb +23 -6
- data/app/models/spree/webhook_endpoint.rb +45 -11
- data/app/services/spree/carts/destroy.rb +7 -2
- data/app/services/spree/carts/price_items.rb +3 -0
- data/app/services/spree/carts/split_by_seller.rb +3 -1
- data/app/services/spree/carts/update.rb +1 -4
- data/app/services/spree/catalogs/set_product_terms.rb +8 -3
- data/app/services/spree/collections/add_products.rb +1 -1
- data/app/services/spree/collections/regenerate_products.rb +1 -3
- data/app/services/spree/collections/remove_products.rb +1 -1
- data/app/services/spree/data_requests/confirm.rb +37 -0
- data/app/services/spree/data_requests/create.rb +61 -13
- data/app/services/spree/newsletter/subscribe.rb +9 -1
- data/app/services/spree/order_groups/allocate_payments.rb +7 -5
- data/app/services/spree/orders/update.rb +19 -0
- data/app/services/spree/product_memberships.rb +16 -6
- data/app/services/spree/products/duplicator.rb +12 -4
- data/app/services/spree/search_provider/custom_field_schema.rb +10 -2
- data/app/services/spree/stock_levels/correct.rb +11 -4
- data/app/services/spree/stock_reservations/reserve.rb +1 -1
- data/app/services/spree/stores/provision_defaults.rb +11 -7
- data/app/services/spree/tags/bulk_add.rb +16 -6
- data/app/workflows/spree/carts/complete.rb +25 -9
- data/app/workflows/spree/carts/upsert_items.rb +31 -4
- data/app/workflows/spree/customers/anonymize.rb +40 -19
- data/app/workflows/spree/data_requests/fulfill.rb +20 -6
- data/app/workflows/spree/digital_assets/create.rb +56 -0
- data/app/workflows/spree/digital_assets/update.rb +44 -0
- data/app/workflows/spree/digital_assets/writing.rb +108 -0
- data/app/workflows/spree/imports/start_mapping.rb +3 -1
- data/app/workflows/spree/orders/complete.rb +12 -5
- data/app/workflows/spree/orders/create.rb +41 -17
- data/app/workflows/spree/payments/capture.rb +12 -0
- data/app/workflows/spree/products/nested_attributes.rb +12 -28
- data/app/workflows/spree/products/propose.rb +9 -4
- data/app/workflows/spree/stock_levels/bulk_upsert.rb +28 -13
- data/app/workflows/spree/variants/nested_attributes.rb +35 -10
- data/config/locales/en.yml +14 -0
- data/db/migrate/20261022000001_store_credit_actor_polymorphic.rb +13 -0
- data/db/migrate/20261023100001_add_deactivated_at_to_spree_admin_users.rb +7 -0
- data/lib/generators/spree/dummy/templates/rails/test.rb +87 -0
- data/lib/spree/checkout/default_requirements.rb +12 -2
- data/lib/spree/core/dependencies.rb +5 -0
- data/lib/spree/core/engine.rb +2 -0
- data/lib/spree/core/permissions/default_catalog.rb +7 -2
- data/lib/spree/core/version.rb +1 -1
- data/lib/spree/reporting/default_vocabulary.rb +1 -1
- data/lib/spree/testing_support/factories/data_request_factory.rb +4 -0
- data/lib/spree/testing_support/factories/product_factory.rb +10 -4
- data/lib/spree/testing_support/factories/store_factory.rb +4 -1
- data/lib/spree/upgrades/5_6_to_6_0/manifest.yml +10 -0
- data/lib/tasks/backfill_tagging_tenants.rake +35 -0
- data/lib/tasks/seller_sample_data.rake +114 -112
- metadata +15 -6
|
@@ -7,14 +7,6 @@ module Spree
|
|
|
7
7
|
'best_selling' => :by_best_selling
|
|
8
8
|
}.freeze
|
|
9
9
|
|
|
10
|
-
# @param store [Spree::Store]
|
|
11
|
-
# @param audience [Symbol, nil] who filters: `:store` (shoppers) limits
|
|
12
|
-
# filters to what the storefront may query; `nil` is the back office
|
|
13
|
-
def initialize(store, audience: :store)
|
|
14
|
-
super(store)
|
|
15
|
-
@audience = audience
|
|
16
|
-
end
|
|
17
|
-
|
|
18
10
|
def search_and_filter(scope:, query: nil, filters: {}, sort: nil, page: 1, limit: 25)
|
|
19
11
|
filters = filters.is_a?(Hash) ? filters.dup : {}
|
|
20
12
|
option_value_ids = filters.delete('with_option_value_ids') || filters.delete(:with_option_value_ids)
|
|
@@ -73,14 +65,14 @@ module Spree
|
|
|
73
65
|
private
|
|
74
66
|
|
|
75
67
|
def apply_search_and_filters(scope, query: nil, filters: {})
|
|
76
|
-
scope = scope.search(query, store) if query.present?
|
|
68
|
+
scope = scope.search(query, store, audience: audience) if query.present?
|
|
77
69
|
|
|
78
70
|
filters = filters.to_unsafe_h if filters.respond_to?(:to_unsafe_h)
|
|
79
71
|
scope, filters = scope.with_custom_field_filters(filters, schema: custom_field_schema)
|
|
80
72
|
|
|
81
73
|
ransack_filters = sanitize_filters(filters)
|
|
82
74
|
if ransack_filters.present?
|
|
83
|
-
search = scope.ransack(ransack_filters, auth_object:
|
|
75
|
+
search = scope.ransack(ransack_filters, auth_object: audience)
|
|
84
76
|
scope = search.result(distinct: true)
|
|
85
77
|
end
|
|
86
78
|
|
data/app/models/spree/seller.rb
CHANGED
|
@@ -310,10 +310,7 @@ module Spree
|
|
|
310
310
|
#
|
|
311
311
|
# @return [Array<Spree::SellerBalance>]
|
|
312
312
|
def balances
|
|
313
|
-
|
|
314
|
-
distinct.pluck(:currency, :settled_currency)
|
|
315
|
-
|
|
316
|
-
pairs.sort.map { |currency, settlement| Spree::SellerBalance.for(self, currency, settlement) }
|
|
313
|
+
Spree::SellerBalance.all_for(self)
|
|
317
314
|
end
|
|
318
315
|
|
|
319
316
|
# The seller's account with whichever provider pays them — a Stripe Connect
|
|
@@ -399,6 +396,10 @@ module Spree
|
|
|
399
396
|
# returns; falls back to any active one, since a seller with none marked
|
|
400
397
|
# still has to receive what customers send back.
|
|
401
398
|
def returns_location
|
|
399
|
+
if stock_locations.loaded? || lazy_preload_context
|
|
400
|
+
return @returns_location ||= returns_location_from_loaded
|
|
401
|
+
end
|
|
402
|
+
|
|
402
403
|
@returns_location ||= stock_locations.active.returns_enabled.order_default.first ||
|
|
403
404
|
stock_locations.active.order_default.first
|
|
404
405
|
end
|
|
@@ -417,9 +418,24 @@ module Spree
|
|
|
417
418
|
location.address
|
|
418
419
|
end
|
|
419
420
|
|
|
421
|
+
# A team is small, so on a list its members are loaded for every seller
|
|
422
|
+
# at once rather than counted one seller at a time.
|
|
423
|
+
#
|
|
424
|
+
# @return [Integer] how many people are on this seller's team
|
|
425
|
+
def users_count
|
|
426
|
+
users.loaded? || lazy_preload_context ? users.to_a.size : users.count
|
|
427
|
+
end
|
|
428
|
+
|
|
420
429
|
# @return [Integer] how many products this seller lists
|
|
421
430
|
def products_count
|
|
422
|
-
@products_count
|
|
431
|
+
return @products_count if @products_count
|
|
432
|
+
|
|
433
|
+
siblings = lazy_preload_context&.records&.grep(self.class)
|
|
434
|
+
return @products_count = products.count if siblings.blank?
|
|
435
|
+
|
|
436
|
+
counts = Spree::Product.where(seller_id: siblings.map(&:id)).group(:seller_id).count
|
|
437
|
+
siblings.each { |seller| seller.preloaded_products_count = counts.fetch(seller.id, 0) }
|
|
438
|
+
@products_count
|
|
423
439
|
end
|
|
424
440
|
|
|
425
441
|
# Where this seller stands against the marketplace's checklist
|
|
@@ -465,8 +481,22 @@ module Spree
|
|
|
465
481
|
onboarding_requirements.none?(&:blocking?)
|
|
466
482
|
end
|
|
467
483
|
|
|
484
|
+
protected
|
|
485
|
+
|
|
486
|
+
def preloaded_products_count=(count)
|
|
487
|
+
@products_count = count
|
|
488
|
+
end
|
|
489
|
+
|
|
468
490
|
private
|
|
469
491
|
|
|
492
|
+
# The same choice as the query in {#returns_location}, made over the
|
|
493
|
+
# preloaded locations.
|
|
494
|
+
def returns_location_from_loaded
|
|
495
|
+
active_locations = stock_locations.select(&:active?).sort_by { |location| [location.default? ? 0 : 1, location.name.to_s] }
|
|
496
|
+
|
|
497
|
+
active_locations.find(&:returns_enabled?) || active_locations.first
|
|
498
|
+
end
|
|
499
|
+
|
|
470
500
|
|
|
471
501
|
def normalize_slug
|
|
472
502
|
self.slug = (slug.presence || name).to_s.parameterize.presence
|
|
@@ -57,6 +57,34 @@ module Spree
|
|
|
57
57
|
)
|
|
58
58
|
end
|
|
59
59
|
|
|
60
|
+
# Every position the seller holds, read with one grouped query per figure
|
|
61
|
+
# rather than four per position. See {Spree::Seller#balances}.
|
|
62
|
+
#
|
|
63
|
+
# @param seller [Spree::Seller]
|
|
64
|
+
# @return [Array<Spree::SellerBalance>]
|
|
65
|
+
def self.all_for(seller)
|
|
66
|
+
transfers = seller.seller_transfers.where.not(status: 'failed')
|
|
67
|
+
amounts = transfers.group(:currency, :settled_currency, :status).sum(:amount)
|
|
68
|
+
payable = transfers.completed.group(:currency, :settled_currency).sum(:settled_amount)
|
|
69
|
+
paid = transfers.completed.joins(:payout).merge(Spree::SellerPayout.completed).
|
|
70
|
+
group(:currency, :settled_currency).sum(:settled_amount)
|
|
71
|
+
|
|
72
|
+
pairs = amounts.keys.map { |currency, settlement_currency, _status| [currency, settlement_currency] }.uniq.sort
|
|
73
|
+
pairs.map do |pair|
|
|
74
|
+
amount_in = ->(*statuses) { statuses.sum(BigDecimal('0')) { |status| amounts.fetch([*pair, status], 0) } }
|
|
75
|
+
|
|
76
|
+
new(
|
|
77
|
+
seller: seller,
|
|
78
|
+
currency: pair.first,
|
|
79
|
+
settlement_currency: pair.last,
|
|
80
|
+
earned: amount_in.call('completed'),
|
|
81
|
+
pending: amount_in.call('pending', 'processing', 'unresolved'),
|
|
82
|
+
payable: payable.fetch(pair, 0),
|
|
83
|
+
paid: paid.fetch(pair, 0)
|
|
84
|
+
)
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
|
|
60
88
|
# What the marketplace still owes, in the currency it can actually be sent
|
|
61
89
|
# in.
|
|
62
90
|
#
|
|
@@ -11,9 +11,27 @@ module Spree
|
|
|
11
11
|
# has shared at least one of theirs.
|
|
12
12
|
class DeliveryMethod < Spree::SellerRequirement
|
|
13
13
|
def met_by_seller?(seller)
|
|
14
|
-
return true if seller
|
|
14
|
+
return true if ships_own_methods?(seller)
|
|
15
15
|
|
|
16
|
-
|
|
16
|
+
operator_shares_methods?
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
private
|
|
20
|
+
|
|
21
|
+
def ships_own_methods?(seller)
|
|
22
|
+
if seller.read_association_in_memory?(:delivery_methods)
|
|
23
|
+
seller.delivery_methods.any?(&:storefront_visible?)
|
|
24
|
+
else
|
|
25
|
+
seller.delivery_methods.storefront_visible.exists?
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
# The same answer for every seller of the store, so a list of them
|
|
30
|
+
# asks once.
|
|
31
|
+
def operator_shares_methods?
|
|
32
|
+
return @operator_shares_methods if defined?(@operator_shares_methods)
|
|
33
|
+
|
|
34
|
+
@operator_shares_methods = store.delivery_methods.shared_with_sellers.storefront_visible.exists?
|
|
17
35
|
end
|
|
18
36
|
end
|
|
19
37
|
end
|
|
@@ -28,11 +28,14 @@ module Spree
|
|
|
28
28
|
end
|
|
29
29
|
|
|
30
30
|
def build_packages(packages = [])
|
|
31
|
-
stock_locations_with_requested_variants.
|
|
31
|
+
stock_locations = stock_locations_with_requested_variants.to_a
|
|
32
|
+
stock_levels = Packer.stock_levels_by_location(stock_locations, requested_variant_ids)
|
|
33
|
+
|
|
34
|
+
stock_locations.each do |stock_location|
|
|
32
35
|
units = allocatable_units_for(stock_location)
|
|
33
36
|
next if units.empty?
|
|
34
37
|
|
|
35
|
-
packer = build_packer(stock_location, units)
|
|
38
|
+
packer = build_packer(stock_location, units, stock_levels: stock_levels.fetch(stock_location.id, {}))
|
|
36
39
|
packages += packer.packages
|
|
37
40
|
end
|
|
38
41
|
|
|
@@ -114,8 +117,8 @@ module Spree
|
|
|
114
117
|
@estimator ||= Estimator.new(order)
|
|
115
118
|
end
|
|
116
119
|
|
|
117
|
-
def build_packer(stock_location, inventory_units)
|
|
118
|
-
Packer.new(stock_location, inventory_units, splitters(stock_location), owner: order)
|
|
120
|
+
def build_packer(stock_location, inventory_units, stock_levels: nil)
|
|
121
|
+
Packer.new(stock_location, inventory_units, splitters(stock_location), owner: order, stock_levels: stock_levels)
|
|
119
122
|
end
|
|
120
123
|
|
|
121
124
|
def splitters(_stock_location)
|
|
@@ -162,6 +162,12 @@ module Spree
|
|
|
162
162
|
methods = package.eligible_delivery_methods
|
|
163
163
|
# Storeless orders exist only in specs; real orders always carry one.
|
|
164
164
|
methods = methods.merge(order.store.delivery_methods) if order.store
|
|
165
|
+
# Read for every method below and when its rates are built. Through the
|
|
166
|
+
# preloader because an overridden package may hand back an Array.
|
|
167
|
+
methods = methods.to_a
|
|
168
|
+
ActiveRecord::Associations::Preloader.new(
|
|
169
|
+
records: methods, associations: %i[delivery_method_rules calculator services delivery_zone]
|
|
170
|
+
).call
|
|
165
171
|
|
|
166
172
|
# On a marketplace, a package's seller is its stock location's, and it
|
|
167
173
|
# is quoted by that seller's own methods plus whatever the operator
|
|
@@ -11,16 +11,28 @@ module Spree
|
|
|
11
11
|
# rather than pointing at whatever order happens to share the cart's
|
|
12
12
|
# id — a stranger's order whose currency and ship address would then
|
|
13
13
|
# stand in for the customer's.
|
|
14
|
-
@order.line_items.
|
|
14
|
+
line_items = @order.line_items.to_a
|
|
15
|
+
# Allocation asks each variant how it ships, so the variants and their
|
|
16
|
+
# delivery profiles are read once for the whole cart.
|
|
17
|
+
ActiveRecord::Associations::Preloader.new(
|
|
18
|
+
records: line_items, associations: { variant: [:delivery_profile, { product: :delivery_profile }] }
|
|
19
|
+
).call
|
|
20
|
+
|
|
21
|
+
line_items.map do |line_item|
|
|
15
22
|
# They go through multiple splits, avoid loading the
|
|
16
23
|
# association to order until needed.
|
|
17
|
-
Spree::FulfillmentItem.new(
|
|
24
|
+
unit = Spree::FulfillmentItem.new(
|
|
18
25
|
pending: true,
|
|
19
26
|
line_item_id: line_item.id,
|
|
20
27
|
variant_id: line_item.variant_id,
|
|
21
28
|
quantity: line_item.quantity,
|
|
22
29
|
order_id: @order.is_a?(Spree::Order) ? @order.id : nil
|
|
23
30
|
)
|
|
31
|
+
# Set as targets rather than assigned, so the proposed unit is not
|
|
32
|
+
# added to the line item's own fulfillment_items.
|
|
33
|
+
unit.association(:line_item).target = line_item
|
|
34
|
+
unit.association(:variant).target = line_item.variant
|
|
35
|
+
unit
|
|
24
36
|
end
|
|
25
37
|
end
|
|
26
38
|
end
|
|
@@ -9,9 +9,25 @@ module Spree
|
|
|
9
9
|
# belong to no order yet — so anything reading the buyer through the
|
|
10
10
|
# package (the packaging tare, the channel and company eligibility
|
|
11
11
|
# rules) would otherwise see nothing.
|
|
12
|
-
|
|
12
|
+
# Every candidate location's levels for the given variants in one query,
|
|
13
|
+
# lowest id first as StockLocation#stock_level reads, for packing several
|
|
14
|
+
# locations without a read per location.
|
|
15
|
+
#
|
|
16
|
+
# @param stock_locations [Array<Spree::StockLocation>]
|
|
17
|
+
# @param variant_ids [Array]
|
|
18
|
+
# @return [Hash{Integer => Hash{Integer => Spree::StockLevel}}] by location, then variant
|
|
19
|
+
def self.stock_levels_by_location(stock_locations, variant_ids)
|
|
20
|
+
Spree::StockLevel.where(stock_location_id: stock_locations.map(&:id), variant_id: variant_ids).
|
|
21
|
+
order(:id).group_by(&:stock_location_id).
|
|
22
|
+
transform_values { |levels| levels.group_by(&:variant_id).transform_values(&:first) }
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
# @param stock_levels [Hash{Integer => Spree::StockLevel}, nil] this
|
|
26
|
+
# location's levels by variant id, when the caller read them already
|
|
27
|
+
def initialize(stock_location, inventory_units, splitters = [Splitter::Base], owner: nil, stock_levels: nil)
|
|
13
28
|
@stock_location = stock_location
|
|
14
29
|
@inventory_units = inventory_units
|
|
30
|
+
@stock_levels = stock_levels
|
|
15
31
|
@splitters = splitters
|
|
16
32
|
@owner = owner
|
|
17
33
|
end
|
|
@@ -28,13 +44,21 @@ module Spree
|
|
|
28
44
|
package = build_package
|
|
29
45
|
|
|
30
46
|
# Group by variant_id as grouping by variant fires cached query.
|
|
31
|
-
inventory_units.index_by(&:variant_id)
|
|
32
|
-
|
|
47
|
+
units_by_variant_id = inventory_units.index_by(&:variant_id)
|
|
48
|
+
# The package's variants and their levels here, each in one query
|
|
49
|
+
# rather than one per variant. Lowest id first, as StockLocation#stock_level reads.
|
|
50
|
+
variants = variants_for(units_by_variant_id)
|
|
51
|
+
stock_levels = @stock_levels || stock_location.stock_levels.where(variant_id: units_by_variant_id.keys).order(:id).
|
|
52
|
+
group_by(&:variant_id).transform_values(&:first)
|
|
53
|
+
|
|
54
|
+
units_by_variant_id.each do |variant_id, inventory_unit|
|
|
55
|
+
variant = variants[variant_id] || Spree::Variant.find(variant_id)
|
|
33
56
|
unit = inventory_unit.dup # Can be used by others, do not use directly
|
|
34
57
|
if variant.should_track_inventory?
|
|
35
|
-
|
|
58
|
+
stock_level = stock_levels[variant_id]
|
|
59
|
+
next if stock_level.nil?
|
|
36
60
|
|
|
37
|
-
on_hand, backordered = stock_location.fill_status(variant, unit.quantity)
|
|
61
|
+
on_hand, backordered = stock_location.fill_status(variant, unit.quantity, stock_level: stock_level)
|
|
38
62
|
package.add(FulfillmentItem.split(unit, backordered), :backordered) if backordered.positive?
|
|
39
63
|
package.add(FulfillmentItem.split(unit, on_hand), :on_hand) if on_hand.positive?
|
|
40
64
|
else
|
|
@@ -57,6 +81,16 @@ module Spree
|
|
|
57
81
|
|
|
58
82
|
private
|
|
59
83
|
|
|
84
|
+
# The units' own variants when they carry them, so a cart's variants are
|
|
85
|
+
# read once however many locations are packed.
|
|
86
|
+
def variants_for(units_by_variant_id)
|
|
87
|
+
variants = units_by_variant_id.each_with_object({}) do |(variant_id, unit), loaded|
|
|
88
|
+
loaded[variant_id] = unit.variant if unit.association(:variant).loaded?
|
|
89
|
+
end
|
|
90
|
+
missing_ids = units_by_variant_id.keys - variants.keys
|
|
91
|
+
missing_ids.empty? ? variants : variants.merge(Spree::Variant.where(id: missing_ids).index_by(&:id))
|
|
92
|
+
end
|
|
93
|
+
|
|
60
94
|
def build_splitter
|
|
61
95
|
splitter = nil
|
|
62
96
|
splitters.reverse_each do |klass|
|
|
@@ -3,6 +3,22 @@ module Spree
|
|
|
3
3
|
class Quantifier
|
|
4
4
|
attr_reader :variant, :stock_location, :excluded_order, :provided_stock_levels
|
|
5
5
|
|
|
6
|
+
# What a quantifier reads, preloaded for many variants at once, so
|
|
7
|
+
# quantifying each of them reads memory instead of querying per variant.
|
|
8
|
+
# Variants whose levels are loaded already are left as they are.
|
|
9
|
+
#
|
|
10
|
+
# @param variants [Enumerable<Spree::Variant>]
|
|
11
|
+
# @return [void]
|
|
12
|
+
def self.preload(variants)
|
|
13
|
+
unloaded = variants.compact.reject { |variant| variant.association(:stock_levels).loaded? }
|
|
14
|
+
return if unloaded.empty?
|
|
15
|
+
|
|
16
|
+
ActiveRecord::Associations::Preloader.new(
|
|
17
|
+
records: unloaded,
|
|
18
|
+
associations: { stock_levels: [:stock_location, :active_stock_reservations] }
|
|
19
|
+
).call
|
|
20
|
+
end
|
|
21
|
+
|
|
6
22
|
# @param excluded_order [Spree::Cart, Spree::Order, nil] when given,
|
|
7
23
|
# reservations belonging to this cart/order are not counted against
|
|
8
24
|
# availability. Used when checking a cart's own line items so the
|
|
@@ -182,8 +182,10 @@ module Spree
|
|
|
182
182
|
#
|
|
183
183
|
# @param fulfillment [Spree::Fulfillment]
|
|
184
184
|
# @return [Spree::StockMovement]
|
|
185
|
-
|
|
186
|
-
|
|
185
|
+
# @param stock_level [Spree::StockLevel, nil] the variant's level here, when
|
|
186
|
+
# the caller loaded it already
|
|
187
|
+
def allocate(variant, quantity, fulfillment, stock_level: nil)
|
|
188
|
+
move(variant, quantity, kind: 'allocated', cause: fulfillment, stock_level: stock_level)
|
|
187
189
|
end
|
|
188
190
|
|
|
189
191
|
# Withdraws a promise — a canceled, relocated or shrunk fulfillment.
|
|
@@ -199,21 +201,26 @@ module Spree
|
|
|
199
201
|
# a negative quantity writes stock off.
|
|
200
202
|
#
|
|
201
203
|
# @param reason [String] audit text, required
|
|
204
|
+
# @param stock_level [Spree::StockLevel, nil] the variant's level here, when
|
|
205
|
+
# the caller holds it already (locked, say), so the movement updates that
|
|
206
|
+
# instance rather than one looked up again
|
|
202
207
|
# @return [Spree::StockMovement]
|
|
203
|
-
def adjust(variant, quantity, reason:)
|
|
204
|
-
move(variant, quantity, kind: 'adjusted', reason: reason)
|
|
208
|
+
def adjust(variant, quantity, reason:, stock_level: nil)
|
|
209
|
+
move(variant, quantity, kind: 'adjusted', reason: reason, stock_level: stock_level)
|
|
205
210
|
end
|
|
206
211
|
|
|
207
|
-
def move(variant, quantity, kind:, cause: nil, reason: nil, force: false, unit_cost: nil)
|
|
208
|
-
stock_level
|
|
212
|
+
def move(variant, quantity, kind:, cause: nil, reason: nil, force: false, unit_cost: nil, stock_level: nil)
|
|
213
|
+
stock_level ||= stock_level_or_create(variant)
|
|
209
214
|
attributes = { quantity: quantity, kind: kind, reason: reason, unit_cost: unit_cost,
|
|
210
215
|
**cause_attributes(cause) }
|
|
211
216
|
|
|
212
217
|
stock_level.stock_movements.create!(attributes) { |movement| movement.force = force }
|
|
213
218
|
end
|
|
214
219
|
|
|
215
|
-
|
|
216
|
-
|
|
220
|
+
# @param stock_level [Spree::StockLevel, nil] the variant's level here, when
|
|
221
|
+
# the caller loaded it already
|
|
222
|
+
def fill_status(variant, quantity, stock_level: nil)
|
|
223
|
+
if item = stock_level || stock_level_or_create(variant)
|
|
217
224
|
if item.available_count >= quantity
|
|
218
225
|
on_hand = quantity
|
|
219
226
|
backordered = 0
|
data/app/models/spree/store.rb
CHANGED
|
@@ -78,6 +78,22 @@ module Spree
|
|
|
78
78
|
# same reason as auto_approve_sellers: what the marketplace lists is what
|
|
79
79
|
# it vouches for.
|
|
80
80
|
preference :auto_approve_seller_products, :boolean, default: false
|
|
81
|
+
# Let sellers attach downloadable files to their own products. Off for the
|
|
82
|
+
# same reason again: a file a buyer downloads under the marketplace's name
|
|
83
|
+
# is something the marketplace vouches for, and that should be a choice.
|
|
84
|
+
preference :seller_digital_assets_enabled, :boolean, default: false
|
|
85
|
+
# What a seller's file may be, checked against the file's own signature
|
|
86
|
+
# rather than its name. An entry names one type or, with `*` as the
|
|
87
|
+
# subtype, a whole family. The operator's own uploads are not restricted.
|
|
88
|
+
preference :seller_digital_asset_content_types, :array, of: :string,
|
|
89
|
+
default: Spree::DigitalAsset::DEFAULT_SELLER_CONTENT_TYPES
|
|
90
|
+
# A typo here would refuse every seller upload with a message about the
|
|
91
|
+
# file, so the list is checked for the type/subtype shape when written.
|
|
92
|
+
validate :seller_digital_asset_content_types_are_media_types
|
|
93
|
+
# A file added to or replaced on a seller's product already on sale sends
|
|
94
|
+
# the product back to review, so an approved listing cannot change what it
|
|
95
|
+
# delivers without anyone looking. Moot when seller products auto-approve.
|
|
96
|
+
preference :review_seller_digital_asset_changes, :boolean, default: true
|
|
81
97
|
# Who pays this store's sellers. Blank means core's record-only provider,
|
|
82
98
|
# so a marketplace that has connected nothing still keeps a correct ledger
|
|
83
99
|
# and settles by hand.
|
|
@@ -157,6 +173,8 @@ module Spree
|
|
|
157
173
|
# B2B store collects it from everyone, a mixed store only offers it.
|
|
158
174
|
preference :address_requires_company, :boolean, default: false
|
|
159
175
|
preference :address_requires_phone, :boolean, default: false
|
|
176
|
+
MEDIA_TYPE_FORMAT = %r{\A[a-z0-9][a-z0-9!#$&^_.+-]*/[a-z0-9][a-z0-9!#$&^_.+*-]*\z}i
|
|
177
|
+
|
|
160
178
|
# digital assets preferences
|
|
161
179
|
preference :limit_digital_download_count, :boolean, default: true
|
|
162
180
|
preference :limit_digital_download_days, :boolean, default: true
|
|
@@ -186,7 +204,8 @@ module Spree
|
|
|
186
204
|
:pricing_provider, :inventory_provider, :pricing_provider_failure_policy,
|
|
187
205
|
:inventory_provider_failure_policy, :payout_provider, :default_payouts_schedule_interval,
|
|
188
206
|
:default_minimum_payout_amount, :auto_approve_sellers, :auto_approve_seller_products,
|
|
189
|
-
:
|
|
207
|
+
:seller_digital_assets_enabled, :seller_digital_asset_content_types,
|
|
208
|
+
:review_seller_digital_asset_changes, :send_seller_transactional_emails, :default_commission_tax_rate, :document_number_format,
|
|
190
209
|
:order_number_prefix, :order_number_suffix, :order_number_sequence_start,
|
|
191
210
|
:limit_digital_download_count, :digital_asset_authorized_clicks,
|
|
192
211
|
:limit_digital_download_days, :digital_asset_authorized_days,
|
|
@@ -729,7 +748,9 @@ module Spree
|
|
|
729
748
|
# Products without a profile fall back to this one, so it must exist
|
|
730
749
|
# from the store's first moment.
|
|
731
750
|
def create_default_delivery_profile
|
|
732
|
-
Spree::
|
|
751
|
+
Spree::Events.disable do
|
|
752
|
+
Spree::DeliveryProfiles::Shipping.create!(store: self, name: 'General', default: true)
|
|
753
|
+
end
|
|
733
754
|
end
|
|
734
755
|
|
|
735
756
|
def create_default_policies
|
|
@@ -831,5 +852,13 @@ module Spree
|
|
|
831
852
|
|
|
832
853
|
errors.add(:preferred_order_number_sequence_start, :locked_after_first_number)
|
|
833
854
|
end
|
|
855
|
+
|
|
856
|
+
def seller_digital_asset_content_types_are_media_types
|
|
857
|
+
bad = Array(preferred_seller_digital_asset_content_types).reject { |type| type.to_s.match?(MEDIA_TYPE_FORMAT) }
|
|
858
|
+
return if bad.empty?
|
|
859
|
+
|
|
860
|
+
errors.add(:preferred_seller_digital_asset_content_types, :invalid,
|
|
861
|
+
message: I18n.t('spree.digital_assets_content_type_list_invalid', types: bad.join(', ')))
|
|
862
|
+
end
|
|
834
863
|
end
|
|
835
864
|
end
|
|
@@ -3,6 +3,7 @@ module Spree
|
|
|
3
3
|
has_prefix_id :credit
|
|
4
4
|
|
|
5
5
|
include Spree::SingleStoreResource
|
|
6
|
+
include Spree::ActedBy
|
|
6
7
|
include Spree::HasCustomFields
|
|
7
8
|
include Spree::Metadata
|
|
8
9
|
include Spree::CanonicalDecimals
|
|
@@ -25,7 +26,7 @@ module Spree
|
|
|
25
26
|
# to attach it to.
|
|
26
27
|
belongs_to :customer, class_name: "::#{Spree.customer_class}", optional: true
|
|
27
28
|
include Spree::DeprecatedCustomerAlias
|
|
28
|
-
|
|
29
|
+
acted_by :created_by
|
|
29
30
|
belongs_to :originator, polymorphic: true, optional: true
|
|
30
31
|
belongs_to :refunded_order, class_name: 'Spree::Order', optional: true,
|
|
31
32
|
inverse_of: :store_credit_refunds
|
|
@@ -40,8 +41,6 @@ module Spree
|
|
|
40
41
|
validate :amount_used_less_than_or_equal_to_amount
|
|
41
42
|
validate :amount_authorized_less_than_or_equal_to_amount
|
|
42
43
|
|
|
43
|
-
delegate :email, to: :created_by, prefix: true, allow_nil: true
|
|
44
|
-
|
|
45
44
|
# Redemption order: the oldest credit is spent first.
|
|
46
45
|
scope :oldest_first, -> { order(:created_at, :id) }
|
|
47
46
|
# @deprecated Removed in Spree 6.1 — store credit types are gone; use {.oldest_first}.
|
|
@@ -110,7 +109,7 @@ module Spree
|
|
|
110
109
|
|
|
111
110
|
self.whitelisted_ransackable_attributes = %w[customer_id created_by_id amount currency memo]
|
|
112
111
|
search_by :memo
|
|
113
|
-
self.whitelisted_ransackable_associations = %w[customer
|
|
112
|
+
self.whitelisted_ransackable_associations = %w[customer]
|
|
114
113
|
self.whitelisted_ransackable_scopes = %w[outstanding from_gift_card]
|
|
115
114
|
self.ransackable_scope_types = %w[outstanding from_gift_card].index_with('boolean')
|
|
116
115
|
# Staff notes and who issued the credit are back-office data.
|
|
@@ -283,6 +282,7 @@ module Spree
|
|
|
283
282
|
amount: amount,
|
|
284
283
|
customer_id: customer_id,
|
|
285
284
|
created_by_id: created_by_id,
|
|
285
|
+
created_by_type: created_by_type,
|
|
286
286
|
currency: currency,
|
|
287
287
|
memo: credit_allocation_memo,
|
|
288
288
|
store: store
|
|
@@ -22,10 +22,11 @@ module Spree
|
|
|
22
22
|
# core doesn't know about registers its reason here. The bar for a new
|
|
23
23
|
# value: it distinguishes a treatment that reporting or invoicing has to
|
|
24
24
|
# tell apart, not merely a different amount.
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
25
|
+
BUILT_IN_TAXABILITY_REASONS = %w[standard_rated reduced_rated zero_rated reverse_charge
|
|
26
|
+
intra_community_supply export customer_exempt product_exempt
|
|
27
|
+
not_collecting not_subject_to_tax].freeze
|
|
28
|
+
|
|
29
|
+
class_attribute :taxability_reasons, default: BUILT_IN_TAXABILITY_REASONS.dup
|
|
29
30
|
|
|
30
31
|
# Provider payloads (jurisdiction breakdowns, external ids) read nil-safe.
|
|
31
32
|
attribute :data, default: -> { {} }
|
data/app/models/spree/variant.rb
CHANGED
|
@@ -393,7 +393,11 @@ module Spree
|
|
|
393
393
|
@tax_category ||= if self[:tax_category_id].nil?
|
|
394
394
|
product.tax_category
|
|
395
395
|
else
|
|
396
|
-
|
|
396
|
+
# The association, so a page of variants loads its
|
|
397
|
+
# categories together; one deleted since it was
|
|
398
|
+
# assigned no longer counts.
|
|
399
|
+
own = super
|
|
400
|
+
own.nil? || own.destroyed? || own.try(:deleted?) ? product.tax_category : own
|
|
397
401
|
end
|
|
398
402
|
end
|
|
399
403
|
|
|
@@ -466,7 +470,7 @@ module Spree
|
|
|
466
470
|
# Returns the options text of the variant.
|
|
467
471
|
# @return [String] the options text of the variant
|
|
468
472
|
def options_text
|
|
469
|
-
@options_text ||= if option_values
|
|
473
|
+
@options_text ||= if read_association_in_memory?(:option_values)
|
|
470
474
|
option_values.sort_by do |ov|
|
|
471
475
|
ov.option_type.position
|
|
472
476
|
end.map { |ov| "#{ov.option_type.label}: #{ov.label}" }.to_sentence(words_connector: ', ', two_words_connector: ', ')
|
|
@@ -663,7 +667,7 @@ module Spree
|
|
|
663
667
|
def price_in(currency)
|
|
664
668
|
currency = currency&.upcase
|
|
665
669
|
|
|
666
|
-
price = if read_association_in_memory?(:prices)
|
|
670
|
+
price = if read_association_in_memory?(:prices)
|
|
667
671
|
prices.detect { |p| p.currency == currency && p.price_list_id.nil? }
|
|
668
672
|
else
|
|
669
673
|
prices.base_prices.find_by(currency: currency)
|
|
@@ -864,7 +868,12 @@ module Spree
|
|
|
864
868
|
# @return [void]
|
|
865
869
|
def set_stock(count_on_hand, backorderable = nil, stock_location = nil)
|
|
866
870
|
stock_location ||= default_stock_location
|
|
867
|
-
stock_level = stock_levels.
|
|
871
|
+
stock_level = if stock_levels.loaded?
|
|
872
|
+
stock_levels.detect { |level| level.stock_location_id == stock_location.id } ||
|
|
873
|
+
stock_levels.build(stock_location: stock_location)
|
|
874
|
+
else
|
|
875
|
+
stock_levels.find_or_initialize_by(stock_location: stock_location)
|
|
876
|
+
end
|
|
868
877
|
# `unless nil?`, not `if present?` — a caller passing false means it.
|
|
869
878
|
stock_level.backorderable = backorderable unless backorderable.nil?
|
|
870
879
|
|
|
@@ -880,7 +889,10 @@ module Spree
|
|
|
880
889
|
delta = count_on_hand.to_i - stock_level.count_on_hand.to_i
|
|
881
890
|
return if delta.zero?
|
|
882
891
|
|
|
883
|
-
|
|
892
|
+
# Through the level already in hand rather than StockLocation#adjust,
|
|
893
|
+
# which would look the same row up again.
|
|
894
|
+
stock_level.stock_movements.create!(quantity: delta, kind: 'adjusted',
|
|
895
|
+
reason: Spree::StockMovement.default_adjustment_reason)
|
|
884
896
|
end
|
|
885
897
|
|
|
886
898
|
def default_stock_location
|
|
@@ -967,6 +979,11 @@ module Spree
|
|
|
967
979
|
# Variants carry no store of their own; the product owns it.
|
|
968
980
|
# @return [Spree::Store, nil]
|
|
969
981
|
def preference_store
|
|
982
|
+
# The request's store instance when it is this one, so a page of
|
|
983
|
+
# variants reads its settings from one record rather than a copy each.
|
|
984
|
+
current_store = Spree::Current.store
|
|
985
|
+
return current_store if current_store && product&.store_id == current_store.id
|
|
986
|
+
|
|
970
987
|
product&.store
|
|
971
988
|
end
|
|
972
989
|
|
|
@@ -1188,7 +1205,7 @@ module Spree
|
|
|
1188
1205
|
end
|
|
1189
1206
|
|
|
1190
1207
|
def set_cost_currency
|
|
1191
|
-
self.cost_currency = Spree::Store.default.default_currency if cost_currency.blank?
|
|
1208
|
+
self.cost_currency = (product&.store || Spree::Store.default).default_currency if cost_currency.blank?
|
|
1192
1209
|
end
|
|
1193
1210
|
|
|
1194
1211
|
# Only the product's own store's locations — a new variant must not grow
|