关于 spree-inventory
面向 Spree 开发者和运营人员的库存指南,介绍如何按变体和地点管理库存。内容涵盖库存变动台账、结账预留、缺货销售、调拨、采购及 Admin API 工作流。
主要功能
- 01按变体和地点跟踪库存
- 02记录不可变库存变动
- 03结账期间预留库存
- 04处理缺货销售与履约
- 05管理调拨和采购订单
- 06通过 Admin API 批量更新库存
适用场景
- 01通过 ERP 或 WMS 数据同步库存
- 02排查库存变动记录
- 03为结账购物车预留商品
- 04接收供应商交货或调拨库存
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ spree/ agent-skills --skill spree-inventory安装后在 Claude Code 中输入/spree-inventory确认已加载。
详细介绍
Spree 库存
库存按变体和地点分别跟踪。变体没有单一的库存数量——每个承载该变体的 Spree::StockLocation(sloc_)都有一个 Spree::StockLevel(sl_)。每次变更都会记录为一个注明原因的 Spree::StockMovement(sm_)。
从 5.x 迁移而来(使用 Spree::StockItem,订单完成时扣减库存,Spree::StockMovement 只有一个带符号的数量)?别名 Spree::StockItem = Spree::StockLevel 会保留到 6.1——新代码请使用 StockLevel。参见 spree-upgrade-5-to-6。
计数器
| 列/方法 | 含义 | 写入者 |
|---|---|---|
count_on_hand | 货架上的实物库存 | 仅由 received、adjusted 和 shipped 库存变动写入 |
allocated_count | 已承诺给已下单但尚未发货的订单的数量 | allocated(增加)、released/shipped(减少)库存变动 |
reserved_count | 结账期间被购物车占用的数量 | 创建/更新/销毁预留项(计数器缓存) |
incoming_count | 已标记为已订购的采购订单或运输途中的调拨单中尚未收货的数量 | 采购订单/调拨工作流 |
available_count(方法) | count_on_hand − allocated_count | 派生值 |
purchasable_count(方法) | available_count − reserved_count | 派生值 |
backorderable | 是否允许超出库存销售 | 管理端/API |
下单不会减少 count_on_hand。 Spree::Orders::Complete 会为每个履约分配数量(allocated 库存变动 → allocated_count 增加)。只有履约发货时,库存才会实际出库(Spree::Fulfillments::Fulfill → shipped 库存变动:现货数量减少,分配量结清)。取消订单会释放分配量(released)——包括通过编辑已下单订单而增加的数量,因为编辑会就地调整现有履约项(Spree::OrderInventory),而不是重新创建。超卖会表现为 allocated_count > count_on_hand,货架库存绝不会变成负数。因此,“我卖出 3 件后,网站上仍显示有 10 件现货”是正确的——请查看 available_count。
reserved_count 和 incoming_count 是缓存的计数器。如果它们出现偏差(手动 SQL、任务崩溃),请重新计算——该任务还会清理已过期的预留项,并打印每一条被修正的记录:
spree rake spree:stock:recount_levels # classic: bin/rake spree:stock:recount_levels
(服务:Spree.stock_level_recount_service。)
系统会自动计算面向购物者的可用性:Store API 变体会公开 in_stock 和 purchasable(有库存或允许延期交货),且已扣除分配量和其他购物者的预留量。在 Ruby 中,Spree::Stock::Quantifier.new(variant).can_supply?(qty)/total_on_hand 会执行相同的计算;还可使用 variant.in_stock?、variant.purchasable?、variant.backorderable?。StockLevel 作用域:in_stock、out_of_stock、low_stock(threshold)(存储 preferred_low_stock_threshold,默认值为 5)、with_incoming、with_reserved,以及可用于 ransack 的 with_stock_status。
库存地点
| 属性 | 说明 |
|---|---|
name, code, active, default | 非活动地点不会用于新的分配 |
kind | warehouse、store、fulfillment_center(StockLocation::KINDS,内置选项) |
地址字段(country_code、state_code、city、postal_code、…) | 用于计算费率来源地和距离 |
backorderable_default | 此处新库存级别的默认 backorderable 值 |
propagate_all_variants | 创建时,为此地点所属商店的每个变体创建库存级别(卖家的地点:仅创建该卖家的变体);此后新变体也会在这类地点创建库存级别 |
pickup_enabled | 客户可以在此处提货(提货配送方式) |
returns_enabled | 可在此处接收退货(参见 spree-returns) |
哪些地点可以为产品供货,由其配送配置文件的来源地分组和渠道服务的地点决定(spree-fulfillment),而非仅由库存级别决定。
以编程方式调拨库存——始终通过地点进行,该操作会记录库存变动并在行锁下更新计数器:
location.restock(variant, 10, purchase_order, unit_cost: 12.5) # 'received'
location.adjust(variant, -2, reason: 'damaged') # 'adjusted' — reason required
location.allocate(variant, 1, fulfillment) # 'allocated' (core calls this)
location.release(variant, 1, fulfillment) # 'released' (core calls this)
location.unstock(variant, 1, fulfillment) # 'shipped' (core calls this)
location.stock_level_or_create(variant)
切勿使用 stock_level.update!(count_on_hand: …) 或 update_column——这些操作会跳过库存变动账本(没有审计记录)、缺货订单填充,以及用于清除缓存的变体触碰操作。
库存变动
Spree::StockMovement::KINDS = %w[received allocated shipped released adjusted]。allocated/shipped/released 均以正数写入(方向由类型表示);received/adjusted 保留其正负号;adjusted 必须提供 reason(StockMovement::ADJUSTMENT_REASONS 中的惯用值:manual_adjustment correction count received return_restock damaged theft_or_loss promotion_or_donation inventory_feed,也接受自由文本)。库存变动会关联其成因(履约、退货、调拨、采购订单、库存收货),采购订单到货的库存变动还会携带 unit_cost。库存变动是不可变账本——如需纠正错误,请新增一条 adjusted 库存变动。
结账期间的预留
Spree::StockReservation(res_)是对购物车行项目对应库存级别的短期预留:
| 时间点 | 处理方式 |
|---|---|
| 购物车进入结账流程(填写电子邮件或收货地址) | Spree::StockReservations::Reserve 为每个有库存追踪、不可接受缺货订单且非预售的行项目预留库存;库存不足时失败 |
| 结账期间编辑购物车(添加/更改商品、更新地址) | 重新预留/Extend 将 expires_at 向后延长 |
| 购物车离开结账流程 | Release |
| 购物车完成结账 | Orders::Complete 释放预留,并改为分配履约所需的库存单位 |
| 已放弃 | 预留到期;reserved_count 恢复 |
商店设置:preferred_stock_reservations_enabled(默认值为 true)和 preferred_stock_reservation_ttl_minutes(默认值为 10)。预留不会影响 count_on_hand。
请自行安排到期作业。 Spree 提供 Spree::StockReservations::ExpireJob,但不会自动运行。使用 Solid Queue(默认)时,将其添加到 config/recurring.yml;使用 Sidekiq 时,使用 sidekiq-cron:
# config/recurring.yml (Solid Queue)
production:
expire_stock_reservations:
class: Spree::StockReservations::ExpireJob
schedule: every minute
如果未安排该作业,读取时仍会停止计算已到期的预留,但记录和 reserved_count 会一直保留,直到下次重新计数。
缺货订单
backorderable 库存级别允许在库存降至零后继续销售:订单会被接受,其履约商品项会标记为 backordered。库存到货(count_on_hand 增加)后,该地点的缺货订单商品项会优先获得库存——按订单时间从早到晚处理——剩余库存随后变为可用。若履约包含缺货商品项,Fulfillments::Fulfill 会拒绝履约,除非设置 force: true。结账时不会为允许缺货的行项目预留库存。
启用/停用库存追踪
variant.should_track_inventory? 的值为 variant.track_inventory? && store.preferred_track_inventory_levels。未追踪库存的变体始终可购买,其库存变动不会更改计数器(adjusted 除外)。可将其用于服务、按订单生产的商品和数字产品。
管理 API(权限键 read_stock / write_stock)
GET /api/v3/admin/stock_levels?q[stock_location_id_eq]=sloc_…&q[with_stock_status][]=low_stock
PATCH /api/v3/admin/stock_levels/sl_… { "count_on_hand": 150, "reason": "count" } # absolute
PATCH /api/v3/admin/stock_levels/sl_… { "adjustment": -2, "reason": "damaged" } # relative
POST /api/v3/admin/stock_levels/bulk_upsert { "stock_levels": [ { "variant_id": "variant_…", "stock_location_id": "sloc_…", "count_on_hand": 40, "backorderable": false } ] }
GET /api/v3/admin/stock_movements?q[stock_level_variant_id_eq]=variant_…
GET /api/v3/admin/stock_reservations
/api/v3/admin/stock_locations (CRUD)
count_on_hand 和 adjustment 不能同时使用。ERP/WMS 数据源应使用 bulk_upsert,而不是循环发送 PATCH 请求:它接受绝对值(count_on_hand)或相对值(adjustment)行;对于每个(变体、地点)组合,以最后一次写入为准;会创建缺失的库存级别;每次变更都会记录一条原因是 inventory_feed 的 adjusted 库存移动;并且所有操作都在一个事务中运行——无法读取的行会导致整个批次失败,而不会只应用一部分。发送 "stock_levels": [] 不执行任何操作;省略该键会返回 422。在 Ruby 中:Spree.stock_level_bulk_upsert_service.call(rows: [...])(其中的 ID 是内部整数)以及 Spree.stock_level_correct_service.call(stock_level:, count_on_hand: | adjustment:, reason:)。
TypeScript:adminClient.stockLevels.list/update/bulkUpsert(...)、adminClient.stockMovements.list(...)。
如果库存的真实数据位于外部系统,请持续同步本地快照(使用 bulk upsert)——读取路径仍使用本地数据。如需在决策点(加入购物车、预留、完成)实时检查,库存提供商契约为:Spree::InventoryProvider::Base#stock_levels_for(variant, stock_location:) 返回未保存的 StockLevel 行,在 Spree.inventory_providers 中注册,并通过 store.preferred_inventory_provider(默认值为 'internal')和失败策略选择。参见 spree-providers。
库存调拨(调拨自有库存)
Spree::StockTransfer(st_)——状态为 draft → ready_to_ship → in_transit → partially_received / received / over_received,或 canceled。商品项为 Spree::StockTransferItem(sti_,quantity_shipped)。
- 单位离开来源地点(产生一条由调拨引起的
shipped库存移动),并且在标记为运输中时计入目的地的incoming,而非在创建草稿时计入。 - 只有库存收货将这些单位计入后,目的地的可用库存才会增加。
- 发货后取消需要设置
on_in_transit: 'restock'(退回来源地点)或'write_off'。 close用于结算短缺交付(closed_short_at+ 原因);不会发生库存移动。
工作流(Spree.stock_transfer_*_workflow):Create、Update、MarkReady、MarkInTransit(钩子 validate、before_unstock、after_mark_in_transit;force:)、Receive(validate、before_restock、after_receive)、Cancel、MarkDraft、Close。
API:/api/v3/admin/stock_transfers(CRUD;创建时传入 source_location_id、destination_location_id、items: [{ variant_id, quantity_shipped }])+ PATCH mark_ready | mark_in_transit | mark_draft | close | cancel,以及 POST …/stock_transfers/:id/stock_receipts。
采购订单和供应商(采购库存)
Spree::Supplier(sup_)是按店铺区分的地址簿。Spree::PurchaseOrder(po_)——状态为 draft → ordered → partially_received / received / over_received,或 canceled;商品项为 Spree::PurchaseOrderItem(poi_,quantity_ordered、unit_cost);expected_at、cancel_by(日历日期;不会自动取消)。标记为已下单会增加 incoming_count;已下单的库存在收货前始终不可用。收货的单位会生成携带 unit_cost 的 received 库存移动。支持采购订单的 CSV 导出/导入。
工作流(Spree.purchase_order_*_workflow):Create、Update、MarkOrdered、Receive(validate、before_restock、after_receive)、Cancel、MarkDraft、Close——每个工作流都有一个 validate 钩子,例如,可使用 purchase_orders.mark_ordered.validate 设置审批限额。
API(权限键 read_purchasing / write_purchasing——有意与 write_stock 分开):/api/v3/admin/suppliers(CRUD)、/api/v3/admin/purchase_orders(CRUD;创建时传入 supplier_id、destination_location_id、expected_at、cancel_by、items: [{ variant_id, quantity_ordered, unit_cost }])+ PATCH mark_ordered | mark_draft | close | cancel,以及 POST …/purchase_orders/:id/stock_receipts。
库存收货
两种单据的收货方式相同:每次实物交付对应一个 Spree::StockReceipt(sr_),其中包含 reference(装箱单)和逐行记录的 Spree::StockReceiptItem(sri_)数量:
{ "reference": "DN-4471",
"items": [{ "id": "poi_…", "quantity_accepted": 58, "quantity_rejected": 2, "rejection_reason": "damaged" }] }
id 是采购订单/调拨单行(poi_… / sti_…)。数量是本次交付的数量,而非累计总数。省略 items 即表示完整收货所有未完成的数量。rejection_reason 可选值:damaged、wrong_item、expired、other。被拒收的采购订单单位仍由供应商负责交付;被拒收的调拨单位则视为已“到达”(运输已结束)。
事件
stock_level.created/.updated/.deleted(旧版 stock_item.* 名称会双重发送,直到 6.1)、stock_movement.*、stock_reservation.*、stock_transfer.*(以及 draft、ready_to_ship、shipped、partially_received、received、over_received、canceled)、purchase_order.*(以及 draft、ordered、partially_received、received、over_received、canceled)、stock_receipt.*、supplier.*。订阅 stock_level.updated 通常用于发送有货通知,或将数量推送到市场渠道——请根据载荷/记录计算可用量,并注意,仅计数器变化的写入(分配/预留)也会触及库存水平。
注意事项
- 所有操作都要限定到店铺范围:使用
Spree::StockLevel.for_store(store)、store.stock_locations——在多店铺应用中,切勿不加范围地使用Spree::StockLevel.all。 - 不要直接修改计数器(
update_column(:count_on_hand, …)、increment!(:reserved_count));应使用restock/adjust/服务或 API。如果已直接修改,请运行spree:stock:recount_levels(它只修复预留/在途数量;现有库存/已分配数量由台账负责,需通过adjust修复)。 - 管理列表中的“有货”和
StockLevel.in_stock会扣除预留数量;StockLevel#in_stock?/available_count则不会。请根据实际问题选择对应的数值。 - 作业、rake 任务和控制台脚本必须设置
Spree::Current.store——Spree::Store.default可能为 nil,且店铺偏好设置(预留 TTL、库存跟踪、低库存阈值)会通过它读取。 - 自定义库存拆分器应在
Rails.application.config.after_initialize中注册(核心代码会在此处重新赋值Spree.stock_splitters)——参见 spree-fulfillment。
测试
工厂::stock_location、:stock_location_with_items、:stock_level、:stock_movement、:stock_reservation、:stock_transfer、:stock_transfer_item、:purchase_order、:purchase_order_item、:stock_receipt、:stock_receipt_item、:supplier。在规格测试中,应使用 location.stock_level_or_create(variant).set_count_on_hand(10) 或 location.restock(variant, 10) 设置库存,而不是直接写入列,以保持计数器和台账一致。
延伸阅读
node_modules/@spree/docs/dist/developer/core-concepts/inventory.mdnode_modules/@spree/docs/dist/developer/deployment/background_jobs.md(调度ExpireJob)node_modules/@spree/docs/dist/user/inventory/stock-levels.md、stock-transfers.md、purchase-orders.md、suppliers.md- 相关技能:spree-fulfillment(分配 → 发货、路由、拆分器)、spree-returns(补回库存)、spree-checkout(购物车进入结账时)、spree-providers、spree-events-webhooks、spree-performance。