Skip to main content

Movement types & fields reference

Every change to your owned stock is written to the inventory movement ledger as an inventory movement — a single, immutable + or line stamped with a type, a status, a source document, and (for stocked changes) a FIFO layer. This page is the field-level lookup for that record: the full type taxonomy, the two inventory statuses, the polymorphic source link, and every field and list column, plus the three invariants that decide which movements can exist at all.

For how movements behave as an audit trail, read the concept The inventory movement ledger. To view and filter them in the app, see View & filter the inventory movement ledger. For how movements feed cost, see FIFO layers & how COGS is realized.

Movement type taxonomy

The type field records which subsystem created the movement and what it means. Each type is emitted by exactly one part of the app, so the type tells you where a stock change came from without opening the source document. The table below lists every type SKU.io writes, the label shown in the Type column and filter, its typical sign, the subsystem that emits it, and its effect on FIFO layers.

type valueUI labelSignEmitted byFIFO effect
purchase_receiptInbound Receipt+Receiving an inbound shipment (InboundShipmentReceipt)Creates a new FIFO layer at the received cost
saleSaleShipping a sales order fulfillment (SalesOrderLineInventoryManager)Consumes FIFO layers oldest-first; realizes COGS
adjustmentAdjustment±An inventory adjustment (InventoryAdjustmentManager)Increases create a layer; decreases consume oldest-first
stock_takeStock Take±Finalizing a stock take (StockTakeManager)Reconciliation delta; same layer behavior as an adjustment
transferTransfer±A warehouse transfer (WarehouseTransferManager)Uses paired in_transit and active movements; carries cost between warehouses
reclassificationReclassification±A location transfer within one warehouse (LocationTransferRepository)Paired active movements that move a layer between bins
returnReturn+Receiving an RMA (ReturnReceiptLine)Returns stock to a layer
purchase_returnPurchase ReturnA vendor credit shipping stock back to a supplier (VendorCredit*)Consumes layers
manufacturing_consumptionManufacturing ConsumptionA manufacturing order consuming components (RecordProductionAction)Consumes component layers
manufacturing_outputManufacturing Output+A manufacturing order producing finished goodsCreates a finished-good layer at rolled-up cost
manufacturing_scrapManufacturing ScrapA manufacturing order scrapping materialConsumes layers
assemblyAssembly±Legacy assembly flow (InventoryAssemblyLine)Legacy; superseded by the manufacturing_* types
assembly is legacy

The assembly type is retained for historical rows only. New builds emit manufacturing_consumption, manufacturing_output, and manufacturing_scrap. In the frontend, legacy assembly source links are bucketed under the manufacturing_order name for filter parity, but they still route to the assembly, not to a non-existent manufacturing order.

reclassification never crosses a warehouse

A reclassification movement is emitted only by a location transfer — moving stock between bins inside one warehouse. A move between warehouses is a transfer and uses the in_transit/active status pair described below. See Move stock between bins (location transfers) and How warehouse transfers work.

Inventory status: active vs in_transit

The inventory_status field has exactly two values. It's separate from the movement type: type says what happened, status says whether the stock is available to work with right now.

inventory_statusUI labelChip colorWhat it means
activeActiveGreen (success)Physically on hand in a warehouse. Counts toward availability and feeds the per-location on-hand cache.
in_transitIn TransitBlue (info)Owned but on a warehouse-to-warehouse transfer. Counted in your owned totals and valuation, but not available to allocate or ship.

Both statuses are included in inventory valuation, because in-transit stock is still owned. The difference is availability: only active movements that carry a location participate in the location-quantity cache, and only active stock feeds the in-warehouse availability figure. In-transit movements are excluded from availability but included in the owned total. This is the ledger-level version of the In Transit stock state.

There is no "reserved" status

Only active and in_transit exist. There is no reserved inventory status anywhere in the product. Allocation and holds are tracked separately from movement status — allocated stock and on-hold stock are their own states (the product list column for held stock is labelled On Hold, not "Reserved"). See Allocations & backorders and Place & manage stock holds.

Every movement carries a polymorphic source link back to the exact document line that created it — a SalesOrderLine, an InboundShipmentReceiptLine, a WarehouseTransferShipmentLine, a StockTakeItem, an InventoryAdjustment, a ReturnReceiptLine, a VendorCreditLine, an MoOutputLine/MoComponentLine, and more. Two columns store it:

  • link_type — the source model's class name (for example App\Models\SalesOrderLine).
  • link_id — that record's id.

In the Source column the app resolves this pair into a friendly link to the parent document, with a tooltip that always shows the raw link_type and link_id so the underlying record is never ambiguous.

The raw link points at a line, but you usually want to open the document. Two computed accessors do that walk for you:

FieldWhat it resolves to
parent_link_idThe top-level document id — the sales order behind a SalesOrderLine, the purchase order behind an InboundShipmentReceiptLine, the transfer behind a WarehouseTransferShipmentLine, the stock take behind a StockTakeItem, the manufacturing order behind an MoComponentLine, and more.
detail_link_idThe immediate document id — the receipt behind an InboundShipmentReceiptLine, the shipment receipt behind a WarehouseTransferShipmentReceiptLine, or the fulfillment behind a SalesOrderFulfillmentLine.

Fulfillment-line and inbound-receipt-line movements route to their fulfillment or receipt detail using detail_link_id; most others route to the top-level document using parent_link_id.

  • reference is a cached copy of the source document's own reference string (for example the PO or SO number), captured from the link when the movement is saved. It's denormalized so the ledger reads fast without joining every source table, and it refreshes on save.
  • If a movement's link_type points at a class that no longer exists, the link relationship resolves to null instead of throwing. The movement, its type, and its cached reference still display; only the live navigable link is suppressed.

