spree-inventory について
Spree でバリアントとロケーションごとの在庫を管理する開発者や運用担当者向けのガイドです。在庫移動台帳、チェックアウト時の予約、バックオーダー、移動、購買、Admin API のワークフローを説明します。
主な機能
- 01バリアントとロケーション別の在庫管理
- 02不変の在庫移動記録
- 03チェックアウト中の在庫予約
- 04バックオーダーとフルフィルメント対応
- 05移動と発注書の管理
- 06Admin API による在庫一括更新
活用シーン
- 01ERP または WMS の在庫データ同期
- 02在庫移動履歴の調査
- 03チェックアウト中のカート向け在庫予約
- 04仕入先納品や在庫移動の受領
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ spree/ agent-skills --skill spree-inventoryインストール後、Claude Code で/spree-inventoryを入力して読み込まれたか確認します。
詳細
Spree Inventory
在庫はバリアントごと、ロケーションごとに追跡されます。バリアントに単一の在庫数はなく、そのバリアントを扱う各 Spree::StockLocation(sloc_)に Spree::StockLevel(sl_)があります。在庫の変更はすべて、その原因を示す Spree::StockMovement(sm_)として記録されます。
5.x(Spree::StockItem、注文完了時に在庫を減算、符号付き数量のみを持つ Spree::StockMovement)から移行する場合は、6.1 までエイリアス Spree::StockItem = Spree::StockLevel が存在します。新しいコードでは 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 は絶対に使用しないでください。これらは移動台帳(監査証跡が残りません)、バックオーダーの補充、キャッシュを無効化するバリアントの touch をスキップします。
在庫移動
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 のフィードには、PATCH をループで実行するのではなく、bulk_upsert を使用してください。絶対値(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)。読み取りパスはローカルのままです。カートへの追加、在庫の引き当て、完了といった判断時点でリアルタイムに確認するには、在庫プロバイダーのコントラクトを使用できます。これは、未保存の StockLevel 行を返す Spree::InventoryProvider::Base#stock_levels_for(variant, stock_location:) で、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。
在庫受け入れ
どちらのドキュメントも同じ方法で受け入れます。物理的な納品ごとに reference(納品書)を持つ Spree::StockReceipt(sr_)が作成され、各明細の数量は 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(6.1 までは旧 stock_item.* 名も併せて発行されます)、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内で行ってください(core はそこで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。