Corrections and bulk jobs
The four write endpoints book one movement each. What follows books many, or books a difference rather than a quantity, or does not finish inside the request at all.
Counting
Section titled “Counting”
POST warehouse/stock/countACL:p2lab_stockly.execute_stock_operations
Sets locations to what somebody counted. The endpoint takes a whole sheet, not one line:
{ "warehouseId": "0191f1…", "level": "aggregate", "comment": "Cycle count, aisle A", "lines": [ { "productId": "0191f3…", "binLocationId": "0191f2…", "countedQty": 22 }, { "productId": "0191f8…", "binLocationId": "0191f2…", "countedQty": 0 } ]}level is aggregate (the bin’s total for that product) or batch. At batch level each line may
also carry batchId, batchNumber and expiresAt.
The answer reports what each line actually changed:
{ "success": true, "results": [ { "productId": "0191f3…", "binLocationId": "0191f2…", "batchId": null, "delta": -2 }] }Only the difference is booked, as a count movement. A line whose counted figure already matches
books nothing and reports delta: 0. A count of zero is legitimate, because an empty shelf has to be
bookable, but a negative one is refused with P2LAB_STOCKLY__STOCK__NEGATIVE_COUNT: a physical
count has no direction.
The system quantity is re-read when the correction is applied, not when the count was taken, so the result equals the counted figure however much drifted in between.
Correcting a lot
Section titled “Correcting a lot”
POST warehouse/stock/batch/{batchId}/correctACL:p2lab_stockly.execute_stock_operations
Re-labels a lot: its number, its expiry. Stock-neutral: it books a batch_correction movement with
quantity zero, so the product’s history shows when a lot was re-dated without ever implying that
units moved.
Housekeeping
Section titled “Housekeeping”| Method | Path | What it does | ACL |
|---|---|---|---|
POST | warehouse/stock/delete-empty | drops one empty stock row by stockId | p2lab_stockly.execute_stock_operations |
POST | warehouse/stock/sync-product/{productId} | recalculates product.stock from the shelves | p2lab_stockly.execute_stock_operations |
delete-empty refuses with 409 not_deletable when the row still holds stock or reserved units.
The guard is in the delete statement itself, so a row that became meaningful a moment ago cannot be
removed by a request that was already in flight.
sync-product is the repair for a product.stock somebody else overwrote. It needs no payload and
is safe to repeat, because it derives the value rather than adjusting it.
Jobs that run in the background
Section titled “Jobs that run in the background”Four operations return a progressId immediately and continue in the background:
| Method | Path | Starts | ACL |
|---|---|---|---|
POST | warehouse/stock/initialize/{warehouseId} | seeds stock rows from product.stock | p2lab_stockly.execute_stock_operations |
POST | warehouse/stock/transfer-bulk | drains one warehouse into another | p2lab_stockly.execute_stock_operations |
POST | warehouse/{warehouseId}/auto-assign-bins | gives every unassigned row a suggested bin | p2lab_stockly.execute_stock_operations |
POST | warehouse/stock/sync-variants | rolls variant stock up to parents | p2lab_stockly.execute_stock_operations |
Each answers {"success": true, "progressId": "0191f9…"}. Poll it:
GET /api/_action/p2lab-stockly/warehouse/stock/job/{progressId}/status{ "progressId": "0191f9…", "status": "running", "totalItems": 4812, "processedItems": 1200, "error": null }initialize and sync-variants have status routes of their own,
warehouse/stock/initialize/{progressId}/status and warehouse/stock/sync-variants/{progressId}/status,
with the same response shape. All three status routes need p2lab_stockly_task_progress:read, which is a
different privilege from the one that started the job: a system that may watch is not automatically
one that may act.
An unknown progressId is a 404.
Seeding a warehouse
Section titled “Seeding a warehouse”initialize copies product.stock onto the warehouse’s unassigned bin, and it is deliberately
narrow about what it touches:
- leaf products only, because a variant parent’s stock is the sum of its children, so seeding both would count everything twice;
- products with stock above zero;
- products not already tracked in any warehouse, because
product.stockis the total across the whole network, so seeding an article that already lives somewhere else would copy its whole quantity a second time and the next recalculation would inflate the figure.
It is a one-time onboarding step, not a synchronisation. Running it twice adds nothing, which is the intent.
Related privileges
Section titled “Related privileges”p2lab_stockly.execute_stock_operations covers the ordinary write path. Four operations are gated
separately because they are decisions rather than movements:
| Privilege | Guards |
|---|---|
p2lab_stockly.commit_stocktake | committing or reverting a stock-check session |
p2lab_stockly.variance_approve | approving lines a variance gate held back |
p2lab_stockly.reassign_bins | moving a bin, with its stock, to another warehouse |
p2lab_stockly.run_integrity_fix | applying an integrity repair, and adopting open orders |
An integration that only books movements should not be granted any of the four.
- Putting stock in a bin
- External writes — what happens when somebody skips all of this
- Stock check — the counting workflow as the operator sees it