Movement field reference

A movement stores these columns. The valuation fields unit_cost and cost_version are internal snapshots and are never exposed in the standard API resource.

ColumnTypeWhat it holds
idintegerThe movement's unique id.
inventory_movement_datedatetimeThe effective date/time of the stock change (drives valuation, not created_at).
product_idintegerThe stocked product this movement is for.
quantityfloatSigned quantity — positive adds stock, negative removes it.
typestringOne of the type taxonomy values.
inventory_statusstringactive or in_transit.
warehouse_idintegerThe warehouse the change occurred in.
warehouse_location_idintegerThe bin/location within the warehouse.
fifo_layer_idinteger, nullableThe FIFO layer this movement created or consumed, if any. See FIFO layer fields, statuses & enums.
link_id / link_typeinteger / stringThe polymorphic source link.
referencestring, nullableCached source document reference (PO/SO number, etc.).
unit_costfloat, nullableInternal per-unit cost snapshot (not in the resource).
cost_versioninteger, nullableInternal cost-version stamp paired with unit_cost (not in the resource).
batch_numberstring, nullableLot/batch captured when the inbound was received against a batch.
expiry_datedate, nullableExpiry captured for a batch.
split_from_idinteger, nullableLinks a split-off consumption movement back to the movement it was split from.
created_at / updated_atdatetimeRow timestamps.

API resource fields

The InventoryMovementResource enriches those columns with the joined product, warehouse, location, and layer detail returned to the UI:

Resource fieldContents
id, inventory_movement_date, product_id, inventory_status, quantity, reference, created_at, updated_atDirect from the columns above.
sku, barcode, product_name, stock_uom_codePulled from the product (stock unit of measure code).
typeAn object: name (the type value), link_type, link_id, parent_link_id, detail_link_id.
warehouse{ id, name } for the warehouse.
warehouse_location{ id, code, name, type, type_label }, or null when no location is set.
layer{ fifo_layer_id, name } when the movement touches a FIFO layer, otherwise null.
fifo_layer_idThe layer id, surfaced flat for convenience.

Movements list columns

The movement ledger list shows these columns. Some are sortable, some link elsewhere, and a few exist only in the export.

ColumnSortableNotes
IDYesThe movement id.
DateYesinventory_movement_date, in your local timezone.
SKUYesThe product SKU.
Product NameYesThe product name.
TypeYesThe friendly type label.
StatusYesActive (green) or In Transit (blue).
QuantityYesRight-aligned and colored by sign — green for positive, red for negative.
WarehouseNoThe warehouse name.
LocationNoThe bin/location.
LayerNoA purple FIFO Layer #N chip linking to /inventory/fifo-layers/{id}; empty when the movement has no layer. See Browse & manage FIFO layers.
SourceNoA friendly link to the parent document, with a mandatory tooltip showing the raw link_type and link_id.
CreatedYesRow creation timestamp.
UpdatedYesRow update timestamp.

Export-only columns (available in the CSV/XLSX export, not shown as list columns):

Export columnValue
SourceThe class basename plus id, for example SalesOrderLine #4821.
Link TypeThe link_type class basename.
Link IDThe raw link_id.
FIFO Layer IDThe fifo_layer_id.

Guardrails: Which movements can exist

Three invariants are enforced whenever a movement is saved. They can't be worked around through the normal event-firing save path, and understanding them explains several "why can't I…" cases.

Bundle and matrix products carry no movements

A movement can only be recorded against a stockable product — a simple product or a variant. Saving a movement against a bundle or a matrix parent throws InventoryMovementTypeException, because those are non-stocked abstractions rather than physical stock.

  • Selling a bundle expands to movements on its component products, never a single bundle movement.
  • A matrix parent never appears in the ledger; only its variants do.

Supplier warehouses carry no movements

A supplier (dropship/vendor) warehouse isn't an owned stock location, so no movement may post to it — the save throws SupplierWarehouseCantHaveInventoryMovementsException. This guards against accidentally receiving or allocating owned stock into a supplier location.

Cross-product FIFO contamination is blocked

A movement can never point at a FIFO layer that belongs to a different product. If fifo_layer_id's layer has a different product_id than the movement, the save throws — the invariant that keeps per-product costing correct.

Bulk paths set cost explicitly

The cross-product check (and the automatic cost snapshot) run on the event-firing save/create path. High-volume bulk insert/upsert paths bypass Eloquent events by design and instead set unit_cost and validate layer ownership explicitly at the choke point, so the same invariant holds without firing per-row events.

These aren't movement guardrails, but they affect what you see in the ledger:

  • On Hand reconciles from the parts. On Hand = Available + Allocated + On Hold + Non-Sellable, which equals your owned total minus In Transit. Non-Sellable bin stock is a first-class column, not a hidden bucket. See Stock states & fields reference.
  • Empty FIFO layers can be archived. A fully consumed (zero-remaining) layer is archivable — its consuming movements stay in the ledger. See Browse & manage FIFO layers.
  • Channel-managed transfers are received by the channel. Manually receiving a transfer whose destination is an Amazon FBA/AWD/WFS-managed warehouse is rejected with a clear message and isn't offered in the UI — those movements are written when the channel confirms receipt. See Receive a transfer.

Next steps

Last verified: