mcpField
S
spree-inventory

spree-inventory

github.com/spree开发10104 次安装4 周前更新
代码仓库

关于 spree-inventory

面向 Spree 开发者和运营人员的库存指南,介绍如何按变体和地点管理库存。内容涵盖库存变动台账、结账预留、缺货销售、调拨、采购及 Admin API 工作流。

主要功能

  1. 01按变体和地点跟踪库存
  2. 02记录不可变库存变动
  3. 03结账期间预留库存
  4. 04处理缺货销售与履约
  5. 05管理调拨和采购订单
  6. 06通过 Admin API 批量更新库存

适用场景

  1. 01通过 ERP 或 WMS 数据同步库存
  2. 02排查库存变动记录
  3. 03为结账购物车预留商品
  4. 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非活动地点不会用于新的分配
kindwarehouse、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.md
  • node_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。

评论

支持 Markdown