@spree/docs 0.1.181 → 0.1.183
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.
- package/dist/api-reference/seller-api/errors.md +1 -1
- package/dist/api-reference/store.yaml +78 -91
- package/dist/developer/core-concepts/addresses.md +106 -198
- package/dist/developer/core-concepts/architecture.md +97 -126
- package/dist/developer/core-concepts/calculators.md +75 -252
- package/dist/developer/core-concepts/carts.md +1 -1
- package/dist/developer/core-concepts/channels.md +0 -4
- package/dist/developer/core-concepts/companies-and-catalogs.md +1 -1
- package/dist/developer/core-concepts/customers.md +0 -3
- package/dist/developer/core-concepts/discounts.md +133 -0
- package/dist/developer/core-concepts/events.md +83 -576
- package/dist/developer/core-concepts/fees.md +144 -0
- package/dist/developer/core-concepts/imports-exports.md +105 -679
- package/dist/developer/core-concepts/inventory.md +114 -248
- package/dist/developer/core-concepts/markets.md +9 -12
- package/dist/developer/core-concepts/media.md +9 -11
- package/dist/developer/core-concepts/metafields.md +123 -200
- package/dist/developer/core-concepts/order-totals.md +110 -0
- package/dist/developer/core-concepts/orders.md +1 -1
- package/dist/developer/core-concepts/payments.md +11 -14
- package/dist/developer/core-concepts/pricing.md +11 -13
- package/dist/developer/core-concepts/products.md +173 -19
- package/dist/developer/core-concepts/promotions.md +12 -11
- package/dist/developer/core-concepts/search-filtering.md +2 -4
- package/dist/developer/core-concepts/store-credits-gift-cards.md +0 -3
- package/dist/developer/core-concepts/taxes.md +125 -113
- package/dist/developer/core-concepts/translations.md +61 -68
- package/dist/developer/core-concepts/webhooks.md +25 -59
- package/dist/developer/how-to/custom-promotion.md +3 -3
- package/package.json +1 -1
- package/dist/developer/core-concepts/taxes-discounts-fees.md +0 -199
|
@@ -1,325 +1,191 @@
|
|
|
1
1
|
---
|
|
2
2
|
title: Inventory
|
|
3
|
-
description:
|
|
3
|
+
description: How Spree tracks stock across locations — what's on hand, what's spoken for, and what a shopper is allowed to buy.
|
|
4
4
|
---
|
|
5
5
|
|
|
6
|
-
import { Since } from '/snippets/since.mdx';
|
|
7
|
-
|
|
8
6
|
## Overview
|
|
9
7
|
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
When products are sold or returned, individual `InventoryUnit` records track each unit through the fulfillment process.
|
|
13
|
-
|
|
14
|
-
Adding new inventory to an out-of-stock product that has backorders will first fill the backorders, then update the available count with the remainder.
|
|
15
|
-
|
|
16
|
-
During checkout, Spree holds stock with time-limited [Stock Reservations](#stock-reservations) to prevent two customers from buying the same last unit simultaneously.
|
|
8
|
+
Inventory answers one question a shopper cares about — *can I buy this?* — and several a merchant cares about: where is it, how much is left, and what happened to the rest.
|
|
17
9
|
|
|
18
|
-
|
|
10
|
+
Spree tracks stock **per location**. A variant doesn't have "a stock count"; it has a count at each warehouse or shop that carries it. That's what makes it possible to ship from the nearest location, or to let a customer collect in store.
|
|
19
11
|
|
|
20
12
|
```mermaid
|
|
21
13
|
erDiagram
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
StockItem {
|
|
14
|
+
Variant ||--o{ StockLevel : "stocked at each location"
|
|
15
|
+
StockLocation ||--o{ StockLevel : "holds"
|
|
16
|
+
StockLevel ||--o{ StockMovement : "changed by"
|
|
17
|
+
StockLevel ||--o{ StockReservation : "held by checkouts"
|
|
18
|
+
Order ||--o{ Fulfillment : "ships as"
|
|
19
|
+
Fulfillment ||--o{ FulfillmentItem : "contains"
|
|
20
|
+
|
|
21
|
+
StockLevel {
|
|
32
22
|
integer count_on_hand
|
|
23
|
+
integer allocated_count
|
|
33
24
|
boolean backorderable
|
|
34
|
-
integer stock_location_id
|
|
35
|
-
integer variant_id
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
StockMovement {
|
|
39
|
-
integer quantity
|
|
40
|
-
string originator_type
|
|
41
|
-
integer originator_id
|
|
42
|
-
integer stock_item_id
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
StockTransfer {
|
|
46
|
-
string number
|
|
47
|
-
string reference
|
|
48
|
-
integer source_location_id
|
|
49
|
-
integer destination_location_id
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
InventoryUnit {
|
|
53
|
-
string state
|
|
54
|
-
integer variant_id
|
|
55
|
-
integer order_id
|
|
56
|
-
integer shipment_id
|
|
57
25
|
}
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
integer stock_item_id
|
|
63
|
-
integer line_item_id
|
|
64
|
-
integer order_id
|
|
26
|
+
StockLocation {
|
|
27
|
+
string name
|
|
28
|
+
boolean active
|
|
29
|
+
string country_code
|
|
65
30
|
}
|
|
66
|
-
|
|
67
|
-
StockLocation ||--o{ StockItem : "has many"
|
|
68
|
-
StockLocation ||--o{ StockTransfer : "source"
|
|
69
|
-
StockLocation ||--o{ StockTransfer : "destination"
|
|
70
|
-
Variant ||--o{ StockItem : "has many"
|
|
71
|
-
Variant ||--o{ InventoryUnit : "has many"
|
|
72
|
-
StockItem ||--o{ StockMovement : "has many"
|
|
73
|
-
StockItem ||--o{ StockReservation : "has many"
|
|
74
|
-
StockTransfer ||--o{ StockMovement : "has many"
|
|
75
|
-
Order ||--o{ InventoryUnit : "has many"
|
|
76
|
-
Order ||--o{ StockReservation : "has many"
|
|
77
|
-
Shipment ||--o{ InventoryUnit : "has many"
|
|
78
31
|
```
|
|
79
32
|
|
|
80
|
-
|
|
81
|
-
- **Stock Location** → Contains Stock Items (inventory per variant) and is the source/destination for Stock Transfers
|
|
82
|
-
- **Stock Item** → Tracks quantity (`count_on_hand`) for a specific Variant at a specific Stock Location
|
|
83
|
-
- **Stock Movement** → Records changes to Stock Item quantities (purchases, returns, transfers)
|
|
84
|
-
- **Stock Transfer** → Moves inventory between Stock Locations, creating Stock Movements at source and destination
|
|
85
|
-
- **Inventory Unit** → Represents individual units in [Orders](orders.md) and [Shipments](fulfillments.md)
|
|
86
|
-
- **Stock Reservation** → Time-limited soft hold on a Stock Item during checkout, scoped to a specific Order and Line Item
|
|
87
|
-
|
|
88
|
-
## Inventory Management
|
|
89
|
-
|
|
90
|
-
### Stock Locations
|
|
91
|
-
|
|
92
|
-
Stock Locations are the physical locations where your inventory is stored and shipped from.
|
|
93
|
-
|
|
94
|
-
Stock Locations can be created in the Admin Panel under **Settings → Stock Locations**, or via the Admin API.
|
|
33
|
+
## The three numbers
|
|
95
34
|
|
|
96
|
-
|
|
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.
|
|
97
36
|
|
|
98
|
-
|
|
|
99
|
-
|
|
100
|
-
| `
|
|
101
|
-
| `
|
|
102
|
-
|
|
|
103
|
-
| `address2` | The secondary address line for the stock location. | Suite 100 |
|
|
104
|
-
| `city` | The city where the stock location is based. | New York |
|
|
105
|
-
| `state_id` | The ID of the state where the stock location is based. This references the `State` model. | 1 |
|
|
106
|
-
| `country_id` | The ID of the country where the stock location is based. This references the `Country` model. | 1 |
|
|
107
|
-
| `zipcode` | The postal code for the stock location. | 10001 |
|
|
108
|
-
| `phone` | The contact phone number for the stock location. | 555-1234 |
|
|
109
|
-
| `active` | A boolean indicating whether the stock location is active. Inactive stock locations will not be used in stock calculations or be available for selection during checkout. | `true` |
|
|
110
|
-
| `default` | A boolean indicating whether the stock location is the default one used for new inventory operations. | `false` |
|
|
111
|
-
| `backorderable_default` | A boolean indicating whether new stock items in this location are backorderable by default. | `false` |
|
|
112
|
-
| `propagate_all_variants` | A boolean indicating whether new stock items should be automatically created for all Store variants when a new stock location is added. | `false` |
|
|
37
|
+
| | What it means |
|
|
38
|
+
|---|---|
|
|
39
|
+
| `count_on_hand` | What is physically on the shelf, right now |
|
|
40
|
+
| `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 |
|
|
113
42
|
|
|
114
|
-
|
|
43
|
+
> **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
|
+
>
|
|
45
|
+
> This is deliberate. Until it leaves the building, the stock *is* still on the shelf — and a warehouse worker counting boxes should find the number Spree reports. Overselling shows up honestly as `allocated_count` exceeding what's on hand, rather than as an impossible negative count.
|
|
115
46
|
|
|
116
|
-
|
|
47
|
+
## Stock locations
|
|
117
48
|
|
|
118
|
-
|
|
49
|
+
A stock location is somewhere stock physically sits: a warehouse, a shop, a third-party fulfillment centre.
|
|
119
50
|
|
|
51
|
+
| Attribute | Description |
|
|
52
|
+
|---|---|
|
|
53
|
+
| `name` | What staff call it |
|
|
54
|
+
| `active` | Whether it can be used for new orders |
|
|
55
|
+
| `default` | Used first when nothing else decides |
|
|
56
|
+
| `country_code`, `state_code`, `city`, `postal_code` | Where it is — used for shipping rates and for finding the nearest one |
|
|
57
|
+
| `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 |
|
|
120
60
|
|
|
121
61
|
```typescript Admin SDK
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
const client = createAdminClient({
|
|
125
|
-
baseUrl: 'https://store.example.com',
|
|
126
|
-
secretKey: 'sk_xxx',
|
|
127
|
-
})
|
|
62
|
+
const { data: locations } = await adminClient.stockLocations.list()
|
|
128
63
|
|
|
129
|
-
|
|
130
|
-
name: 'Warehouse
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
propagate_all_variants: true,
|
|
64
|
+
await adminClient.stockLocations.create({
|
|
65
|
+
name: 'Berlin Warehouse',
|
|
66
|
+
country_code: 'DE',
|
|
67
|
+
city: 'Berlin',
|
|
68
|
+
active: true,
|
|
135
69
|
})
|
|
136
|
-
|
|
137
|
-
await client.stockLocations.update('sloc_xxx', { active: false })
|
|
138
|
-
```
|
|
139
|
-
|
|
140
|
-
```bash CLI
|
|
141
|
-
spree api post /stock_locations -d '{
|
|
142
|
-
"name": "Warehouse 1",
|
|
143
|
-
"default": true,
|
|
144
|
-
"country_iso": "US",
|
|
145
|
-
"propagate_all_variants": true
|
|
146
|
-
}'
|
|
147
70
|
```
|
|
148
71
|
|
|
72
|
+
## Reading and adjusting stock
|
|
149
73
|
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
Stock Items represent the inventory at a stock location for a specific variant. Stock item count on hand can be increased or decreased by creating stock movements.
|
|
153
|
-
|
|
154
|
-
| Attribute | Description | Example Value |
|
|
155
|
-
|-------------------|-----------------------------------------------------------------------------|---------------------|
|
|
156
|
-
| `stock_location_id` | References the stock location where the stock item belongs. | `1` |
|
|
157
|
-
| `variant_id` | References the variant associated with the stock item. | `32` |
|
|
158
|
-
| `count_on_hand` | The number of items available on hand. | `150` |
|
|
159
|
-
| `backorderable` | Indicates whether the stock item can be backordered. | `true` |
|
|
160
|
-
|
|
161
|
-
Stock items are created automatically — for all variants when a location has `propagate_all_variants`, or via a variant's `stock_items` on create. To adjust quantity or backorderable status, **update** the existing stock item via the Admin API. The example below uses a Ransack predicate (`stock_location_id_eq`) to [list a location's stock items](../../api-reference/admin-api/querying.md) before updating one:
|
|
74
|
+
A stock level is the intersection of a variant and a location. They're created for you — for every variant when a location propagates all variants, or as variants are added.
|
|
162
75
|
|
|
163
76
|
|
|
164
77
|
```typescript Admin SDK
|
|
165
|
-
//
|
|
166
|
-
const { data:
|
|
78
|
+
// Stock for one location
|
|
79
|
+
const { data: levels } = await adminClient.stockLevels.list({
|
|
80
|
+
filter: { stock_location_id_eq: 'sl_xxx' },
|
|
81
|
+
})
|
|
167
82
|
|
|
168
|
-
|
|
83
|
+
// Correct a single count
|
|
84
|
+
await adminClient.stockLevels.update('slv_xxx', {
|
|
169
85
|
count_on_hand: 150,
|
|
170
86
|
backorderable: true,
|
|
171
87
|
})
|
|
88
|
+
|
|
89
|
+
// Set many at once — the right call for a nightly sync
|
|
90
|
+
await adminClient.stockLevels.bulkUpsert({
|
|
91
|
+
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 },
|
|
94
|
+
],
|
|
95
|
+
})
|
|
172
96
|
```
|
|
173
97
|
|
|
174
98
|
```bash CLI
|
|
175
|
-
spree api get /
|
|
176
|
-
spree api patch /
|
|
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}'
|
|
177
101
|
```
|
|
178
102
|
|
|
179
103
|
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
Stock transfers allow you to move inventory in bulk from one stock location to another stock location. This is handy when you want to integrate with a POS system or other inventory management system. Or you can just rely on Spree being the source of truth for your inventory.
|
|
104
|
+
> **WARNING:** Use `bulkUpsert` for feeds from a warehouse or ERP rather than a loop of single updates. It's one request instead of thousands, and it won't half-apply if the connection drops.
|
|
183
105
|
|
|
184
|
-
|
|
106
|
+
## Stock movements
|
|
185
107
|
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
| Attribute | Description | Example Value |
|
|
189
|
-
|--------------------------|-----------------------------------------------------------------------------|---------------------|
|
|
190
|
-
| `number` | The unique number identifier for the stock transfer, generated automatically. | `T123456789` |
|
|
191
|
-
| `reference` | An optional reference field for the stock transfer. | Transfer for Event |
|
|
192
|
-
| `source_location_id` | The ID of the stock location where the stock is transferred from. | `2` |
|
|
193
|
-
| `destination_location_id`| The ID of the stock location where the stock is transferred to. | `3` |
|
|
194
|
-
|
|
195
|
-
Create a transfer via the [Admin API](../../api-reference/admin-api/introduction.md), listing the variants and quantities to move. Omit `source_location_id` to record an incoming receipt from a vendor:
|
|
108
|
+
Every change to stock is recorded as a movement, so "why is this number what it is" always has an answer.
|
|
196
109
|
|
|
110
|
+
| Kind | When it happens |
|
|
111
|
+
|---|---|
|
|
112
|
+
| `received` | New stock arrives |
|
|
113
|
+
| `allocated` | An order claims stock — `allocated_count` goes up |
|
|
114
|
+
| `shipped` | A parcel leaves — `count_on_hand` goes down, the allocation is retired |
|
|
115
|
+
| `released` | An allocation is given back, because an order was cancelled |
|
|
116
|
+
| `adjusted` | A manual correction, such as after a stock count |
|
|
197
117
|
|
|
198
118
|
```typescript Admin SDK
|
|
199
|
-
const
|
|
200
|
-
|
|
201
|
-
destination_location_id: 'sloc_store',
|
|
202
|
-
reference: 'Transfer for Event',
|
|
203
|
-
variants: [
|
|
204
|
-
{ variant_id: 'variant_xxx', quantity: 20 },
|
|
205
|
-
{ variant_id: 'variant_yyy', quantity: 5 },
|
|
206
|
-
],
|
|
119
|
+
const { data: movements } = await adminClient.stockMovements.list({
|
|
120
|
+
filter: { variant_id_eq: 'var_xxx' },
|
|
207
121
|
})
|
|
208
122
|
```
|
|
209
123
|
|
|
210
|
-
|
|
211
|
-
spree api post /stock_transfers -d '{
|
|
212
|
-
"source_location_id": "sloc_warehouse",
|
|
213
|
-
"destination_location_id": "sloc_store",
|
|
214
|
-
"variants": [{ "variant_id": "variant_xxx", "quantity": 20 }]
|
|
215
|
-
}'
|
|
216
|
-
```
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
Stock transfers are crucial for managing inventory across multiple locations, ensuring that stock levels are accurate and up-to-date.
|
|
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.
|
|
220
125
|
|
|
221
|
-
|
|
126
|
+
## What a shopper sees
|
|
222
127
|
|
|
223
|
-
|
|
128
|
+
For a storefront, all of the above collapses into one question, and the answer is already computed:
|
|
224
129
|
|
|
225
|
-
|
|
130
|
+
```typescript Store SDK
|
|
131
|
+
const product = await client.products.get('spree-tote')
|
|
226
132
|
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
Here's the list of attributes for the Stock Movement model:
|
|
234
|
-
|
|
235
|
-
| Attribute | Description | Example Value |
|
|
236
|
-
|-------------------|-----------------------------------------------------------------------------|---------------------|
|
|
237
|
-
| `stock_item_id` | References the stock item that the movement belongs to. | `45` |
|
|
238
|
-
| `quantity` | The quantity by which the stock item's count on hand is changed. Positive values indicate stock being added, while negative values indicate stock being removed. | `-10` |
|
|
239
|
-
| `originator_type` | The type of the originator of the stock movement. This is a polymorphic association. | `Spree::Shipment` |
|
|
240
|
-
| `originator_id` | The ID of the originator of the stock movement. This is used in conjunction with `originator_type`. | `2` |
|
|
241
|
-
|
|
242
|
-
Stock Movements are crucial for maintaining accurate inventory levels and for historical tracking of inventory adjustments.
|
|
243
|
-
|
|
244
|
-
## Stock Reservations
|
|
133
|
+
product.variants.forEach((variant) => {
|
|
134
|
+
variant.in_stock // can it be bought right now
|
|
135
|
+
variant.purchasable // in stock, or backorderable
|
|
136
|
+
})
|
|
137
|
+
```
|
|
245
138
|
|
|
246
|
-
|
|
139
|
+
`in_stock` already accounts for what's allocated and what other shoppers are holding in checkout. There is nothing to compute client-side, and no separate inventory request to make.
|
|
247
140
|
|
|
248
|
-
|
|
141
|
+
## Reservations during checkout
|
|
249
142
|
|
|
250
|
-
|
|
143
|
+
Without reservations, two customers can both see the last unit, both start checkout, and one discovers at the payment step that it's gone. That's a bad moment to find out.
|
|
251
144
|
|
|
252
|
-
|
|
145
|
+
A reservation is a short, time-limited hold placed when a customer enters checkout. Availability drops for everyone else immediately.
|
|
253
146
|
|
|
254
|
-
|
|
|
147
|
+
| Moment | What happens |
|
|
255
148
|
|---|---|
|
|
256
|
-
| Customer enters checkout |
|
|
257
|
-
|
|
|
258
|
-
|
|
|
259
|
-
|
|
|
260
|
-
|
|
261
|
-
Reservations attach to each line item; when a line item or order is removed, the reservation goes with it.
|
|
262
|
-
|
|
263
|
-
### Configuration
|
|
149
|
+
| Customer enters checkout | Their items are held, with an expiry |
|
|
150
|
+
| They keep editing the cart | The expiry is pushed forward |
|
|
151
|
+
| They complete the order | The hold becomes a real allocation |
|
|
152
|
+
| They abandon it | The hold expires and stock returns |
|
|
264
153
|
|
|
265
|
-
|
|
266
|
-
|---|---|---|
|
|
267
|
-
| `Spree::Config[:stock_reservations_enabled]` | `true` | Global kill switch. When `false`, reservations are not created and availability ignores them — behavior matches pre-5.5. |
|
|
268
|
-
| `Spree::Config[:default_stock_reservation_ttl_minutes]` | `10` | Fallback hold duration when a Store doesn't override. |
|
|
269
|
-
| `store.preferred_stock_reservation_ttl_minutes` | `10` | Per-Store override. Falls back to the global default only when explicitly unset/blank. |
|
|
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.
|
|
270
155
|
|
|
271
|
-
|
|
156
|
+
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.
|
|
272
157
|
|
|
273
|
-
|
|
158
|
+
> **INFO:** Expired reservations are cleaned up by a background job. Spree ships the job but does not schedule it — your app's job runner should run it every minute or so.
|
|
274
159
|
|
|
275
|
-
|
|
160
|
+
## Backorders
|
|
276
161
|
|
|
277
|
-
|
|
162
|
+
A stock level marked `backorderable` can be sold past zero. Orders are accepted, allocated, and wait for stock.
|
|
278
163
|
|
|
279
|
-
|
|
164
|
+
When new stock arrives, backorders are filled first, and only the remainder becomes available to new customers — so the people who waited longest aren't overtaken by whoever happens to visit the site next.
|
|
280
165
|
|
|
281
|
-
|
|
166
|
+
## Moving stock between locations
|
|
282
167
|
|
|
283
|
-
|
|
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.
|
|
284
169
|
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
We create `InventoryUnit` objects when:
|
|
290
|
-
|
|
291
|
-
* a product is sold (they are added to the Shipment)
|
|
292
|
-
* a product is returned
|
|
293
|
-
|
|
294
|
-
Here's a list of attributes for the Inventory Unit model:
|
|
295
|
-
|
|
296
|
-
| Attribute | Description | Example Value |
|
|
297
|
-
|-------------------|-----------------------------------------------------------------------------|---------------------|
|
|
298
|
-
| `variant_id` | References the variant associated with the inventory unit. | `32` |
|
|
299
|
-
| `order_id` | References the order associated with the inventory unit. | `123` |
|
|
300
|
-
| `shipment_id` | References the shipment associated with the inventory unit. | `77` |
|
|
301
|
-
| `state` | The state of the inventory unit | `on_hand` |
|
|
170
|
+
```typescript Admin SDK
|
|
171
|
+
const { data: transfers } = await adminClient.stockTransfers.list()
|
|
172
|
+
```
|
|
302
173
|
|
|
303
|
-
|
|
174
|
+
## Turning tracking off
|
|
304
175
|
|
|
305
|
-
|
|
306
|
-
* `backordered` - the inventory unit is backordered
|
|
307
|
-
* `shipped` - the inventory unit is shipped
|
|
308
|
-
* `returned` - the inventory unit has been returned
|
|
176
|
+
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.
|
|
309
177
|
|
|
310
|
-
|
|
178
|
+
This is a store-level setting, so a store that sells only digital goods needn't manage stock at all.
|
|
311
179
|
|
|
312
|
-
##
|
|
180
|
+
## Syncing from an external system
|
|
313
181
|
|
|
314
|
-
If
|
|
182
|
+
If your inventory truth lives in an ERP or a warehouse system, Spree can defer to it. The rule that matters: **the read path stays local.** Whether a variant is in stock is answered from Spree's own data, not by calling out to another system while a shopper waits for a product page.
|
|
315
183
|
|
|
316
|
-
|
|
317
|
-
- **Globally** — disable inventory tracking for the entire store in your Spree configuration
|
|
184
|
+
Feeds come in through the bulk update above; live checks happen only at decision moments, like completing an order. See [Providers](../providers/overview.md).
|
|
318
185
|
|
|
319
|
-
## Related
|
|
186
|
+
## Related
|
|
320
187
|
|
|
321
|
-
- [Products](products.md)
|
|
322
|
-
- [
|
|
323
|
-
- [Orders](orders.md)
|
|
324
|
-
- [
|
|
325
|
-
- [Admin API authentication](../../api-reference/admin-api/authentication.md) - How to obtain and scope the secret key (`sk_xxx`) used by these calls
|
|
188
|
+
- [Products](products.md) — variants, the things stock is counted for
|
|
189
|
+
- [Fulfillments](fulfillments.md) — how stock leaves the building
|
|
190
|
+
- [Orders](orders.md) — where allocation happens
|
|
191
|
+
- [Carts](carts.md) — checkout and reservations
|
|
@@ -3,9 +3,6 @@ title: Markets
|
|
|
3
3
|
description: Configure Spree Markets to bundle geography, currency, and locale into distinct selling regions and run multi-region commerce from a single store.
|
|
4
4
|
---
|
|
5
5
|
|
|
6
|
-
import { Since } from '/snippets/since.mdx';
|
|
7
|
-
|
|
8
|
-
|
|
9
6
|
## Overview
|
|
10
7
|
|
|
11
8
|
Markets let you segment a single [Store](stores.md) into distinct geographic regions, each with its own currency, locale, and set of countries. For example, an international store might define:
|
|
@@ -50,14 +47,14 @@ erDiagram
|
|
|
50
47
|
| `supported_locales` | All locales available in this market | `["en", "es"]` |
|
|
51
48
|
| `tax_inclusive` | Whether prices include tax (affects display and checkout calculation) | `false` |
|
|
52
49
|
| `default` | Whether this is the fallback market when no country match is found | `true` |
|
|
53
|
-
| `
|
|
50
|
+
| `country_codes` | Countries in this market, as ISO codes | `["US", "CA"]` |
|
|
54
51
|
|
|
55
52
|
## How Markets Work
|
|
56
53
|
|
|
57
54
|
When a customer visits your store, their country determines which market applies. The market then sets the currency, locale, and tax behavior for that session.
|
|
58
55
|
|
|
59
56
|
```
|
|
60
|
-
Customer's Country → Market → Currency + Locale + Tax
|
|
57
|
+
Customer's Country → Market → Currency + Locale + Tax treatment
|
|
61
58
|
```
|
|
62
59
|
|
|
63
60
|
The resolution chain:
|
|
@@ -65,7 +62,7 @@ The resolution chain:
|
|
|
65
62
|
1. Customer's country is detected (from URL, geolocation, `X-Spree-Country` header, or manual selection)
|
|
66
63
|
2. Spree finds the market containing that country
|
|
67
64
|
3. The market's currency and locale become the defaults for the session
|
|
68
|
-
4. The market's tax
|
|
65
|
+
4. The market's tax setting determines whether prices are shown with or without tax
|
|
69
66
|
|
|
70
67
|
If no market matches the customer's country, the store's **default market** is used.
|
|
71
68
|
|
|
@@ -215,9 +212,9 @@ The `tax_inclusive` flag on a market controls how prices are displayed and calcu
|
|
|
215
212
|
- **`tax_inclusive: true`** (common in Europe) — the price shown to the customer already includes tax
|
|
216
213
|
- **`tax_inclusive: false`** (common in the US) — tax is added at checkout on top of the displayed price
|
|
217
214
|
|
|
218
|
-
|
|
215
|
+
This matters because prices are shown long before anyone knows where the shopper lives. The market's own country supplies the assumed rate for browsing; once a shipping address is entered at checkout, that address takes over and the real rate applies.
|
|
219
216
|
|
|
220
|
-
See [Taxes](taxes.md)
|
|
217
|
+
A market also chooses **which tax provider works out its numbers** — Spree's own rate tables in a simple market, an external tax service in a complicated one. See [Taxes](taxes.md).
|
|
221
218
|
|
|
222
219
|
## Pricing Integration
|
|
223
220
|
|
|
@@ -247,7 +244,7 @@ const northAmerica = await client.markets.create({
|
|
|
247
244
|
name: 'North America',
|
|
248
245
|
currency: 'USD',
|
|
249
246
|
default_locale: 'en',
|
|
250
|
-
|
|
247
|
+
country_codes: ['US', 'CA'],
|
|
251
248
|
default: true,
|
|
252
249
|
})
|
|
253
250
|
|
|
@@ -258,7 +255,7 @@ const europe = await client.markets.create({
|
|
|
258
255
|
default_locale: 'de',
|
|
259
256
|
supported_locales: ['de', 'en', 'fr'],
|
|
260
257
|
tax_inclusive: true,
|
|
261
|
-
|
|
258
|
+
country_codes: ['DE', 'FR', 'AT', 'NL'],
|
|
262
259
|
})
|
|
263
260
|
```
|
|
264
261
|
|
|
@@ -267,7 +264,7 @@ spree api post /markets -d '{
|
|
|
267
264
|
"name": "North America",
|
|
268
265
|
"currency": "USD",
|
|
269
266
|
"default_locale": "en",
|
|
270
|
-
"
|
|
267
|
+
"country_codes": ["US", "CA"],
|
|
271
268
|
"default": true
|
|
272
269
|
}'
|
|
273
270
|
```
|
|
@@ -291,6 +288,6 @@ spree api delete /markets/market_xxx
|
|
|
291
288
|
|
|
292
289
|
- [Markets (Store SDK)](../sdk/store/markets.md) — Listing, resolving, and reading markets from the Store SDK
|
|
293
290
|
- [Pricing](pricing.md) — Price Lists, Price Rules, and the Pricing Context
|
|
294
|
-
- [Addresses](addresses.md) — Countries,
|
|
291
|
+
- [Addresses](addresses.md) — Countries, states, and address forms
|
|
295
292
|
- [Localization](../../api-reference/store-api/localization.md) — Locale, currency, and country headers in API requests
|
|
296
293
|
- [Translations](translations.md) — Resource and UI translations
|
|
@@ -4,8 +4,6 @@ sidebarTitle: "Media"
|
|
|
4
4
|
description: Manage product media in Spree — images, hosted and external video, named variants, focal points, and how media is uploaded, resized, and served via the Store API.
|
|
5
5
|
---
|
|
6
6
|
|
|
7
|
-
import { Since } from '/snippets/since.mdx';
|
|
8
|
-
|
|
9
7
|
## Overview
|
|
10
8
|
|
|
11
9
|
Spree handles uploads, processing, and delivery for product media. Images are automatically converted to WebP format and preprocessed into multiple sizes for optimal performance.
|
|
@@ -21,7 +19,7 @@ A media record carries:
|
|
|
21
19
|
- **Preprocessed named variants** for fast delivery
|
|
22
20
|
- **`variant_ids`** — which product variants the media represents. An empty array means it represents the product as a whole.
|
|
23
21
|
|
|
24
|
-
### Product-level Gallery
|
|
22
|
+
### Product-level Gallery
|
|
25
23
|
|
|
26
24
|
In Spree 5.5 the **product** is the default owner of media. Before 5.5, every image was pinned to a specific variant (usually the master), and sharing the same image across variants meant re-uploading the file. From 5.5 onward, an image lives on the product, and any subset of variants can reference it through `variant_ids` — without duplicating the underlying file.
|
|
27
25
|
|
|
@@ -106,7 +104,7 @@ The Store API's `media` field on a product returns its gallery — product-level
|
|
|
106
104
|
|
|
107
105
|
This dual rendering means existing storefronts keep working during the upgrade; new uploads attach to the product, and you opt into a [one-shot migration](/v5/developer/upgrades/5.4-to-5.5) to re-home legacy variant-pinned data when convenient.
|
|
108
106
|
|
|
109
|
-
### Video
|
|
107
|
+
### Video
|
|
110
108
|
|
|
111
109
|
A product gallery can hold video as well as images. Spree supports both ways merchants usually have it:
|
|
112
110
|
|
|
@@ -186,7 +184,7 @@ Where the poster comes from, in order:
|
|
|
186
184
|
|
|
187
185
|
Spree does not extract a frame from an uploaded video, so give hosted video and Vimeo links a poster if you want them to show a still.
|
|
188
186
|
|
|
189
|
-
### Focal Point
|
|
187
|
+
### Focal Point
|
|
190
188
|
|
|
191
189
|
`focal_point_x` and `focal_point_y` mark the part of an image that must stay in frame when a storefront crops it to a different shape. Both are fractions between 0 and 1, measured from the top left, so `{ x: 0.5, y: 0.5 }` is dead centre — which is also what a storefront should assume when they are null.
|
|
192
190
|
|
|
@@ -199,7 +197,7 @@ await client.products.media.update('prod_86Rf07xd4z', 'media_k5nR8xLq', {
|
|
|
199
197
|
|
|
200
198
|
Spree stores the focal point and serves it; the cropping itself is the storefront's decision, since only it knows the shape it needs.
|
|
201
199
|
|
|
202
|
-
### Named Variant Sizes
|
|
200
|
+
### Named Variant Sizes
|
|
203
201
|
|
|
204
202
|
When an image is uploaded, Spree automatically generates optimized versions in the background:
|
|
205
203
|
|
|
@@ -319,13 +317,13 @@ curl 'https://api.mystore.com/api/v3/store/products/spree-tote?expand=media,vari
|
|
|
319
317
|
| `focal_point_x` | number \| null | Horizontal focal point (0.0–1.0) |
|
|
320
318
|
| `focal_point_y` | number \| null | Vertical focal point (0.0–1.0) |
|
|
321
319
|
| `external_video_url` | string \| null | External video URL (YouTube/Vimeo) |
|
|
322
|
-
| `video_provider` | string \| null | `youtube` or `vimeo`, derived from the link
|
|
323
|
-
| `video_embed_url` | string \| null | Embeddable player URL, derived from the link
|
|
324
|
-
| `video_url` | string \| null | Uploaded video file URL
|
|
325
|
-
| `poster_url` | string \| null | Still frame for a video
|
|
320
|
+
| `video_provider` | string \| null | `youtube` or `vimeo`, derived from the link |
|
|
321
|
+
| `video_embed_url` | string \| null | Embeddable player URL, derived from the link |
|
|
322
|
+
| `video_url` | string \| null | Uploaded video file URL |
|
|
323
|
+
| `poster_url` | string \| null | Still frame for a video |
|
|
326
324
|
| `original_url` | string \| null | Full-size image URL (inline disposition) |
|
|
327
325
|
| `mini_url` ... `xlarge_url` | string \| null | Named variant URLs |
|
|
328
|
-
| `download_url` | string \| null | Same blob as `original_url` but with `Content-Disposition: attachment`. Admin API only.
|
|
326
|
+
| `download_url` | string \| null | Same blob as `original_url` but with `Content-Disposition: attachment`. Admin API only. |
|
|
329
327
|
|
|
330
328
|
## Image Processing
|
|
331
329
|
|