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 value | UI label | Sign | Emitted by | FIFO effect |
|---|---|---|---|---|
purchase_receipt | Inbound Receipt | + | Receiving an inbound shipment (InboundShipmentReceipt) | Creates a new FIFO layer at the received cost |
sale | Sale | − | Shipping a sales order fulfillment (SalesOrderLineInventoryManager) | Consumes FIFO layers oldest-first; realizes COGS |
adjustment | Adjustment | ± | An inventory adjustment (InventoryAdjustmentManager) | Increases create a layer; decreases consume oldest-first |
stock_take | Stock Take | ± | Finalizing a stock take (StockTakeManager) | Reconciliation delta; same layer behavior as an adjustment |
transfer | Transfer | ± | A warehouse transfer (WarehouseTransferManager) | Uses paired in_transit and active movements; carries cost between warehouses |
reclassification | Reclassification | ± | A location transfer within one warehouse (LocationTransferRepository) | Paired active movements that move a layer between bins |
return | Return | + | Receiving an RMA (ReturnReceiptLine) | Returns stock to a layer |
purchase_return | Purchase Return | − | A vendor credit shipping stock back to a supplier (VendorCredit*) | Consumes layers |
manufacturing_consumption | Manufacturing Consumption | − | A manufacturing order consuming components (RecordProductionAction) | Consumes component layers |
manufacturing_output | Manufacturing Output | + | A manufacturing order producing finished goods | Creates a finished-good layer at rolled-up cost |
manufacturing_scrap | Manufacturing Scrap | − | A manufacturing order scrapping material | Consumes layers |
assembly | Assembly | ± | Legacy assembly flow (InventoryAssemblyLine) | Legacy; superseded by the manufacturing_* types |
assembly is legacyThe 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 warehouseA 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_status | UI label | Chip color | What it means |
|---|---|---|---|
active | Active | Green (success) | Physically on hand in a warehouse. Counts toward availability and feeds the per-location on-hand cache. |
in_transit | In Transit | Blue (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.
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.
Source link: link_type + link_id
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 exampleApp\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.
Navigable parents: parent_link_id and detail_link_id
The raw link points at a line, but you usually want to open the document. Two computed accessors do that walk for you:
| Field | What it resolves to |
|---|---|
parent_link_id | The 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_id | The 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.
The reference cache and the null-link guard
referenceis 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_typepoints at a class that no longer exists, thelinkrelationship resolves tonullinstead of throwing. The movement, its type, and its cachedreferencestill 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.
| Column | Type | What it holds |
|---|---|---|
id | integer | The movement's unique id. |
inventory_movement_date | datetime | The effective date/time of the stock change (drives valuation, not created_at). |
product_id | integer | The stocked product this movement is for. |
quantity | float | Signed quantity — positive adds stock, negative removes it. |
type | string | One of the type taxonomy values. |
inventory_status | string | active or in_transit. |
warehouse_id | integer | The warehouse the change occurred in. |
warehouse_location_id | integer | The bin/location within the warehouse. |
fifo_layer_id | integer, nullable | The FIFO layer this movement created or consumed, if any. See FIFO layer fields, statuses & enums. |
link_id / link_type | integer / string | The polymorphic source link. |
reference | string, nullable | Cached source document reference (PO/SO number, etc.). |
unit_cost | float, nullable | Internal per-unit cost snapshot (not in the resource). |
cost_version | integer, nullable | Internal cost-version stamp paired with unit_cost (not in the resource). |
batch_number | string, nullable | Lot/batch captured when the inbound was received against a batch. |
expiry_date | date, nullable | Expiry captured for a batch. |
split_from_id | integer, nullable | Links a split-off consumption movement back to the movement it was split from. |
created_at / updated_at | datetime | Row timestamps. |
API resource fields
The InventoryMovementResource enriches those columns with the joined product, warehouse, location, and layer detail returned to the UI:
| Resource field | Contents |
|---|---|
id, inventory_movement_date, product_id, inventory_status, quantity, reference, created_at, updated_at | Direct from the columns above. |
sku, barcode, product_name, stock_uom_code | Pulled from the product (stock unit of measure code). |
type | An 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_id | The 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.
| Column | Sortable | Notes |
|---|---|---|
| ID | Yes | The movement id. |
| Date | Yes | inventory_movement_date, in your local timezone. |
| SKU | Yes | The product SKU. |
| Product Name | Yes | The product name. |
| Type | Yes | The friendly type label. |
| Status | Yes | Active (green) or In Transit (blue). |
| Quantity | Yes | Right-aligned and colored by sign — green for positive, red for negative. |
| Warehouse | No | The warehouse name. |
| Location | No | The bin/location. |
| Layer | No | A purple FIFO Layer #N chip linking to /inventory/fifo-layers/{id}; empty when the movement has no layer. See Browse & manage FIFO layers. |
| Source | No | A friendly link to the parent document, with a mandatory tooltip showing the raw link_type and link_id. |
| Created | Yes | Row creation timestamp. |
| Updated | Yes | Row update timestamp. |
Export-only columns (available in the CSV/XLSX export, not shown as list columns):
| Export column | Value |
|---|---|
| Source | The class basename plus id, for example SalesOrderLine #4821. |
| Link Type | The link_type class basename. |
| Link ID | The raw link_id. |
| FIFO Layer ID | The 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.
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.
Related edge behaviors worth knowing
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
- The inventory movement ledger — how the ledger works as an audit trail.
- View & filter the inventory movement ledger — filter by type, status, warehouse, and source.
- Stock states: on-hand, available, allocated, on-hold, non-sellable, incoming & in-transit — how movement status rolls up into the stock states.
- FIFO layers & how COGS is realized — how
+/−movements create and consume layers. - Stock states & fields reference — the companion field lookup for stock states.