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.
One quantity, one place
Section titled “One quantity, one place”A quantity lives in p2lab_stockly_warehouse_stock, keyed by warehouse + bin + product:
| Field | Meaning |
|---|---|
warehouseId | which node holds it |
binLocationId | which location inside that node |
productId | which article |
stock | how many are physically there |
reserved | how 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.
Bin roles
Section titled “Bin roles”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:
| Role | Holds | Counts as available |
|---|---|---|
receiving | goods from a purchase-order receipt, before directed put-away | yes — they are in the building |
quarantine | goods received under quality control, awaiting inspection | no |
transit | goods consolidated for an outgoing transfer | no |
returns | goods back from a customer, awaiting a disposition decision | no |
unassigned | book stock that has never been placed physically | yes |
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.
What is derived from what
Section titled “What is derived from what”product.stock = SUM(stock - reserved) over every bin whose role counts as availableproduct.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:
| Group | Fields |
|---|---|
| What | type, quantity, productId, batchId, lotId, batchNumber, expiresAt |
| From | sourceWarehouseId, sourceBinLocationId, sourceBinLpCode |
| To | targetWarehouseId, targetBinLocationId, targetBinLpCode |
| Before / after | sourceStockBefore, sourceStockAfter, sourceReservedBefore, sourceReservedAfter, targetStockBefore, targetStockAfter |
| Why | comment, event, reasonCode, meta, userId, userName |
| Which document | orderId, 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:
| Entity | Answers |
|---|---|
p2lab_stockly_warehouse_stock_batch | how much of this bin’s quantity carries which lot number and expiry — the operational truth removal draws against |
p2lab_stockly_stock_lot | the identity of a lot across the whole network |
p2lab_stockly_stock_lot_placement | where that lot currently sits, and on which license plate |
p2lab_stockly_handling_unit | a 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.
The entities this model rests on
Section titled “The entities this model rests on”| Entity | Holds |
|---|---|
p2lab_stockly_warehouse | one node: its code, its policies |
p2lab_stockly_warehouse_bin_location | one location: code, coordinates, capacity, role |
p2lab_stockly_warehouse_stock | the quantity row above |
p2lab_stockly_warehouse_stock_batch | the lot split of one such row |
p2lab_stockly_warehouse_stock_movement | the ledger |
p2lab_stockly_handling_unit | license plates |
p2lab_stockly_stock_lot / …_stock_lot_placement | lot identity and where it sits |
p2lab_stockly_reservation_line | what an order claimed, per location |
p2lab_stockly_order_allocation | what an order is owed and where it is sourced from |
p2lab_stockly_external_stock_incident | foreign 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.
- Putting stock in a bin — the four write endpoints
- Reading stock — the queries that already exist
- Movement history — the same ledger, as the operator sees it