Skip to main content

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.

Cost is realized at fulfillment, not at allocation

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 kindMovements & COGS?Why
Standard warehouse (for example Main Warehouse)YesYou hold and ship the stock.
Amazon FBANoAmazon ships from its own warehouses.
Amazon AWDNoAmazon Warehousing & Distribution holds the units.
Walmart WFSNoWalmart fulfills the order.
Virtual (Amazon MCF)NoAn external network ships the units.
Dropship (supplier-linked)NoThe 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).

One warehouse per shipment

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 layerUnitsUnit costLayer 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.)

External warehouses bump the count without movements

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:

GuardProtects againstWhat happens
Row-locked status flipTwo workers both flipping the same shipment to FulfilledOne 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 onceThe second worker waits up to 10 seconds for the lock; if it can't get it, it skips rather than double-deduct.
Deterministic layer lockingDatabase deadlocks when many shipments drain the same product poolEvery fulfillment locks the shared cost layers in the same global order, removing the circular wait that caused deadlocks.
Voided-shipment refusalA 3PL reporting a recalled shipment as shippedThe transition is refused and logged — a voided shipment is never silently resurrected to fulfilled.
Bounded recovery loopA deadlock tearing the transaction and leaving a line under-deductedSKU.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 failureA shipment left showing Fulfilled with no movements behind itIf 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.
Why some checks report instead of block

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.

A line must be allocated before it can ship from stock

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.

ActionEffect on inventory & COGS
Void or delete the shipmentReverses 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 receiptThe 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.

Next steps

Last verified: