Stock Transfers
Moving inventory between stock sites — full flow and every action
Stock Transfers
A stock transfer has two linked entities:
StockTransfer(outgoing) — created and managed by the sender site.IncomingStockTransfer(incoming) — auto-created at the destination site on approval.
Both are driven independently by their own status machines and endpoints.
Statuses
Outgoing (StockTransfer)
Incoming (IncomingStockTransfer)
End-to-end flow
1. Sender — Create draft
POST /stock-transfers/empty → StockTransferService.createDraft()
Creates a new transfer in DRAFT with a unique transfer number. No products yet.
2. Sender — Edit transfer
PUT /stock-transfers/{id} → StockTransferService.update()
Allowed from DRAFT or REJECTED only. Sets subsidiary, source site, destination, products, batch allocations, delivery date/method/priority. Validates available stock at source and batch allocations.
3. Sender — Validate (optional preview)
GET /stock-transfers/{id}/validate → StockTransferService.validate()
Read-only pre-flight. Checks products, source/destination stock, batch allocations. Does not change state.
4. Sender — Cancel draft / reinstate (optional)
PATCH /stock-transfers/{id}/cancel-draft→cancelDraft()—DRAFT/REJECTED→DRAFT_CANCELLED.PATCH /stock-transfers/{id}/reinstate→reinstate()—DRAFT_CANCELLED→DRAFT.
5. Sender — Request approval
PATCH /stock-transfers/{id}/request-approval → requestApproval()
From DRAFT/REJECTED. Runs validation, transitions to PENDING_APPROVAL, notifies all TENANT_ADMIN users.
6. Approver — Approve or reject
PATCH /stock-transfers/{id}/approve→approve()—PENDING_APPROVAL→OPEN. Auto-creates theIncomingStockTransferat destination with statusEXPECTED. Assigns transfer number if missing.PATCH /stock-transfers/{id}/reject→reject()—PENDING_APPROVAL→REJECTED. Sender can edit and resubmit.
7. Warehouse — Reserve products
POST /stock-transfers/{id}/reservations → reserveProducts()
Only from OPEN. Reserves source stock via StockInventoryService.reserve(). Partial reservation is allowed — requested quantity may be less than pending. Updates transfer.reservedProducts.
8. Warehouse — Cancel reservations (optional)
PATCH /stock-transfers/{id}/reservations/cancel → cancelReservations()
Only from OPEN. Fails if the reservation is already assigned to a picklist. Removes reservations from inventory.
9. Warehouse — Generate picklist
POST /stock-transfers/{id}/picklists → generatePicklist()
Only from OPEN. Requires reserved, unassigned quantities. Creates a Picklist (type StockReservationType.TRANSFER) and links it to the transfer via picklistIds.
10. Warehouse — Cancel picklist (optional)
PATCH /stock-transfers/{id}/picklists/{picklistId}/cancel → cancelPicklist()
Marks the linked picklist CANCELLED. Does not auto-release the underlying reservations — cancel those separately if needed.
11. Warehouse — Pick, ship, deliver (driven by the picklist lifecycle)
The picklist service calls back into the transfer:
markAsPicked()— picked products removed fromreservedProducts.markAsShipped()— shipment event recorded; stock moves to in-transit.markAsDelivered()— auto-transitions the outgoing transfer toCOMPLETED(all delivered) orPARTIALLY_COMPLETED(nothing still pending).
These are not directly called from the UI — they fire when the picklist is advanced.
12. Receiver — Receive goods
PATCH /incoming-stock-transfers/{id}/receive → IncomingStockTransferService.receive()
Allowed from EXPECTED or PARTIALLY_RECEIVED. Accepts an array of received products with, per line:
- Accepted / rejected quantities (rejections carry a reason).
- Batch and serial numbers.
- Destination location within the site.
- UoM split (if receiving in different units than shipped).
Side effects:
- Accepted stock →
StockInventoryService.add()at the destination site. - In-transit inventory decremented.
- Status auto-derived per call:
RECEIVED(all accepted),REJECTED(all rejected), otherwisePARTIALLY_RECEIVED. - Events recorded per rejection reason and per acceptance.
Call as many times as needed until everything is resolved.
13. Receiver — Close incoming transfer
PATCH /incoming-stock-transfers/{id}/close → close()
Requires zero pending items on the enriched transfer. Sets status to CLOSED. Incoming transfers cannot be bulk-deleted — the DELETE endpoint throws InvalidOperationException.
14. Sender — Close outgoing transfer
PATCH /stock-transfers/{id}/close → StockTransferService.close()
From COMPLETED or PARTIALLY_COMPLETED. Finalises the outgoing record.
Full endpoint reference
Outgoing — /stock-transfers
| Method | Path | Service method | Purpose |
|---|---|---|---|
POST | /empty | createDraft | Create blank draft |
GET | /{id} | getEnriched | Read one, enriched |
PUT | /{id} | update | Edit while DRAFT/REJECTED |
GET | /{id}/validate | validate | Pre-flight check |
PATCH | /{id}/request-approval | requestApproval | → PENDING_APPROVAL |
PATCH | /{id}/approve | approve | → OPEN, creates incoming |
PATCH | /{id}/reject | reject | → REJECTED |
PATCH | /{id}/cancel-draft | cancelDraft | → DRAFT_CANCELLED |
PATCH | /{id}/reinstate | reinstate | → DRAFT |
POST | /{id}/reservations | reserveProducts | Reserve source stock |
PATCH | /{id}/reservations/cancel | cancelReservations | Release reservations |
POST | /{id}/picklists | generatePicklist | Create linked picklist |
PATCH | /{id}/picklists/{picklistId}/cancel | cancelPicklist | Cancel a linked picklist |
PATCH | /{id}/close | close | → CLOSED |
GET | /page | findPage | Paginated list |
GET | /page-enriched | findPageEnriched | Paginated list, enriched |
POST | /dynamic-search-enriched | searchEnriched | Dynamic search |
GET | /stats | getStats | Top products + status counts |
POST | /{id}/comment | addCommentAndReturnEnriched | Append comment |
Service-only (called from PicklistService): markAsPicked, markAsShipped, markAsDelivered.
Incoming — /incoming-stock-transfers
| Method | Path | Service method | Purpose |
|---|---|---|---|
GET | /{id} | getEnriched | Read one, enriched |
PATCH | /{id}/receive | receive | Accept/reject lines |
PATCH | /{id}/close | close | → CLOSED |
POST | /{id}/comment | addCommentAndReturnEnriched | Append comment |
GET | /page | findPage | Paginated list |
GET | /page-enriched | findPageEnriched | Paginated list, enriched |
GET | /stats | getIncomingStockTransferStats | Top products + status counts |
DELETE | / | deleteMany | Blocked — throws InvalidOperationException |
Service-only: create (called from StockTransferService.approve()).
Batch, serial, UoM, location handling
- Batch allocations are validated on
update()viavalidateBatchAllocations()against actual batches at the source site. - UoM splits —
receive()acceptsReceivedGoodsDto.uomSplitso goods can be received in a different unit than shipped. - Serials / batch numbers — carried on both allocation and acceptance. Stored on incoming
acceptedProducts. - Locations — destination bin/location recorded per accepted line (
ProductPriceQuantityLocation).
Inventory side effects summary
| Action | Source site | Destination site |
|---|---|---|
reserveProducts | reserve() | — |
cancelReservations | unreserve | — |
generatePicklist | — | — (links picklist) |
markAsPicked (via picklist) | deduct from reserved | — |
markAsShipped (via picklist) | → in-transit | — |
receive (accepted) | in-transit decrement | add() to on-hand |
receive (rejected) | in-transit decrement | — |
Frontend routes
| Route | Purpose |
|---|---|
/stock/transfers/ | List + create |
/stock/transfers/$stockTransferId | Sender-side editor |
/stock/transfers/received/$stockTransferReceivedId | Receiver-side |
Key files
| Layer | Path |
|---|---|
| Outgoing model | src/backend/.../model/StockTransfer.kt |
| Incoming model | src/backend/.../model/IncomingStockTransfer.kt |
| Outgoing controller | src/backend/.../controller/StockTransferController.kt |
| Outgoing service | src/backend/.../service/StockTransferService.kt |
| Incoming controller | src/backend/.../controller/IncomingStockTransferController.kt |
| Incoming service | src/backend/.../service/IncomingStockTransferService.kt |
| Frontend routes | src/frontend/src/routes/_app/(erp)/stock/transfers/ |