Reading stock
Most integrations that get stock wrong do not get the write wrong. They re-derive a figure the module already publishes, in a slightly different way, and then disagree with the admin about it.
Everything here is a read. p2lab_stockly.viewer or the entity’s own :read privilege is enough.
One product, everywhere
Section titled “One product, everywhere”
GET warehouse/stock/product/{productId}?page=1&limit=10ACL:p2lab_stockly_warehouse_stock:read· at most 100 rows per page
One row per (warehouse, bin), plus the reorder rules and the purchasable figure that belong to the whole warehouse rather than to the page.
{ "data": [ { "id": "0191f0…", "productId": "0191f3…", "warehouseId": "0191f1…", "warehouseName": "Main warehouse", "binLocationId": "0191f2…", "binLocationCode": "A-01-02", "binRole": null, "stock": 24, "reserved": 6, "available": 18 } ], "rules": {}, "purchasable": {}, "total": 3, "page": 1, "limit": 10}binRole: null is an ordinary storage bin. Any other value is a buffer; see
the stock model for what each one excludes.
One product, one number each
Section titled “One product, one number each”
GET warehouse/stock/product/{productId}/summary?withVariants=1ACL:p2lab_stockly_warehouse_stock:read
The figures behind the product page’s tiles. withVariants=1 makes a parent aggregate its variants.
{ "data": { "total": 24, "reserved": 6, "available": 17, "backordered": 1, "pending": 0, "warehouses": 2, "expiring": 0, "inTransit": 0, "onOrder": 40 }}| Field | Means |
|---|---|
total | the raw physical count, buffers included |
reserved | how much orders have claimed |
available | what can still be sold |
backordered | demand no shelf covers |
pending | units in the receiving buffer or in transit — on their way in |
expiring | lots with an expiry within 30 days |
inTransit, onOrder | owed by another warehouse / by a supplier |
One product, one warehouse
Section titled “One product, one warehouse”
GET warehouse/{warehouseId}/product/{productId}/availableACL:p2lab_stockly_warehouse_stock:read
Answers {"available": 12} and nothing else. Use it for a scan check or a gate, where loading the
rows and summing them locally is extra work for the same answer.
Who holds the reserved units
Section titled “Who holds the reserved units”
GET warehouse/stock/product/{productId}/reservations?grain=order&page=1&limit=10ACL:p2lab_stockly_warehouse_stock:read
grain | One row per |
|---|---|
order | order (the default) |
bin | warehouse + bin + license plate + order |
backorder | order still owed goods |
The response carries data, total, totalReserved, totalBackordered, page, limit and the
grain it answered. An unknown grain is a 400, not a silent fallback.
These rows come from the reservation lines, the same figures the admin’s Reserved tile sums: a list and a tile that disagree is the bug this endpoint exists to prevent.
Lots, batches and placements
Section titled “Lots, batches and placements”| Method | Path | Answers |
|---|---|---|
GET | warehouse/stock/batches/{productId} | every lot of the product, with expiry |
GET | warehouse/stock/{warehouseId}/batches/{productId} | the same, in one warehouse |
GET | warehouse/stock/placements/{productId} | where its lots physically sit |
GET | warehouse/stock/{warehouseId}/{binLocationId}/placements/{productId} | the placements in one bin |
GET | warehouse/stock/{warehouseId}/loose-bins/{productId} | bins holding it loose, off any plate |
GET | warehouse/{warehouseId}/stock/pickable-source/{productId} | placements a pick may draw from |
GET | warehouse/stock/product/{productId}/batch-history | how its lots changed over time |
GET | warehouse/batch-trace/{batchNumber} | where one lot number went, across the network |
GET | warehouse/order-lots/{orderId} | which lots an order shipped |
The last two are what a recall question actually needs: they answer “who received this lot” without a join written by hand.
Movements
Section titled “Movements”
GET|POST warehouse/stock/movements/{productId}ACL:p2lab_stockly_warehouse_stock_movement:read
The ledger for one product. POST takes a filter body for the cases a query string cannot express.
For anything report-shaped, the ledger is also an ordinary entity:
/api/search/p2lab-stockly-warehouse-stock-movement with custom filters and aggregations is
often less work than paging this endpoint.
Orders
Section titled “Orders”| Method | Path | Answers | ACL |
|---|---|---|---|
GET | warehouse/order/{orderId}/allocations | per-line sourcing picture; add ?warehouseId= for the “outstanding elsewhere” verdict | p2lab_stockly.viewer |
GET | warehouse/order/{orderId}/stock-targets | which node each line is booked against | p2lab_stockly_warehouse_stock:read |
GET | warehouse/order/{orderId}/pick-plan | where a picker would be sent | p2lab_stockly_warehouse_stock:read |
pick-plan is a proposal and writes nothing, which makes it useful for showing a plan before anybody
walks.
POST warehouse/{warehouseId}/bin-optionsACL:p2lab_stockly.viewer
One page of bin picker options with their flags and ordering, assembled server-side. It exists because the alternative, pulling every bin and every stock row of the warehouse to the client and sorting there, is how a bin picker becomes unusable in a real warehouse.
Related: POST warehouse/{warehouseId}/bin-occupancy for how full bins are, and
POST warehouse/{warehouseId}/suggest-bin for where a put-away would go.
When to use plain DAL instead
Section titled “When to use plain DAL instead”Everything is a Shopware entity, and /api/search/… is the right tool for reporting, exports and any
question with custom filters. What is lost is what these endpoints add on top: availability
arithmetic that matches product.stock row for row, reservation figures taken from the lines rather
than from the cache, and variant aggregation.
Two rules carry over from the packing API and apply just as much here:
Null is not zero. An absent value means nobody recorded one: an expiry of null is a lot with
no shelf life on record, not one that expired.
Do not re-derive a published figure. If a number has an endpoint, read the endpoint. Every locally re-implemented “available” in the history of this module has eventually disagreed with the one the shop sells against.
- Putting stock in a bin — the write side
- Corrections and bulk jobs
- Events — being told instead of asking