Skip to main content

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.
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.
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.
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.

Stock movements

Every change to stock is recorded as a movement, so “why is this number what it is” always has an answer.
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. 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.

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 marked backorderable 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.
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.
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.
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.

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.
  • Products — variants, the things stock is counted for
  • Fulfillments — how stock leaves the building
  • Orders — where allocation happens
  • Carts — checkout and reservations