How fulfilling moves inventory and realizes COGS
Shipping a sales order is the moment committed stock actually leaves the warehouse — and the moment its cost becomes COGS (cost of goods sold, the cost of the specific units you shipped). Everything before it — approving, reserving, allocating — only earmarks stock and books no cost. This page explains what happens to inventory and cost the instant a shipment is marked fulfilled, and how SKU.io keeps that math correct when two shipping signals arrive at once.
For the hands-on steps of shipping an order, see Fulfill a sales order. This page is the "what happens and why" behind that button.
Allocating stock to a line reserves it but books no cost — the FIFO layers are untouched. Only when a shipment reaches Fulfilled does SKU.io draw down those layers and turn their cost into COGS. See Inventory allocation and backorders for the earmarking model that precedes this step.
The chain from shipment to COGS
When a shipment (a SalesOrderFulfillment, or SOF) reaches the Fulfilled status on a warehouse that holds real stock, SKU.io runs one deduction sequence per shipped line — consuming cost layers, writing the movements that carry their cost, advancing each line's shipped count, and settling the reservations behind it:
The whole sequence runs inside a single database transaction, so a mid-way failure rolls back as a unit rather than leaving a half-shipped line. The sections below walk each stage.
What triggers the deduction — and which warehouses skip it
The deduction fires only when two conditions hold: the shipment's status becomes Fulfilled, and its warehouse actually manages physical inventory. Warehouses that don't hold your stock never write movements or COGS — the units were shipped by someone else, so SKU.io only records that they shipped by bumping the line's fulfilled quantity directly:
| Warehouse kind | Movements & COGS? | Why |
|---|---|---|
| Standard warehouse (for example Main Warehouse) | Yes | You hold and ship the stock. |
| Amazon FBA | No | Amazon ships from its own warehouses. |
| Amazon AWD | No | Amazon Warehousing & Distribution holds the units. |
| Walmart WFS | No | Walmart fulfills the order. |
| Virtual (Amazon MCF) | No | An external network ships the units. |
| Dropship (supplier-linked) | No | The supplier ships direct to the customer. |
For those external kinds, the units are tracked as externally fulfilled and no local cost is booked. See Amazon FBA, MCF, and externally fulfilled orders for that path, and Two-tier fulfillment for the difference between a plan (FulfillmentOrder) and an executed shipment (SOF).
Each shipment ships from exactly one warehouse, and its warehouse is frozen once it has movements. If an order draws from two warehouses, it produces two shipments — each with its own deduction. See Warehouse routing and the warehouse lock.
FIFO consumption: Which units, at what cost
For a stock-holding warehouse, each shipped line consumes FIFO cost layers — the priced batches of units created when you received the stock. FIFO stands for First In, First Out: the oldest layer is drawn down first, so the cost of your oldest stock becomes the cost of this sale.
SKU.io walks the layers for the line's product and warehouse in oldest-first order and takes what the line needs, layer by layer, until the shipped quantity is covered. A couple of refinements shape which layer a line may take from:
- Tight-coverage protection. If another line has pinned a specific incoming batch to itself (a tight-coverage claim), this line's walk is capped so it can't drain the units that other line is counting on.
- Lot / expiry override. For expiry-tracked products, the walk consumes the soonest-to-expire layer first (FEFO — First Expired, First Out). If you confirmed specific lots or source bins in the pick step, those exact layers and bins are consumed instead of the automatic walk. A saved lot override that no longer matches available stock is dropped and the line falls back to the automatic FEFO/FIFO walk — an out-of-date pick never blocks the ship.
A worked FIFO-to-COGS example
Suppose Main Warehouse received the Stainless Water Bottle 750ml on two purchase orders at different costs, then order SO-DOCS-0001 for Blue Bottle Retail ships 150 units:
| Cost layer | Units | Unit cost | Layer value |
|---|---|---|---|
| Layer 1 (older) | 100 | $8.50 | $850.00 |
| Layer 2 (newer) | 100 | $9.00 | $900.00 |
FIFO draws from the oldest layer first:
- 100 units from Layer 1 at $8.50 = $850.00
- 50 units from Layer 2 at $9.00 = $450.00
- COGS for this shipment = $850.00 + $450.00 = $1,300.00
After the shipment, Layer 1 is fully consumed and Layer 2 has 50 units left at $9.00 — the cost basis for the next sale of that product.
Negative movements: The COGS basis
For every batch a line consumes, SKU.io writes a negative inventory movement — the audit record of stock leaving the warehouse. Where a receipt movement is positive, a fulfillment movement is negative, and it carries the cost of the exact layer it drew from. That cost, summed across a line's movements, is the line's COGS, and it flows into the order's profit figures and its accounting entry. See How order financials flow to accounting.
You can audit every one of these on the order's Movements tab. Each fulfillment movement shows a negative quantity, the cost layer it consumed, and a Source that links back to the shipment line that created it. The order's Profit tab rolls the same costs into per-line and per-order margin.
As each line's movements are written, its fulfilled quantity rises by the shipped amount. This is what advances the line — and, once every line is fully shipped, the whole order — toward a fulfilled state. (A sales order line has no status column of its own; its state is derived entirely from these quantities. See How sales order status works.)
For FBA, AWD, WFS, MCF, and dropship warehouses no movement is written, so the fulfilled quantity is incremented directly rather than as a side effect of the deduction. Doing it in exactly one place per warehouse kind is what stops a unit being counted twice.
Allocations settle, and freed stock re-releases
Approving an order created allocations — the earmark rows that hold stock against each line. As a line ships, its Allocated allocations flip to Fulfilled, closing out the reservation now that the stock has physically moved.
One deliberate side effect closes the loop with the backorder queue. If this shipment consumed FIFO that a different order's allocation was relying on — which can happen when a line is shipped without a prior reservation, such as a historical channel sync — that other allocation is now unbacked. SKU.io detects it and releases it back to backorder, so the queue reflects the true remaining supply rather than a claim on stock that's already gone. See Inventory allocation and backorders for how the queue then re-allocates freed stock FIFO.
Concurrent fulfillment: How the math stays right
Shipping signals rarely arrive one at a time. A shipping provider webhook, a marketplace webhook, and a polling job can all report the same shipment as shipped within the same second — and without guards, each would run the deduction again, double-counting movements and draining layers twice. SKU.io layers several protections around the deduction so it runs exactly once and never tears:
| Guard | Protects against | What happens |
|---|---|---|
| Row-locked status flip | Two workers both flipping the same shipment to Fulfilled | One caller wins and runs the side effects once; the loser sees the row already fulfilled and returns without re-deducting. |
| Named database lock (10-second wait) | A shipping-provider webhook and a marketplace webhook deducting the same shipment at once | The second worker waits up to 10 seconds for the lock; if it can't get it, it skips rather than double-deduct. |
| Deterministic layer locking | Database deadlocks when many shipments drain the same product pool | Every fulfillment locks the shared cost layers in the same global order, removing the circular wait that caused deadlocks. |
| Voided-shipment refusal | A 3PL reporting a recalled shipment as shipped | The transition is refused and logged — a voided shipment is never silently resurrected to fulfilled. |
| Bounded recovery loop | A deadlock tearing the transaction and leaving a line under-deducted | SKU.io re-reads the committed result and backfills the exact missing quantity (up to three passes); a line that already persisted is never double-deducted. |
| Revert-to-submitted on failure | A shipment left showing Fulfilled with no movements behind it | If the deduction fails and nothing rolls it back, the shipment drops back to a submitted state so it can be retried — never shipped-with-no-COGS. |
Every one of these events represents something that already happened in the physical world — a box that already left the warehouse. So at the automatic shipping chokepoint SKU.io reports a shortfall (a critical log entry plus a note on the order) rather than throwing, because failing the deduction would only widen the gap between the books and reality. Manual paths where you can still stop — for example marking items for in-store pickup — validate up front and block instead.
SKU.io refuses to fulfill a stock-warehouse line that has no Allocated stock behind it — the ship path raises an insufficient-allocation error (surfaced in the UI as a shortfall list and an order note) rather than inventing cost from nothing. Allocate the line first; see Approve, reserve, and allocate stock.
Reversing a shipment restores layers and COGS
Because fulfilling draws down real cost layers, undoing it has to unwind them just as carefully. Voiding or deleting a shipment reverses its inventory movements and restores the FIFO layers it consumed, releasing that cost back out of COGS — the mirror image of the deduction chain above. A credit note on its own doesn't restock: money can flow back to a customer without any stock physically returning. Physical stock comes back only through a return.
| Action | Effect on inventory & COGS |
|---|---|
| Void or delete the shipment | Reverses its movements and restores the consumed FIFO layers; the units become available again. |
| Sales credit (credit note) | Posts money back only — no restock. |
| RMA → return receipt | The physical path: received units are restocked per their disposition. |
See Void, restore, or reset a shipment for the void-versus-delete distinction and the provider-shipped pre-check, and Returns, credits, and refunds for which return step restocks stock versus posts money.