Skip to content

The stock model

Everything in the warehouse half of Stockly rests on one row shape. Once it is understood, the endpoints stop looking like an arbitrary list.

A quantity lives in p2lab_stockly_warehouse_stock, keyed by warehouse + bin + product:

FieldMeaning
warehouseIdwhich node holds it
binLocationIdwhich location inside that node
productIdwhich article
stockhow many are physically there
reservedhow many of them an order has already claimed

There is no such thing as stock without a location. Goods placed “nowhere” land on the warehouse’s unassigned bin, a real row with a real id, created on first use. Every API call that takes binLocationId: null resolves it to that bin before doing anything else, which is why a removal from “no bin” draws from exactly the same place a null add put it.

reserved is a cache. The source of truth for what an order claimed is p2lab_stockly_reservation_line, one row per order line and location. Writing reserved without writing the lines puts the two out of step, and the integrity check has a finding for precisely that.

A bin with no role is an ordinary storage bin, the only kind put-away targets. The five roles below are buffers, at most one of each per warehouse:

RoleHoldsCounts as available
receivinggoods from a purchase-order receipt, before directed put-awayyes — they are in the building
quarantinegoods received under quality control, awaiting inspectionno
transitgoods consolidated for an outgoing transferno
returnsgoods back from a customer, awaiting a disposition decisionno
unassignedbook stock that has never been placed physicallyyes

One of them is closed to ordinary callers. The transit bin fills when a transfer ships and empties when it arrives or is cancelled; an add, remove or move naming it as either end is refused, because a hand-placed unit there would strand the transfer that thinks it owns the goods.

returns is deliberately not the same pool as quarantine, and the difference matters to an integration: goods in returns are not sellable. A listener that treats a received return as “stock is back” will oversell; availability changes only when somebody dispositions the units back to storage.

product.stock = SUM(stock - reserved) over every bin whose role counts as available

product.stock is an output. Stockly recalculates it after every operation and overwrites whatever it finds there. Writing it from outside is not an integration path; it is the failure mode the external-write guard exists to catch.

The ledger works the other way round: p2lab_stockly_warehouse_stock_movement is append-only. Nothing edits a movement row; a correction is a new row with its own type. That is what makes the history answerable: the question of why a bin holds four has an answer made of rows, not of the current value.

A movement carries both ends and both balances:

GroupFields
Whattype, quantity, productId, batchId, lotId, batchNumber, expiresAt
FromsourceWarehouseId, sourceBinLocationId, sourceBinLpCode
TotargetWarehouseId, targetBinLocationId, targetBinLpCode
Before / aftersourceStockBefore, sourceStockAfter, sourceReservedBefore, sourceReservedAfter, targetStockBefore, targetStockAfter
Whycomment, event, reasonCode, meta, userId, userName
Which documentorderId, orderLineItemId, purchaseOrderId, purchaseOrderItemId, transferId, operationId, importId

Direction is in the ends, never in the sign: an arrival has no source, a removal has no target, a move has both. quantity is always positive.

Batches, lots, placements and license plates

Section titled “Batches, lots, placements and license plates”

Four names that sound alike and answer different questions:

EntityAnswers
p2lab_stockly_warehouse_stock_batchhow much of this bin’s quantity carries which lot number and expiry — the operational truth removal draws against
p2lab_stockly_stock_lotthe identity of a lot across the whole network
p2lab_stockly_stock_lot_placementwhere that lot currently sits, and on which license plate
p2lab_stockly_handling_unita license plate: a pallet or carton goods travel on, which may be nested inside another

Batches decide what a removal consumes. Lots and placements are a mirror maintained alongside them, which is what lets a lot be traced across bins and warehouses with one query. An operation writes both; no caller has to.

EntityHolds
p2lab_stockly_warehouseone node: its code, its policies
p2lab_stockly_warehouse_bin_locationone location: code, coordinates, capacity, role
p2lab_stockly_warehouse_stockthe quantity row above
p2lab_stockly_warehouse_stock_batchthe lot split of one such row
p2lab_stockly_warehouse_stock_movementthe ledger
p2lab_stockly_handling_unitlicense plates
p2lab_stockly_stock_lot / …_stock_lot_placementlot identity and where it sits
p2lab_stockly_reservation_linewhat an order claimed, per location
p2lab_stockly_order_allocationwhat an order is owed and where it is sourced from
p2lab_stockly_external_stock_incidentforeign writes to product.stock the guard caught

They are ordinary Shopware entities, so /api/search/p2lab-stockly-warehouse-stock and friends work exactly as they do anywhere else.