Overview
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. 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.The five numbers
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.
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; marking a purchase order ordered or a transfer in transit adds to incoming, and receiving, closing, cancelling or reopening the document takes the remainder back off. Nothing else writes them. If a figure ever looks wrong, one task 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.
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.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.Stock locations
A stock location is somewhere stock physically sits: a warehouse, a shop, a third-party fulfillment centre.Reading and adjusting stock
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.Stock movements
Every change to stock is recorded as a movement, so “why is this number what it is” always has an answer.What a shopper sees
For a storefront, all of the above collapses into one question, and the answer is already computed: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.
Reservations during checkout
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. A reservation is a short, time-limited hold placed when a customer enters checkout. Availability drops for everyone else immediately.
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.
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.
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.
Backorders
A stock level markedbackorderable can be sold past zero. Orders are accepted, allocated, and wait for stock.
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.
Moving stock between locations
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. That is four steps, not one. A transfer is planned as a draft, packed, sent, and finally counted in:
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.
Buying stock in
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.
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.
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.
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.
Suppliers themselves are an address book, kept per store:
Counting a delivery in
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.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.
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.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.
The Inventory page
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. 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.Turning tracking off
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. This is a store-level setting, so a store that sells only digital goods needn’t manage stock at all.Syncing from an external system
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. Feeds come in through the bulk update above; live checks happen only at decision moments, like completing an order. See Providers.Related
- Products — variants, the things stock is counted for
- Fulfillments — how stock leaves the building
- Orders — where allocation happens
- Carts — checkout and reservations

