spree_cm_commissioner 2.12.2 → 2.12.3
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/Gemfile.lock +1 -1
- data/app/controllers/spree/admin/inventory_lock_movements_controller.rb +37 -0
- data/app/controllers/spree/admin/inventory_locks_controller.rb +4 -1
- data/app/controllers/spree/admin/private_sales_controller.rb +2 -0
- data/app/helpers/spree_cm_commissioner/admin/inventory_locks_helper.rb +33 -0
- data/app/jobs/spree_cm_commissioner/inventory_holds/bulk_release_stale_job.rb +1 -1
- data/app/jobs/spree_cm_commissioner/inventory_holds/bulk_release_stale_payment_locked_job.rb +1 -1
- data/app/jobs/spree_cm_commissioner/inventory_holds/release_job.rb +1 -1
- data/app/jobs/spree_cm_commissioner/inventory_items/bulk_adjust_quantities_by_variant_job.rb +2 -0
- data/app/jobs/spree_cm_commissioner/inventory_items/bulk_adjust_quantities_job.rb +2 -0
- data/app/jobs/spree_cm_commissioner/inventory_items/bulk_adjust_quantities_on_hold_job.rb +1 -1
- data/app/models/spree_cm_commissioner/inventory_item.rb +7 -2
- data/app/models/spree_cm_commissioner/inventory_lock_movement.rb +20 -0
- data/app/overrides/spree/admin/shared/_product_tabs/clock_history.html.erb.deface +8 -0
- data/app/services/spree_cm_commissioner/events/publish_stock_statuses_to_firestore.rb +13 -11
- data/app/services/spree_cm_commissioner/inventory_items/adjust_available_quantity.rb +16 -11
- data/app/services/spree_cm_commissioner/inventory_items/bulk_adjust_quantities.rb +17 -13
- data/app/services/spree_cm_commissioner/inventory_items/bulk_adjust_quantities_on_hold.rb +17 -13
- data/app/services/spree_cm_commissioner/inventory_items/move_to_locked.rb +16 -3
- data/app/views/spree/admin/inventory_lock_movements/index.html.erb +43 -0
- data/app/views/spree/admin/inventory_locks/index.html.erb +6 -2
- data/app/views/spree/admin/private_sales/_form.html.erb +1 -0
- data/app/views/spree/admin/private_sales/_stock.html.erb +50 -30
- data/app/views/spree/admin/shared/_availability_percentage.html.erb +18 -0
- data/config/locales/en.yml +15 -4
- data/config/locales/km.yml +0 -3
- data/config/routes.rb +2 -0
- data/db/migrate/20260831080000_create_cm_inventory_lock_movements.rb +16 -0
- data/lib/spree_cm_commissioner/version.rb +1 -1
- metadata +9 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: b375a606aa20cb41edb29f05952ed603df382741dc452a218c55d63c2017e867
|
|
4
|
+
data.tar.gz: 4752e598b58b115740b2205a50517fabb95617e2cea760f1e7a8e9501823033b
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: c2c18a4eed93d148dc07d07726d1575a5ebb44b5e743f991d452d1ea2754f5772ea20f6c909f729e02907c5d4e5ed8adc4560ae4afee057f38a351bbe87afa91
|
|
7
|
+
data.tar.gz: 64e3a068061c742b7684adddab10b3984094f5f77ef8a3ba9fbbe9978ba2d34c7ceb7b68c3bde121da8bde8ed4a4f80873d125813df01627cb50ac38d5045953
|
data/Gemfile.lock
CHANGED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
module Spree
|
|
2
|
+
module Admin
|
|
3
|
+
# Read-only history of InventoryItems::MoveToLocked for one product — kept on its own page,
|
|
4
|
+
# separate from InventoryLocksController, so changing a lock and auditing past changes don't
|
|
5
|
+
# compete for space on one screen.
|
|
6
|
+
class InventoryLockMovementsController < Spree::Admin::ResourceController
|
|
7
|
+
skip_before_action :load_resource
|
|
8
|
+
|
|
9
|
+
before_action :load_parent
|
|
10
|
+
|
|
11
|
+
# GET /admin/products/:product_id/inventory_lock_movements
|
|
12
|
+
def index
|
|
13
|
+
# Not scoped to `active` inventory items — a movement stays visible after its date has
|
|
14
|
+
# passed, since the question "who locked this and when" doesn't expire with the stock.
|
|
15
|
+
@lock_movements = SpreeCmCommissioner::InventoryLockMovement
|
|
16
|
+
.where(inventory_item: @product.inventory_items)
|
|
17
|
+
.includes(:created_by, inventory_item: { variant: { option_values: :option_type } })
|
|
18
|
+
.order(created_at: :desc)
|
|
19
|
+
.page(params[:page]).per(50)
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
private
|
|
23
|
+
|
|
24
|
+
def load_parent
|
|
25
|
+
@product = Spree::Product.find_by(slug: params[:product_id])
|
|
26
|
+
return if @product
|
|
27
|
+
|
|
28
|
+
flash[:error] = t('spree.admin.inventory_locks.create.product_not_found')
|
|
29
|
+
redirect_to spree.admin_products_path
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def model_class
|
|
33
|
+
SpreeCmCommissioner::InventoryLockMovement
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
|
@@ -11,6 +11,8 @@ module Spree
|
|
|
11
11
|
# product, calls it, and turns its named failures into flash messages.
|
|
12
12
|
#
|
|
13
13
|
class InventoryLocksController < Spree::Admin::ResourceController
|
|
14
|
+
helper SpreeCmCommissioner::Admin::InventoryLocksHelper
|
|
15
|
+
|
|
14
16
|
skip_before_action :load_resource
|
|
15
17
|
|
|
16
18
|
before_action :load_parent
|
|
@@ -71,7 +73,8 @@ module Spree
|
|
|
71
73
|
SpreeCmCommissioner::InventoryItems::MoveToLocked.call(
|
|
72
74
|
inventory_item: inventory_item,
|
|
73
75
|
quantity: params[:quantity],
|
|
74
|
-
caller_source: "#{self.class.name}#create"
|
|
76
|
+
caller_source: "#{self.class.name}#create",
|
|
77
|
+
user: try_spree_current_user
|
|
75
78
|
)
|
|
76
79
|
end
|
|
77
80
|
|
|
@@ -8,6 +8,8 @@ module Spree
|
|
|
8
8
|
# allotment is actually set up now that the products are known.
|
|
9
9
|
#
|
|
10
10
|
class PrivateSalesController < Spree::Admin::ResourceController
|
|
11
|
+
helper SpreeCmCommissioner::Admin::InventoryLocksHelper
|
|
12
|
+
|
|
11
13
|
# Also on update: ResourceController re-renders :edit when validation fails, and the stock
|
|
12
14
|
# partial reads these ivars — without this a rejected save is a 500 instead of a form.
|
|
13
15
|
before_action :load_stock_rows, only: %i[edit update]
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
module SpreeCmCommissioner
|
|
2
|
+
module Admin
|
|
3
|
+
module InventoryLocksHelper
|
|
4
|
+
# Share of this date's capacity still on public sale.
|
|
5
|
+
#
|
|
6
|
+
# max_capacity is the right denominator because MoveToLocked only ever moves units BETWEEN
|
|
7
|
+
# quantity_available and quantity_locked — it never touches max_capacity — so this figure falls
|
|
8
|
+
# by exactly what an operator withholds.
|
|
9
|
+
#
|
|
10
|
+
# nil when there is nothing to divide by, so the caller renders a dash instead of a 0% that
|
|
11
|
+
# would read as "sold out" on a date that simply has no stock configured.
|
|
12
|
+
def available_capacity_percentage(inventory_item)
|
|
13
|
+
return nil if inventory_item.nil?
|
|
14
|
+
|
|
15
|
+
capacity = inventory_item.max_capacity.to_i
|
|
16
|
+
return nil if capacity.zero?
|
|
17
|
+
|
|
18
|
+
(inventory_item.quantity_available.to_f / capacity * 100).round
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
# High/medium/low as risk of running out of public stock, not as a grade — a bare number in a
|
|
22
|
+
# dense table doesn't say whether 22% is fine or a problem, so the color carries that instead.
|
|
23
|
+
# Reuses the badge-success/warning/danger classes already styled for this admin (see
|
|
24
|
+
# gems/spree_backend's _badges.scss) rather than introducing new color tokens.
|
|
25
|
+
def availability_percentage_badge_class(percentage)
|
|
26
|
+
return 'badge-danger' if percentage <= 20
|
|
27
|
+
return 'badge-warning' if percentage <= 50
|
|
28
|
+
|
|
29
|
+
'badge-success'
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
data/app/jobs/spree_cm_commissioner/inventory_holds/bulk_release_stale_payment_locked_job.rb
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
module SpreeCmCommissioner
|
|
2
2
|
module InventoryHolds
|
|
3
3
|
class BulkReleaseStalePaymentLockedJob < SpreeCmCommissioner::ApplicationJob
|
|
4
|
-
queue_as :
|
|
4
|
+
queue_as :inventory_item
|
|
5
5
|
|
|
6
6
|
def perform
|
|
7
7
|
SpreeCmCommissioner::InventoryHolds::BulkReleaseStalePaymentLocked.call!
|
|
@@ -3,7 +3,7 @@ module SpreeCmCommissioner
|
|
|
3
3
|
# Primary release for :active holds. Scheduled at hold creation (wait_until: expires_at).
|
|
4
4
|
# Safety net: BulkReleaseStaleJob catches any active holds this job misses.
|
|
5
5
|
class ReleaseJob < SpreeCmCommissioner::ApplicationJob
|
|
6
|
-
queue_as :
|
|
6
|
+
queue_as :inventory_item
|
|
7
7
|
|
|
8
8
|
def perform(options = {})
|
|
9
9
|
hold = SpreeCmCommissioner::InventoryHold.find(options[:hold_id])
|
|
@@ -3,6 +3,8 @@ module SpreeCmCommissioner
|
|
|
3
3
|
class BulkAdjustQuantitiesJob < ApplicationUniqueJob
|
|
4
4
|
include SpreeCmCommissioner::IdempotentJob
|
|
5
5
|
|
|
6
|
+
queue_as :inventory_item
|
|
7
|
+
|
|
6
8
|
def perform(options = {})
|
|
7
9
|
with_idempotency(idempotency_key_for(options)) do
|
|
8
10
|
bulk_adjust_quantities!(options)
|
|
@@ -20,6 +20,8 @@ module SpreeCmCommissioner
|
|
|
20
20
|
# not re-query a already-loaded association.
|
|
21
21
|
has_many :locked_reserved_blocks, -> { locked }, class_name: 'SpreeCmCommissioner::ReservedBlock', inverse_of: :inventory_item
|
|
22
22
|
|
|
23
|
+
has_many :lock_movements, class_name: 'SpreeCmCommissioner::InventoryLockMovement', inverse_of: :inventory_item
|
|
24
|
+
|
|
23
25
|
# Validation
|
|
24
26
|
validates :quantity_available, numericality: { greater_than_or_equal_to: 0 }
|
|
25
27
|
validates :quantity_on_hold, numericality: { greater_than_or_equal_to: 0 }
|
|
@@ -120,8 +122,11 @@ module SpreeCmCommissioner
|
|
|
120
122
|
self.quantity_available = quantity_available + quantity
|
|
121
123
|
save!
|
|
122
124
|
|
|
123
|
-
#
|
|
124
|
-
#
|
|
125
|
+
# Deliberately still inside with_lock, unlike the checkout-path deductors (e.g.
|
|
126
|
+
# InventoryItems::BulkAdjustQuantities): this is a rare admin-only edit, not a
|
|
127
|
+
# high-contention hot path, so keeping it in the same transaction as `save!` means a
|
|
128
|
+
# Redis failure rolls the DB change back too, instead of leaving the DB and Redis counts
|
|
129
|
+
# out of sync with nothing to reconcile them.
|
|
125
130
|
adjust_quantity_in_redis(quantity)
|
|
126
131
|
end
|
|
127
132
|
end
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
module SpreeCmCommissioner
|
|
2
|
+
# An immutable ledger row for one InventoryItems::MoveToLocked call — the audit trail that pool
|
|
3
|
+
# transfer otherwise leaves no trace of. See docs/concepts/locked-stock-flow.md.
|
|
4
|
+
class InventoryLockMovement < Base
|
|
5
|
+
belongs_to :inventory_item, class_name: 'SpreeCmCommissioner::InventoryItem'
|
|
6
|
+
belongs_to :created_by, class_name: 'Spree::User', optional: true
|
|
7
|
+
|
|
8
|
+
validates :quantity, numericality: { other_than: 0 }
|
|
9
|
+
|
|
10
|
+
# Matches Spree::StockMovement's own immutability guard — an audit ledger that can be edited
|
|
11
|
+
# after the fact isn't an audit ledger.
|
|
12
|
+
def readonly?
|
|
13
|
+
persisted?
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def locked?
|
|
17
|
+
quantity.positive?
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
<!-- insert_after "erb[silent]:contains('Spree::Digital')" -->
|
|
2
|
+
|
|
3
|
+
<%= content_tag :li, class: 'nav-item' do %>
|
|
4
|
+
<%= link_to_with_icon 'clock-history.svg',
|
|
5
|
+
t('spree.admin.inventory_lock_movements.tab'),
|
|
6
|
+
admin_product_inventory_lock_movements_url(@product),
|
|
7
|
+
class: "nav-link #{'active' if current == :inventory_lock_movements}" %>
|
|
8
|
+
<% end %>
|
|
@@ -21,8 +21,10 @@ module SpreeCmCommissioner
|
|
|
21
21
|
|
|
22
22
|
# The complete set of stock-status buckets this service can publish — listed in the order
|
|
23
23
|
# `bucket_stock_status` checks them (most-depleted first), so the full state space is visible
|
|
24
|
-
# here without reading every branch.
|
|
25
|
-
#
|
|
24
|
+
# here without reading every branch. SOLD_OUT and PENDING_HOLD have a translation at
|
|
25
|
+
# config/locales/*.yml#stock_status; IN_STOCK and LOW_STOCK intentionally have none — the
|
|
26
|
+
# low-stock message is disabled for now, so that status still publishes but with a nil
|
|
27
|
+
# message, same as IN_STOCK — see `localized_message`.
|
|
26
28
|
SOLD_OUT = :sold_out
|
|
27
29
|
PENDING_HOLD = :pending_hold
|
|
28
30
|
LOW_STOCK = :low_stock
|
|
@@ -170,9 +172,11 @@ module SpreeCmCommissioner
|
|
|
170
172
|
end
|
|
171
173
|
|
|
172
174
|
# Bucket transitions always count; a same-bucket change only counts while LOW_STOCK (the one
|
|
173
|
-
# bucket whose
|
|
174
|
-
#
|
|
175
|
-
#
|
|
175
|
+
# bucket whose raw `quantity_available`/`quantity_on_hold` fields still move meaningfully
|
|
176
|
+
# within the bucket, even though its `message` is nil like IN_STOCK's — see
|
|
177
|
+
# `localized_message`) AND the quantity actually differs from what's already stored —
|
|
178
|
+
# otherwise a poll tick that finds truly nothing new would still write every live event's
|
|
179
|
+
# every product on every cycle.
|
|
176
180
|
def changed?(existing_entry, status, available, on_hold)
|
|
177
181
|
existing_entry ||= {}
|
|
178
182
|
previous_status = existing_entry['status']&.to_sym
|
|
@@ -196,14 +200,12 @@ module SpreeCmCommissioner
|
|
|
196
200
|
|
|
197
201
|
# Pre-translated, per-locale copy baked in at publish time — see
|
|
198
202
|
# config/locales/*.yml#stock_status. nil for in_stock: the design has no badge for the common
|
|
199
|
-
# case, and omitting the field is what tells the client that directly.
|
|
200
|
-
#
|
|
201
|
-
#
|
|
202
|
-
# exact "N left" pill text is built once here rather than duplicated client-side from the
|
|
203
|
-
# bare `quantity_available` field — the client just displays this string verbatim (see
|
|
203
|
+
# case, and omitting the field is what tells the client that directly. Also nil for low_stock
|
|
204
|
+
# for now — the "N left" pill text is disabled; clients fall back to their own generic
|
|
205
|
+
# "Available" copy when message is nil for that status (see
|
|
204
206
|
# `cm_ticket_availability_card.dart`'s `_StatusDisplay._label`).
|
|
205
207
|
def localized_message(status, available)
|
|
206
|
-
return nil if status
|
|
208
|
+
return nil if [IN_STOCK, LOW_STOCK].include?(status)
|
|
207
209
|
|
|
208
210
|
I18n.available_locales.index_with do |locale|
|
|
209
211
|
I18n.t("stock_status.#{status}", count: available, locale: locale)
|
|
@@ -37,27 +37,32 @@ module SpreeCmCommissioner
|
|
|
37
37
|
# thing RedisStock::Unstock and InventoryHolds::Convert do before their own Lua scripts.
|
|
38
38
|
warm_redis_key!(inventory_item) if warm_redis
|
|
39
39
|
|
|
40
|
+
before = after = nil
|
|
41
|
+
|
|
40
42
|
apply = lambda do
|
|
41
43
|
before = inventory_item.quantity_available
|
|
42
44
|
after = before + delta
|
|
43
45
|
|
|
44
46
|
inventory_item.update!(quantity_available: after)
|
|
45
47
|
inventory_item.adjust_quantity_in_redis(delta)
|
|
46
|
-
|
|
47
|
-
CmAppLogger.log(
|
|
48
|
-
label: "#{self.class.name}#call",
|
|
49
|
-
data: {
|
|
50
|
-
caller_source: caller_source,
|
|
51
|
-
inventory_item_id: inventory_item.id,
|
|
52
|
-
quantity_available_before: before,
|
|
53
|
-
delta: delta,
|
|
54
|
-
quantity_available_after: after
|
|
55
|
-
}
|
|
56
|
-
)
|
|
57
48
|
end
|
|
58
49
|
|
|
59
50
|
lock ? inventory_item.with_lock(&apply) : apply.call
|
|
60
51
|
|
|
52
|
+
# Logged after the lock releases: I/O here would otherwise extend how long the row's
|
|
53
|
+
# SELECT ... FOR UPDATE is held under high-traffic contention (this also covers
|
|
54
|
+
# Seats::LockBlocks/UnlockBlocks, which call in with lock: false while already holding it).
|
|
55
|
+
CmAppLogger.log(
|
|
56
|
+
label: "#{self.class.name}#call",
|
|
57
|
+
data: {
|
|
58
|
+
caller_source: caller_source,
|
|
59
|
+
inventory_item_id: inventory_item.id,
|
|
60
|
+
quantity_available_before: before,
|
|
61
|
+
delta: delta,
|
|
62
|
+
quantity_available_after: after
|
|
63
|
+
}
|
|
64
|
+
)
|
|
65
|
+
|
|
61
66
|
success(inventory_item)
|
|
62
67
|
rescue ActiveRecord::RecordInvalid => e
|
|
63
68
|
CmAppLogger.error(
|
|
@@ -49,25 +49,29 @@ module SpreeCmCommissioner
|
|
|
49
49
|
# 1. with_lock (Pessimistic): SELECT ... FOR UPDATE prevents concurrent writes
|
|
50
50
|
# 2. lock_version (Optimistic): Catches race conditions if row is modified between
|
|
51
51
|
# SELECT and UPDATE (e.g., admin adjusts stock while job runs)
|
|
52
|
+
before = after = nil
|
|
53
|
+
|
|
52
54
|
inventory_item.with_lock do
|
|
53
55
|
before = inventory_item[column]
|
|
54
56
|
after = before + quantity
|
|
55
57
|
|
|
56
|
-
CmAppLogger.log(
|
|
57
|
-
label: "#{self.class.name}#adjust_quantity_available",
|
|
58
|
-
data: {
|
|
59
|
-
caller_source: caller_source,
|
|
60
|
-
inventory_item_id: inventory_item.id,
|
|
61
|
-
locked: locked,
|
|
62
|
-
column: column,
|
|
63
|
-
quantity_available_before: before,
|
|
64
|
-
delta: quantity,
|
|
65
|
-
quantity_available_after: after
|
|
66
|
-
}
|
|
67
|
-
)
|
|
68
|
-
|
|
69
58
|
inventory_item.update!(column => after)
|
|
70
59
|
end
|
|
60
|
+
|
|
61
|
+
# Logged after the lock releases: I/O here would otherwise extend how long the row's
|
|
62
|
+
# SELECT ... FOR UPDATE is held under high-traffic contention.
|
|
63
|
+
CmAppLogger.log(
|
|
64
|
+
label: "#{self.class.name}#adjust_quantity_available",
|
|
65
|
+
data: {
|
|
66
|
+
caller_source: caller_source,
|
|
67
|
+
inventory_item_id: inventory_item.id,
|
|
68
|
+
locked: locked,
|
|
69
|
+
column: column,
|
|
70
|
+
quantity_available_before: before,
|
|
71
|
+
delta: quantity,
|
|
72
|
+
quantity_available_after: after
|
|
73
|
+
}
|
|
74
|
+
)
|
|
71
75
|
rescue ActiveRecord::RecordInvalid
|
|
72
76
|
CmAppLogger.error(
|
|
73
77
|
label: "#{self.class.name}#adjust_quantity_available failed",
|
|
@@ -55,25 +55,29 @@ module SpreeCmCommissioner
|
|
|
55
55
|
# 1. with_lock (Pessimistic): SELECT ... FOR UPDATE prevents concurrent writes
|
|
56
56
|
# 2. lock_version (Optimistic): Catches race conditions if row is modified between
|
|
57
57
|
# SELECT and UPDATE (e.g., admin adjusts stock while job runs)
|
|
58
|
+
before = after = nil
|
|
59
|
+
|
|
58
60
|
inventory_item.with_lock do
|
|
59
61
|
before = inventory_item[column]
|
|
60
62
|
after = before + quantity
|
|
61
63
|
|
|
62
|
-
CmAppLogger.log(
|
|
63
|
-
label: "#{self.class.name}#adjust_quantity_on_hold",
|
|
64
|
-
data: {
|
|
65
|
-
caller_source: caller_source,
|
|
66
|
-
inventory_item_id: inventory_item.id,
|
|
67
|
-
locked: locked,
|
|
68
|
-
column: column,
|
|
69
|
-
quantity_on_hold_before: before,
|
|
70
|
-
delta: quantity,
|
|
71
|
-
quantity_on_hold_after: after
|
|
72
|
-
}
|
|
73
|
-
)
|
|
74
|
-
|
|
75
64
|
inventory_item.update!(column => after)
|
|
76
65
|
end
|
|
66
|
+
|
|
67
|
+
# Logged after the lock releases: I/O here would otherwise extend how long the row's
|
|
68
|
+
# SELECT ... FOR UPDATE is held under high-traffic contention.
|
|
69
|
+
CmAppLogger.log(
|
|
70
|
+
label: "#{self.class.name}#adjust_quantity_on_hold",
|
|
71
|
+
data: {
|
|
72
|
+
caller_source: caller_source,
|
|
73
|
+
inventory_item_id: inventory_item.id,
|
|
74
|
+
locked: locked,
|
|
75
|
+
column: column,
|
|
76
|
+
quantity_on_hold_before: before,
|
|
77
|
+
delta: quantity,
|
|
78
|
+
quantity_on_hold_after: after
|
|
79
|
+
}
|
|
80
|
+
)
|
|
77
81
|
rescue ActiveRecord::RecordInvalid
|
|
78
82
|
CmAppLogger.error(
|
|
79
83
|
label: "#{self.class.name}#adjust_quantity_on_hold failed",
|
|
@@ -16,7 +16,7 @@ module SpreeCmCommissioner
|
|
|
16
16
|
prepend ::Spree::ServiceModule::Base
|
|
17
17
|
extend SpreeCmCommissioner::ServiceModuleThrowable
|
|
18
18
|
|
|
19
|
-
def call(inventory_item:, quantity:, caller_source: nil)
|
|
19
|
+
def call(inventory_item:, quantity:, caller_source: nil, user: nil)
|
|
20
20
|
quantity = quantity.to_i
|
|
21
21
|
return failure(nil, :invalid_quantity) if quantity.zero?
|
|
22
22
|
|
|
@@ -32,7 +32,7 @@ module SpreeCmCommissioner
|
|
|
32
32
|
# moved. The savepoint makes the rollback real wherever this is called from.
|
|
33
33
|
inventory_item.transaction(requires_new: true) do
|
|
34
34
|
inventory_item.lock!
|
|
35
|
-
outcome = transfer!(inventory_item, quantity, caller_source)
|
|
35
|
+
outcome = transfer!(inventory_item, quantity, caller_source, user)
|
|
36
36
|
raise ActiveRecord::Rollback unless outcome == :moved
|
|
37
37
|
end
|
|
38
38
|
|
|
@@ -45,7 +45,7 @@ module SpreeCmCommissioner
|
|
|
45
45
|
private
|
|
46
46
|
|
|
47
47
|
# Runs inside the locked transaction. Returns the outcome symbol.
|
|
48
|
-
def transfer!(inventory_item, quantity, caller_source)
|
|
48
|
+
def transfer!(inventory_item, quantity, caller_source, user)
|
|
49
49
|
new_locked = inventory_item.quantity_locked + quantity
|
|
50
50
|
return :quantity_exceeds_lock if new_locked.negative?
|
|
51
51
|
|
|
@@ -67,11 +67,24 @@ module SpreeCmCommissioner
|
|
|
67
67
|
|
|
68
68
|
return insufficient_stock_reason(quantity) unless move_stock_in_redis(inventory_item, quantity)
|
|
69
69
|
|
|
70
|
+
# Inside the same savepoint as the row update above, so a later failure in this same call would
|
|
71
|
+
# roll this row back with it — the ledger can never disagree with quantity_locked.
|
|
72
|
+
create_movement!(inventory_item, quantity, caller_source, user)
|
|
70
73
|
log_adjustment(inventory_item, quantity, caller_source)
|
|
71
74
|
|
|
72
75
|
:moved
|
|
73
76
|
end
|
|
74
77
|
|
|
78
|
+
# The one durable record of this transfer — CmAppLogger below is a log line, not a queryable row.
|
|
79
|
+
def create_movement!(inventory_item, quantity, caller_source, user)
|
|
80
|
+
inventory_item.lock_movements.create!(
|
|
81
|
+
quantity: quantity,
|
|
82
|
+
quantity_locked_after: inventory_item.quantity_locked,
|
|
83
|
+
created_by: user,
|
|
84
|
+
caller_source: caller_source
|
|
85
|
+
)
|
|
86
|
+
end
|
|
87
|
+
|
|
75
88
|
# Both pools as Redis currently holds them, SET-NX'd from the row where a key was cold.
|
|
76
89
|
def warm_pools(inventory_item)
|
|
77
90
|
SpreeCmCommissioner::RedisStock::CachedInventoryItemsBuilder.new([inventory_item]).call.first
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
<%# current: :inventory_lock_movements deliberately matches no tab — this is its own page, not a
|
|
2
|
+
sub-view of Stock Managements, so nothing in the tab bar should light up as if it were. %>
|
|
3
|
+
<%= render partial: 'spree/admin/shared/product_tabs', locals: { current: :inventory_lock_movements } %>
|
|
4
|
+
|
|
5
|
+
<h1 class="mb-3"><%= t('.title') %></h1>
|
|
6
|
+
|
|
7
|
+
<div class="bg-white border rounded table-responsive">
|
|
8
|
+
<table class="table">
|
|
9
|
+
<thead class="text-muted">
|
|
10
|
+
<tr>
|
|
11
|
+
<th><%= Spree.t(:variant) %></th>
|
|
12
|
+
<th><%= Spree.t(:date) %></th>
|
|
13
|
+
<th class="text-right"><%= t('.quantity') %></th>
|
|
14
|
+
<th class="text-right"><%= t('.locked') %></th>
|
|
15
|
+
<th><%= t('.by') %></th>
|
|
16
|
+
<th><%= t('.at') %></th>
|
|
17
|
+
</tr>
|
|
18
|
+
</thead>
|
|
19
|
+
<tbody>
|
|
20
|
+
<% if @lock_movements.empty? %>
|
|
21
|
+
<tr>
|
|
22
|
+
<td colspan="6" class="text-center text-muted"><%= t('.no_movements') %></td>
|
|
23
|
+
</tr>
|
|
24
|
+
<% else %>
|
|
25
|
+
<% @lock_movements.each do |movement| %>
|
|
26
|
+
<% item = movement.inventory_item %>
|
|
27
|
+
<tr>
|
|
28
|
+
<td><%= item.variant.sku_and_options_text %></td>
|
|
29
|
+
<td><%= item.inventory_date %></td>
|
|
30
|
+
<td class="text-right <%= movement.locked? ? 'text-success' : 'text-danger' %>">
|
|
31
|
+
<%= movement.locked? ? "+#{movement.quantity}" : movement.quantity %>
|
|
32
|
+
</td>
|
|
33
|
+
<td class="text-right"><%= movement.quantity_locked_after %></td>
|
|
34
|
+
<td><%= movement.created_by&.email || t('.system') %></td>
|
|
35
|
+
<td><%= movement.created_at.strftime('%d %b %H:%M:%S') %></td>
|
|
36
|
+
</tr>
|
|
37
|
+
<% end %>
|
|
38
|
+
<% end %>
|
|
39
|
+
</tbody>
|
|
40
|
+
</table>
|
|
41
|
+
|
|
42
|
+
<%= paginate @lock_movements, theme: 'admin-twitter-bootstrap-4' %>
|
|
43
|
+
</div>
|
|
@@ -34,7 +34,8 @@
|
|
|
34
34
|
<small class="form-text text-muted"><%= t('.quantity_hint') %></small>
|
|
35
35
|
</div>
|
|
36
36
|
|
|
37
|
-
<%= submit_tag t('.apply'), class: 'btn btn-primary'
|
|
37
|
+
<%= submit_tag t('.apply'), class: 'btn btn-primary',
|
|
38
|
+
data: { confirm: @product.permanent_stock? ? t('.apply_confirm_per_date') : t('.apply_confirm') } %>
|
|
38
39
|
<% end %>
|
|
39
40
|
</div>
|
|
40
41
|
</div>
|
|
@@ -47,6 +48,8 @@
|
|
|
47
48
|
<tr>
|
|
48
49
|
<th><%= Spree.t(:variant) %></th>
|
|
49
50
|
<th><%= Spree.t(:date) %></th>
|
|
51
|
+
<%# The denominator behind the "% left" figure in the Available column. %>
|
|
52
|
+
<th class="text-right"><%= t('.max_capacity') %></th>
|
|
50
53
|
<th class="text-right"><%= t('.public_available') %></th>
|
|
51
54
|
<th class="text-right"><%= t('.locked') %></th>
|
|
52
55
|
<%# In an agent's cart right now. Without this column, a refused release looks arbitrary. %>
|
|
@@ -58,7 +61,8 @@
|
|
|
58
61
|
<tr>
|
|
59
62
|
<td><%= item.variant.sku_and_options_text %></td>
|
|
60
63
|
<td><%= item.inventory_date %></td>
|
|
61
|
-
<td class="text-right"><%= item.
|
|
64
|
+
<td class="text-right text-muted"><%= item.max_capacity %></td>
|
|
65
|
+
<td class="text-right"><%= render partial: 'spree/admin/shared/availability_percentage', locals: { inventory_item: item } %></td>
|
|
62
66
|
<td class="text-right"><strong><%= item.quantity_locked %></strong></td>
|
|
63
67
|
<td class="text-right"><%= item.quantity_locked_on_hold %></td>
|
|
64
68
|
</tr>
|
|
@@ -21,6 +21,7 @@
|
|
|
21
21
|
data: {
|
|
22
22
|
autocomplete_url_value: 'products_api_v2',
|
|
23
23
|
autocomplete_multiple_value: true,
|
|
24
|
+
autocomplete_search_query_value: 'name_or_slug_i_cont',
|
|
24
25
|
autocomplete_placeholder_value: t('.products_placeholder')
|
|
25
26
|
} %>
|
|
26
27
|
<small class="form-text text-muted"><%= t('.products_hint') %></small>
|
|
@@ -16,32 +16,51 @@
|
|
|
16
16
|
<% else %>
|
|
17
17
|
<p class="text-muted small"><%= t('.hint') %></p>
|
|
18
18
|
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
19
|
+
<%# One shared table for every product, not one table per product: separate tables each size their
|
|
20
|
+
own columns from their own content, so the same column (e.g. Max capacity) lands at a different
|
|
21
|
+
x-position under every product — misaligned the moment two products' SKUs differ in length. A
|
|
22
|
+
product is a header row inside this table instead of its own card, so every row shares one set
|
|
23
|
+
of column widths. %>
|
|
24
|
+
<div class="bg-white border rounded table-responsive">
|
|
25
|
+
<table class="table mb-0">
|
|
26
|
+
<thead class="text-muted">
|
|
27
|
+
<tr>
|
|
28
|
+
<%# Same wording as the product's Stock screen, which names the columns after the data they
|
|
29
|
+
hold. Two screens showing the same numbers under different names is how an operator ends
|
|
30
|
+
up mistrusting both. %>
|
|
31
|
+
<th><%= Spree.t(:variant) %></th>
|
|
32
|
+
<th class="text-center"><%= t('.max_capacity') %></th>
|
|
33
|
+
<th class="text-center"><%= t('.quantity_available') %></th>
|
|
34
|
+
<th class="text-center"><%= t('.quantity_locked') %></th>
|
|
35
|
+
<th class="text-center"><%= t('.quantity_locked_on_hold') %></th>
|
|
36
|
+
<th class="text-center"><%= t('.lock_release') %></th>
|
|
37
|
+
<th></th>
|
|
38
|
+
</tr>
|
|
39
|
+
</thead>
|
|
40
|
+
<tbody>
|
|
41
|
+
<% @private_sale.product_ids.each do |product_id| %>
|
|
42
|
+
<% product = @products_by_id[product_id] %>
|
|
43
|
+
<% next if product.nil? %>
|
|
22
44
|
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
<th class="text-center"><%= t('.quantity_locked_on_hold') %></th>
|
|
41
|
-
<th class="text-center"><%= t('.lock_release') %></th>
|
|
45
|
+
<tr class="bg-light">
|
|
46
|
+
<td colspan="6"><strong><%= product.name %></strong></td>
|
|
47
|
+
<td class="text-right">
|
|
48
|
+
<%# Own column so these line up under the same x-position for every product, rather than
|
|
49
|
+
sharing space with the name — icon-only, matching the with-tip pattern used for row
|
|
50
|
+
actions elsewhere in this admin (e.g. import_existing_orders#index). Regardless of
|
|
51
|
+
stock type — the per-date link below only appears for permanent-stock products with
|
|
52
|
+
no undated row, but every product's lock/release history lives here. %>
|
|
53
|
+
<%= link_to admin_product_inventory_lock_movements_path(product),
|
|
54
|
+
class: 'btn btn-sm btn-outline-secondary with-tip icon-link', title: t('.lock_history') do %>
|
|
55
|
+
<%= svg_icon name: 'clock-history.svg', width: '14', height: '14' %>
|
|
56
|
+
<% end %>
|
|
57
|
+
<%= link_to admin_product_stock_managements_path(product),
|
|
58
|
+
class: 'btn btn-sm btn-outline-secondary with-tip icon-link', title: t('.manage_product_stock') do %>
|
|
59
|
+
<%= svg_icon name: 'box-seam.svg', width: '14', height: '14' %>
|
|
60
|
+
<% end %>
|
|
61
|
+
</td>
|
|
42
62
|
</tr>
|
|
43
|
-
|
|
44
|
-
<tbody>
|
|
63
|
+
|
|
45
64
|
<% (@variants_by_product[product_id] || []).each do |variant| %>
|
|
46
65
|
<% item = @inventory_item_by_variant[variant.id] %>
|
|
47
66
|
<tr class="<%= 'text-muted' if item.nil? || item.quantity_locked.zero? %>">
|
|
@@ -50,12 +69,12 @@
|
|
|
50
69
|
<% if item.nil? %>
|
|
51
70
|
<%# No undated row means this product's stock is per date — hundreds of rows a single
|
|
52
71
|
input cannot express. Those are managed on the product's own screen. %>
|
|
53
|
-
<td colspan="
|
|
72
|
+
<td colspan="6" class="text-center text-muted">
|
|
54
73
|
<%= link_to t('.per_date_stock'), admin_product_inventory_locks_path(product) %>
|
|
55
74
|
</td>
|
|
56
75
|
<% else %>
|
|
57
76
|
<td class="text-center text-muted"><%= item.max_capacity %></td>
|
|
58
|
-
<td class="text-center"><%= item
|
|
77
|
+
<td class="text-center"><%= render partial: 'spree/admin/shared/availability_percentage', locals: { inventory_item: item } %></td>
|
|
59
78
|
<td class="text-center"><strong><%= item.quantity_locked %></strong></td>
|
|
60
79
|
<td class="text-center"><%= item.quantity_locked_on_hold %></td>
|
|
61
80
|
<td class="text-center">
|
|
@@ -73,11 +92,12 @@
|
|
|
73
92
|
<% end %>
|
|
74
93
|
</div>
|
|
75
94
|
</td>
|
|
95
|
+
<td></td>
|
|
76
96
|
<% end %>
|
|
77
97
|
</tr>
|
|
78
98
|
<% end %>
|
|
79
|
-
|
|
80
|
-
</
|
|
81
|
-
</
|
|
82
|
-
|
|
99
|
+
<% end %>
|
|
100
|
+
</tbody>
|
|
101
|
+
</table>
|
|
102
|
+
</div>
|
|
83
103
|
<% end %>
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
<%# Public quantity available, with a small color-coded badge next to it for what share of capacity
|
|
2
|
+
that represents — red/yellow/green by high/medium/low, so an operator scanning a table of dates
|
|
3
|
+
spots the ones worth a second look without reading every number.
|
|
4
|
+
|
|
5
|
+
Sized down via inline style rather than a new CSS class: Bootstrap's default .badge already reads
|
|
6
|
+
as a chunky block, and this admin already sizes one-off controls this way (e.g. the width on the
|
|
7
|
+
+/- input groups in this same directory).
|
|
8
|
+
|
|
9
|
+
Shared by the product's lock screen and the private sale's stock table so it never renders two
|
|
10
|
+
different ways on two screens an operator compares side by side. %>
|
|
11
|
+
<% percentage = available_capacity_percentage(inventory_item) %>
|
|
12
|
+
|
|
13
|
+
<%= inventory_item.quantity_available %>
|
|
14
|
+
<% if percentage %>
|
|
15
|
+
<span class="badge <%= availability_percentage_badge_class(percentage) %> ml-1" style="font-size: 0.7em; padding: 0.2em 0.45em;">
|
|
16
|
+
<%= percentage %>%
|
|
17
|
+
</span>
|
|
18
|
+
<% end %>
|
data/config/locales/en.yml
CHANGED
|
@@ -597,6 +597,7 @@ en:
|
|
|
597
597
|
quantity_locked_on_hold: "Quantity locked on hold"
|
|
598
598
|
lock_release: "Lock (+) / Release (−)"
|
|
599
599
|
manage_product_stock: "Product stock"
|
|
600
|
+
lock_history: "Lock history"
|
|
600
601
|
per_date_stock: "This product's stock is per date — manage it on the product"
|
|
601
602
|
revoke:
|
|
602
603
|
revoked: "Private sale revoked. Its link no longer works."
|
|
@@ -609,7 +610,10 @@ en:
|
|
|
609
610
|
to_date: "To date"
|
|
610
611
|
per_date_hint: "The quantity applies to EACH date in the range, not across the range as a whole. Leave both dates empty to change only undated stock."
|
|
611
612
|
apply: "Apply"
|
|
612
|
-
|
|
613
|
+
apply_confirm: "Apply this change to locked stock? This takes effect immediately."
|
|
614
|
+
apply_confirm_per_date: "Apply this change to EVERY date in the range? This takes effect immediately."
|
|
615
|
+
max_capacity: "Max capacity"
|
|
616
|
+
public_available: "Available"
|
|
613
617
|
locked: "Locked"
|
|
614
618
|
locked_on_hold: "On hold"
|
|
615
619
|
create:
|
|
@@ -618,6 +622,16 @@ en:
|
|
|
618
622
|
no_inventory_item: "Pick a variant that has stock on sale."
|
|
619
623
|
invalid_date_range: "Check the dates: both are required together, and From must not be after To."
|
|
620
624
|
product_not_found: "That product no longer exists."
|
|
625
|
+
inventory_lock_movements:
|
|
626
|
+
tab: "Lock history"
|
|
627
|
+
index:
|
|
628
|
+
title: "Lock history"
|
|
629
|
+
quantity: "Quantity"
|
|
630
|
+
locked: "Locked"
|
|
631
|
+
no_movements: "No locked stock has been moved yet."
|
|
632
|
+
by: "By"
|
|
633
|
+
at: "At"
|
|
634
|
+
system: "System"
|
|
621
635
|
display_on:
|
|
622
636
|
frontend_for_early_adopter: "Storefront for Early Adopter"
|
|
623
637
|
external_integrations_title: "External Integrations"
|
|
@@ -992,9 +1006,6 @@ en:
|
|
|
992
1006
|
# SpreeCmCommissioner::Products::PublishStockStatusToFirestore) — not read via I18n at request
|
|
993
1007
|
# time, so every locale we publish for must have an entry here.
|
|
994
1008
|
stock_status:
|
|
995
|
-
low_stock:
|
|
996
|
-
one: "%{count} left"
|
|
997
|
-
other: "%{count} left"
|
|
998
1009
|
pending_hold: "In high demand"
|
|
999
1010
|
sold_out: "Sold out"
|
|
1000
1011
|
|
data/config/locales/km.yml
CHANGED
data/config/routes.rb
CHANGED
|
@@ -234,6 +234,8 @@ Spree::Core::Engine.add_routes do
|
|
|
234
234
|
# Withholding stock from public sale, and the private sales that sell it.
|
|
235
235
|
resources :inventory_locks, only: %i[index create]
|
|
236
236
|
|
|
237
|
+
resources :inventory_lock_movements, only: %i[index]
|
|
238
|
+
|
|
237
239
|
resources :product_completion_steps do
|
|
238
240
|
collection do
|
|
239
241
|
post :update_positions
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
class CreateCmInventoryLockMovements < ActiveRecord::Migration[7.0]
|
|
2
|
+
# Immutable audit ledger for SpreeCmCommissioner::InventoryItems::MoveToLocked — one row per lock
|
|
3
|
+
# or release, mirroring Spree::StockMovement's shape (signed quantity + actor + append-only) since
|
|
4
|
+
# that table cannot be reused: its after_create callback adjusts count_on_hand, which locking must
|
|
5
|
+
# never touch.
|
|
6
|
+
def change
|
|
7
|
+
create_table :cm_inventory_lock_movements, if_not_exists: true do |t|
|
|
8
|
+
t.references :inventory_item, null: false, foreign_key: { to_table: :cm_inventory_items }
|
|
9
|
+
t.integer :quantity, null: false
|
|
10
|
+
t.integer :quantity_locked_after, null: false
|
|
11
|
+
t.references :created_by, foreign_key: { to_table: :spree_users }
|
|
12
|
+
t.string :caller_source
|
|
13
|
+
t.datetime :created_at, null: false
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
end
|
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: spree_cm_commissioner
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 2.12.
|
|
4
|
+
version: 2.12.3
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- You
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: bin
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2026-08-
|
|
11
|
+
date: 2026-08-31 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: spree
|
|
@@ -867,6 +867,7 @@ files:
|
|
|
867
867
|
- app/controllers/spree/admin/integrations_controller.rb
|
|
868
868
|
- app/controllers/spree/admin/inventory_holds_controller.rb
|
|
869
869
|
- app/controllers/spree/admin/inventory_items_controller.rb
|
|
870
|
+
- app/controllers/spree/admin/inventory_lock_movements_controller.rb
|
|
870
871
|
- app/controllers/spree/admin/inventory_locks_controller.rb
|
|
871
872
|
- app/controllers/spree/admin/inventory_monitorings_controller.rb
|
|
872
873
|
- app/controllers/spree/admin/kyc_controller.rb
|
|
@@ -1229,6 +1230,7 @@ files:
|
|
|
1229
1230
|
- app/helpers/spree/transit/sortable_tree_helper.rb
|
|
1230
1231
|
- app/helpers/spree_cm_commissioner/admin/guest_helper.rb
|
|
1231
1232
|
- app/helpers/spree_cm_commissioner/admin/homepage_segment_helper.rb
|
|
1233
|
+
- app/helpers/spree_cm_commissioner/admin/inventory_locks_helper.rb
|
|
1232
1234
|
- app/helpers/spree_cm_commissioner/admin/kycable_helper.rb
|
|
1233
1235
|
- app/helpers/spree_cm_commissioner/admin/service_calendars_helper.rb
|
|
1234
1236
|
- app/helpers/spree_cm_commissioner/admin/ticket_transfers_helper.rb
|
|
@@ -1604,6 +1606,7 @@ files:
|
|
|
1604
1606
|
- app/models/spree_cm_commissioner/inventory.rb
|
|
1605
1607
|
- app/models/spree_cm_commissioner/inventory_hold.rb
|
|
1606
1608
|
- app/models/spree_cm_commissioner/inventory_item.rb
|
|
1609
|
+
- app/models/spree_cm_commissioner/inventory_lock_movement.rb
|
|
1607
1610
|
- app/models/spree_cm_commissioner/invite.rb
|
|
1608
1611
|
- app/models/spree_cm_commissioner/invite_guest.rb
|
|
1609
1612
|
- app/models/spree_cm_commissioner/invite_guest_group.rb
|
|
@@ -1881,6 +1884,7 @@ files:
|
|
|
1881
1884
|
- app/overrides/spree/admin/shared/_order_tabs/guests.html.erb.deface
|
|
1882
1885
|
- app/overrides/spree/admin/shared/_order_tabs/notifications.html.erb.deface
|
|
1883
1886
|
- app/overrides/spree/admin/shared/_order_tabs/ticket_transfers.html.erb.deface
|
|
1887
|
+
- app/overrides/spree/admin/shared/_product_tabs/clock_history.html.erb.deface
|
|
1884
1888
|
- app/overrides/spree/admin/shared/_product_tabs/google_wallet_html.erb.deface
|
|
1885
1889
|
- app/overrides/spree/admin/shared/_product_tabs/guest_card.html.erb.deface
|
|
1886
1890
|
- app/overrides/spree/admin/shared/_product_tabs/kyc.html.erb.deface
|
|
@@ -2580,6 +2584,7 @@ files:
|
|
|
2580
2584
|
- app/views/spree/admin/inventory_holds/index.html.erb
|
|
2581
2585
|
- app/views/spree/admin/inventory_items/prices.html.erb
|
|
2582
2586
|
- app/views/spree/admin/inventory_items/stocks.html.erb
|
|
2587
|
+
- app/views/spree/admin/inventory_lock_movements/index.html.erb
|
|
2583
2588
|
- app/views/spree/admin/inventory_locks/index.html.erb
|
|
2584
2589
|
- app/views/spree/admin/inventory_monitorings/index.html.erb
|
|
2585
2590
|
- app/views/spree/admin/kyc/_form.html.erb
|
|
@@ -2644,6 +2649,7 @@ files:
|
|
|
2644
2649
|
- app/views/spree/admin/promotions/rules/_guest_occupations.html.erb
|
|
2645
2650
|
- app/views/spree/admin/promotions/rules/_vendors.html.erb
|
|
2646
2651
|
- app/views/spree/admin/promotions/rules/_weekend.html.erb
|
|
2652
|
+
- app/views/spree/admin/shared/_availability_percentage.html.erb
|
|
2647
2653
|
- app/views/spree/admin/shared/_cms_pages_tabs.html.erb
|
|
2648
2654
|
- app/views/spree/admin/shared/_customer_notification_tabs.html.erb
|
|
2649
2655
|
- app/views/spree/admin/shared/_direct_upload.html.erb
|
|
@@ -3548,6 +3554,7 @@ files:
|
|
|
3548
3554
|
- db/migrate/20260824120000_add_user_id_to_cm_waiting_room_sessions.rb
|
|
3549
3555
|
- db/migrate/20260825092416_add_live_stock_window_to_spree_taxons.rb
|
|
3550
3556
|
- db/migrate/20260828080349_add_normalized_login_to_spree_users.rb
|
|
3557
|
+
- db/migrate/20260831080000_create_cm_inventory_lock_movements.rb
|
|
3551
3558
|
- docker-compose.yml
|
|
3552
3559
|
- docs/api/scoped-access-token-endpoints.md
|
|
3553
3560
|
- docs/option_types/attr_types.md
|