@spree/docs 0.1.223 → 0.1.225

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.
@@ -17,10 +17,19 @@ erDiagram
17
17
  StockLevel ||--o{ StockReservation : "held by checkouts"
18
18
  Order ||--o{ Fulfillment : "ships as"
19
19
  Fulfillment ||--o{ FulfillmentItem : "contains"
20
+ Supplier ||--o{ PurchaseOrder : "supplies"
21
+ PurchaseOrder ||--o{ PurchaseOrderItem : "orders"
22
+ StockTransfer ||--o{ StockTransferItem : "moves"
23
+ PurchaseOrder ||--o{ StockReceipt : "received as"
24
+ StockTransfer ||--o{ StockReceipt : "received as"
25
+ StockReceipt ||--o{ StockReceiptItem : "counts"
26
+ StockReceipt ||--o{ StockMovement : "lands"
20
27
 
21
28
  StockLevel {
22
29
  integer count_on_hand
23
30
  integer allocated_count
31
+ integer reserved_count
32
+ integer incoming_count
24
33
  boolean backorderable
25
34
  }
26
35
  StockLocation {
@@ -30,15 +39,21 @@ erDiagram
30
39
  }
31
40
  ```
32
41
 
33
- ## The three numbers
42
+ ## The five numbers
34
43
 
35
- A stock level carries two counts, and a third is derived from them. Keeping them apart is what stops a warehouse from disagreeing with a website.
44
+ A stock level carries four counts, and a fifth is derived from them. Keeping them apart is what stops a warehouse from disagreeing with a website.
36
45
 
37
46
  | | What it means |
38
47
  |---|---|
39
48
  | `count_on_hand` | What is physically on the shelf, right now |
40
49
  | `allocated_count` | How much of that is already spoken for by orders not yet shipped |
41
- | **`available_count`** | `count_on_hand − allocated_count` — what's actually sellable |
50
+ | `reserved_count` | How much is held by checkouts still in progress |
51
+ | `incoming_count` | How much is on its way here — on a purchase order placed with a supplier, or a transfer already in transit |
52
+ | **`available_count`** | `count_on_hand − allocated_count` — what's actually sellable, before checkout holds |
53
+
54
+ Reserved and incoming are kept as counters rather than worked out on every read, because the Inventory page in the dashboard reads them for every row, many times a day, and only a handful of things change them: entering or leaving checkout moves reserved; placing, cancelling, closing or receiving a purchase order or transfer moves incoming. Nothing else writes them. If a figure ever looks wrong, `rake spree:stock:recount_levels` recomputes both from their sources and reports every row it corrected — the same task the 6.0 upgrade runs to fill them in for existing stock.
55
+
56
+ A transfer counts as incoming from the moment it is marked in transit, not when it is drafted: until the van leaves, nothing is moving. Units a delivery rejects at the dock leave incoming too — they are recorded on the receipt, but they are not coming.
42
57
 
43
58
  > **NOTE:** **Selling an item does not reduce `count_on_hand`.** Placing an order raises `allocated_count`; the physical count only drops when the parcel actually ships.
44
59
  >
@@ -53,10 +68,11 @@ A stock location is somewhere stock physically sits: a warehouse, a shop, a thir
53
68
  | `name` | What staff call it |
54
69
  | `active` | Whether it can be used for new orders |
55
70
  | `default` | Used first when nothing else decides |
71
+ | `kind` | What sort of place it is — `warehouse`, `store` or `fulfillment_center` |
56
72
  | `country_code`, `state_code`, `city`, `postal_code` | Where it is — used for shipping rates and for finding the nearest one |
57
73
  | `backorderable_default` | Whether new stock levels here allow backorders |
58
- | `fulfillable` | Whether orders can ship from here, or it's display-only stock |
59
- | `pickup` | Whether customers can collect here |
74
+ | `pickup_enabled` | Whether customers can collect here |
75
+ | `returns_enabled` | Whether customers can bring returns here |
60
76
 
61
77
  ```typescript Admin SDK
62
78
  const { data: locations } = await adminClient.stockLocations.list()
@@ -77,11 +93,11 @@ A stock level is the intersection of a variant and a location. They're created f
77
93
  ```typescript Admin SDK
78
94
  // Stock for one location
79
95
  const { data: levels } = await adminClient.stockLevels.list({
80
- filter: { stock_location_id_eq: 'sl_xxx' },
96
+ stock_location_id_eq: 'sloc_xxx',
81
97
  })
82
98
 
83
99
  // Correct a single count
84
- await adminClient.stockLevels.update('slv_xxx', {
100
+ await adminClient.stockLevels.update('sl_xxx', {
85
101
  count_on_hand: 150,
86
102
  backorderable: true,
87
103
  })
@@ -89,15 +105,15 @@ await adminClient.stockLevels.update('slv_xxx', {
89
105
  // Set many at once — the right call for a nightly sync
90
106
  await adminClient.stockLevels.bulkUpsert({
91
107
  stock_levels: [
92
- { variant_id: 'var_xxx', stock_location_id: 'sl_xxx', count_on_hand: 40 },
93
- { variant_id: 'var_yyy', stock_location_id: 'sl_xxx', count_on_hand: 12 },
108
+ { variant_id: 'variant_xxx', stock_location_id: 'sloc_xxx', count_on_hand: 40 },
109
+ { variant_id: 'variant_yyy', stock_location_id: 'sloc_xxx', count_on_hand: 12 },
94
110
  ],
95
111
  })
96
112
  ```
97
113
 
98
114
  ```bash CLI
99
- spree api get '/stock_levels?q[stock_location_id_eq]=sl_xxx'
100
- spree api patch /stock_levels/slv_xxx -d '{"count_on_hand": 150}'
115
+ spree api get '/stock_levels?q[stock_location_id_eq]=sloc_xxx'
116
+ spree api patch /stock_levels/sl_xxx -d '{"count_on_hand": 150}'
101
117
  ```
102
118
 
103
119
 
@@ -116,12 +132,15 @@ Every change to stock is recorded as a movement, so "why is this number what it
116
132
  | `adjusted` | A manual correction, such as after a stock count |
117
133
 
118
134
  ```typescript Admin SDK
135
+ // Everything that happened to one SKU, across every location
119
136
  const { data: movements } = await adminClient.stockMovements.list({
120
- filter: { variant_id_eq: 'var_xxx' },
137
+ stock_level_variant_id_eq: 'variant_xxx',
121
138
  })
122
139
  ```
123
140
 
124
- Each movement points at what caused it — the order, the fulfillment, the transfer — so an audit trail reads as a sequence of business events rather than a list of numbers that changed.
141
+ Each movement points at what caused it — the order, the fulfillment, the transfer, the purchase order, and for stock that arrived, the delivery it came in on — so an audit trail reads as a sequence of business events rather than a list of numbers that changed.
142
+
143
+ A movement that arrived through a purchase order also carries what the units cost, so "what did this stock cost us" is answerable from the ledger rather than reconstructed from the orders that bought it.
125
144
 
126
145
  ## What a shopper sees
127
146
 
@@ -151,7 +170,7 @@ A reservation is a short, time-limited hold placed when a customer enters checko
151
170
  | They complete the order | The hold becomes a real allocation |
152
171
  | They abandon it | The hold expires and stock returns |
153
172
 
154
- Reservations never touch `count_on_hand` — they're a separate layer consulted when availability is read. A backorderable item is skipped entirely, since unlimited supply needs no holding.
173
+ Reservations never touch `count_on_hand` — they're a separate layer consulted when availability is read, and their total per stock level is what `reserved_count` shows. A backorderable item is skipped entirely, since unlimited supply needs no holding.
155
174
 
156
175
  How long the hold lasts is a **store setting**, because it's a decision about the checkout experience rather than a property of a warehouse. Reservations can also be switched off entirely.
157
176
 
@@ -165,12 +184,108 @@ When new stock arrives, backorders are filled first, and only the remainder beco
165
184
 
166
185
  ## Moving stock between locations
167
186
 
168
- A stock transfer moves inventory from one location to another, and reflects that this takes time: stock is booked out of the source, is in transit, and is received at the destination — where a partial receipt is a normal outcome, because sometimes not everything arrives.
187
+ A stock transfer moves inventory from one location to another, and reflects that this takes time: stock is booked out of the source, is in transit, and is counted in at the destination — where a partial delivery is a normal outcome, because sometimes not everything arrives at once.
188
+
189
+ That is four steps, not one. A transfer is planned as a draft, packed, sent, and finally counted in:
190
+
191
+ ```typescript Admin SDK
192
+ const transfer = await adminClient.stockTransfers.create({
193
+ source_location_id: 'sloc_xxx',
194
+ destination_location_id: 'sloc_yyy',
195
+ items: [{ variant_id: 'variant_xxx', quantity_shipped: 10 }],
196
+ })
197
+
198
+ // The units leave the source here — and are on no shelf until they arrive.
199
+ await adminClient.stockTransfers.markInTransit(transfer.id)
200
+
201
+ // Eight arrived intact and two were crushed. See "Counting a delivery in".
202
+ await adminClient.stockTransfers.stockReceipts.create(transfer.id, {
203
+ items: [{ id: 'sti_xxx', quantity_accepted: 8, quantity_rejected: 2, rejection_reason: 'damaged' }],
204
+ })
205
+ ```
206
+
207
+ | Status | What it means |
208
+ |---|---|
209
+ | `draft` | A plan. Lines can still change; nothing has moved |
210
+ | `ready_to_ship` | Packed. Lines are frozen, nothing has left yet — `markDraft` unfreezes it |
211
+ | `in_transit` | The units have left the source and are on no shelf |
212
+ | `partially_received` | Some units have been counted in; the rest are still on the road |
213
+ | `received` | Everything that left has arrived — or the balance was closed short |
214
+ | `over_received` | More arrived than was sent |
215
+ | `canceled` | Called off. Once units have left, the merchant says whether they come back or are written off |
216
+
217
+ Two things follow from the middle step. Availability at the destination does not rise until somebody counts the goods in, which is what makes the number trustworthy. And cancelling a transfer whose units have already left is a decision rather than an undo: either they come back to the source, or they are written off as lost.
218
+
219
+ ## Buying stock in
220
+
221
+ Stock that comes from a supplier is a purchase order, not a transfer — it has a supplier, a cost per unit and a date it was promised for, none of which apply to moving goods you already own.
222
+
223
+ ```typescript Admin SDK
224
+ const order = await adminClient.purchaseOrders.create({
225
+ supplier_id: 'sup_xxx',
226
+ destination_location_id: 'sloc_xxx',
227
+ expected_at: '2026-10-01',
228
+ cancel_by: '2026-10-15',
229
+ items: [{ variant_id: 'variant_xxx', quantity_ordered: 100, unit_cost: '12.50' }],
230
+ })
231
+
232
+ await adminClient.purchaseOrders.markOrdered(order.id)
233
+
234
+ // A delivery arrives: 58 accepted, 2 refused. The order stays open for the rest.
235
+ await adminClient.purchaseOrders.stockReceipts.create(order.id, {
236
+ reference: 'DN-4471',
237
+ items: [{ id: 'poi_xxx', quantity_accepted: 58, quantity_rejected: 2, rejection_reason: 'damaged' }],
238
+ })
239
+ ```
240
+
241
+ | Status | What it means |
242
+ |---|---|
243
+ | `draft` | Being put together. Lines and costs can still change |
244
+ | `ordered` | Sent to the supplier. Lines are frozen — `markDraft` reopens it while nothing has arrived |
245
+ | `partially_received` | Some units have been counted in; the supplier still owes the rest |
246
+ | `received` | Every line has what it expected — or the balance was closed short |
247
+ | `over_received` | The supplier sent more than was ordered |
248
+ | `canceled` | Called off. Units already received stay on the shelf |
249
+
250
+ Ordered stock is never available stock. Nothing reaches `count_on_hand` until a delivery is counted in, because a merchant who has ordered goods does not have them — and suppliers under-ship often enough that receiving in parts is the ordinary case.
251
+
252
+ An order carries the day the supplier promised (`expected_at`) and, optionally, the day after which the goods are no longer wanted (`cancel_by`). Both are calendar dates. The dashboard lists filter on *Overdue* and *Past cancel-by*; nothing cancels automatically, because that is a call to confirm with the supplier.
253
+
254
+ Purchase orders export to CSV, one row per line, which is the file a merchant sends a supplier. They import from one too: rows sharing a `reference` become one draft order, naming the supplier, warehouse and SKU by name — a name that does not exist fails the row rather than creating anything.
255
+
256
+ Suppliers themselves are an address book, kept per store:
257
+
258
+ ```typescript Admin SDK
259
+ const { data: suppliers } = await adminClient.suppliers.list()
260
+ ```
261
+
262
+ ## Counting a delivery in
263
+
264
+ Transfers and purchase orders are received the same way: each delivery is a **stock receipt**, numbered and dated, carrying the packing-slip reference and — per line — what was accepted and what was refused, with a reason.
169
265
 
170
266
  ```typescript Admin SDK
171
- const { data: transfers } = await adminClient.stockTransfers.list()
267
+ const { data: deliveries } = await adminClient.purchaseOrders.stockReceipts.list(order.id, {
268
+ expand: ['items'],
269
+ })
172
270
  ```
173
271
 
272
+ A receipt records *this delivery's* counts, not running totals: a second box adds to the first rather than restating it. Accepted units land on the destination's shelf, each as a `received` movement that names the receipt. Refused units are recorded and never stocked; the reason is one of `damaged`, `wrong_item`, `expired` or `other`. Omit `items` to count in everything still outstanding, intact.
273
+
274
+ > **NOTE:** **Refused units count differently on the two documents.** A supplier's refused units are still owed — the goods go back, and the order stays `partially_received` until they are replaced. A transfer's refused units have *arrived*, in whatever state: a crushed unit is not still in the van, so it counts toward the trip being over.
275
+
276
+ When the balance is not coming — the supplier is out of stock, the missing units fell off the van — the document is **closed short**: it settles in `received` with `closed_short_at` and the reason recorded, and the outstanding count stays on each line as the record of the gap. Nothing moves.
277
+
278
+ ```typescript Admin SDK
279
+ await adminClient.purchaseOrders.close(order.id, { reason: 'Supplier out of stock' })
280
+ await adminClient.stockTransfers.close(transfer.id, { reason: 'Two fell off the van' })
281
+ ```
282
+
283
+ ## The Inventory page
284
+
285
+ The dashboard's Inventory section opens on a list with one row per variant per location: on hand, allocated, reserved, available and incoming — the page uses the column names, with the location as a filter and SKU or product name as the search. Every figure on it is a column read off the stock level.
286
+
287
+ The on-hand figure is corrected in place — set it to a count, or adjust it by a delta, and say why — and the change lands in the stock history like any other adjustment. The incoming figure links to the two ways of making it grow: a transfer from another warehouse, or a purchase order from a supplier.
288
+
174
289
  ## Turning tracking off
175
290
 
176
291
  Some things don't need counting — a service, a made-to-order item, a digital download. Tracking can be switched off, and those variants are always purchasable.
@@ -71,7 +71,9 @@ await client.customers.addresses.create('cus_xxx', {
71
71
  |---|---|
72
72
  | `client.stockLocations` | CRUD on stock locations. |
73
73
  | `client.stockItems` | List, get, update, delete stock items. |
74
- | `client.stockTransfers` | List, get, create, delete stock transfers. |
74
+ | `client.stockTransfers` | CRUD plus the lifecycle: `markReady`, `markInTransit`, `receive`, `cancel`. |
75
+ | `client.suppliers` | CRUD on the suppliers you buy from. |
76
+ | `client.purchaseOrders` | CRUD plus `markOrdered`, `receive`, `cancel`. |
75
77
 
76
78
  ## Sales channels
77
79
 
@@ -130,6 +130,14 @@ bundle exec rake spree:backfill_order_markets
130
130
 
131
131
  Orders missing a market get the store default — `market` is required on carts and orders in 6.0.
132
132
 
133
+ ### Fill in the stock level counters
134
+
135
+ ```bash
136
+ bundle exec rake spree:stock:recount_levels
137
+ ```
138
+
139
+ `spree_stock_levels` gains `reserved_count` (units held by checkouts in progress) and `incoming_count` (units on their way on a purchase order placed with a supplier or a transfer in transit). Both are kept by the workflows that change them and start at zero on an existing install; this recomputes them from active reservations and open documents and prints every level it corrected. Until it runs, the dashboard's Inventory page shows zero reserved and incoming for stock that predates the upgrade. Safe to re-run at any time.
140
+
133
141
  ## The Cart/Order split
134
142
 
135
143
  The single biggest change. What used to be one `Spree::Order` living through checkout and beyond is now two models:
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@spree/docs",
3
- "version": "0.1.223",
3
+ "version": "0.1.225",
4
4
  "description": "Spree Commerce developer documentation for AI agents and local reference",
5
5
  "type": "module",
6
6
  "license": "CC-BY-4.0",