Stock Transfers Built
A stock transfer moves a set of products from one stock location to another through a short, deliberate lifecycle — each stage records its own quantity (requested, then sent, then received) so you always know exactly how much left the source and how much actually arrived at the destination.
What a stock transfer is
A transfer has a header — a source location, a destination location, an optional carrier, an optional schedule — and one or more items, each a product and a requested quantity. Nothing moves the moment you create it: a transfer starts life as a plan, and stock only actually leaves the source location when someone Dispatches it, and only actually arrives at the destination when someone Completes it. In between, three numbers travel with each item:
- Requested — set when the transfer is created; what you intend to move.
- Sent — set by Process; what is actually being sent (may differ from requested).
- Received — set by Receive; what actually arrived (may differ from sent, but can never exceed it).
This three-number trail is what lets a transfer absorb the ordinary friction of moving physical goods between locations — a requested quantity that turns out to be unavailable in full, or a received quantity that falls short of what was despatched — without losing the record of what happened at each stage.
Once a transfer is created, its header and its item list are fixed. There is no edit page and no delete action at any state — canEdit() and canDelete() are hard-coded to refuse on this resource. Every change after creation happens only through the four lifecycle actions below (Process, Dispatch, Receive, Complete), each of which is deliberately narrow in what it is allowed to touch.
Where to find it & who can see it
Open Inventory › Stock transfer in the left navigation. The list is visible to anyone holding stock_transfer.view (or a super administrator). Creating a new transfer additionally needs stock_transfer.create — see Creating a transfer below.
The state machine
A transfer moves through six states. Five buttons on the list drive the five console-exposed transitions; there is no automatic step anywhere in this lifecycle — every move forward (or into Cancelled) is a deliberate click by someone holding the right permission.
| State | Meaning |
|---|---|
| Draft | Just created. Items carry only a requested quantity; nothing has been actioned yet. |
| Processing | Process has set a quantity-to-send on each item; the transfer is being prepared for despatch but stock has not moved. |
| In Transit | Dispatch has run — stock has left the source location and is on its way. No longer cancellable. |
| Received | Receive has recorded what arrived at the destination team's end, but the stock ledger at the destination has not been updated yet. |
| Done | Complete has run — destination stock is updated. The transfer's final, successful state. |
| Cancelled | Abandoned before stock moved. The transfer's final, unsuccessful state. |
Creating a transfer
Select New stock transfer from the list. This needs stock_transfer.create. Creation is routed through TransferService::createDraft() rather than a plain Eloquent save, so that a reference number, the item rows and the creating user are all recorded consistently.
- Fill in the transfer header
Choose the transaction type, the source and destination locations, and (optionally) a carrier and a schedule.
- Add one or more items
Use Add product in the items repeater to add a row for each product being moved, with the quantity you intend to send. At least one item is required.
- Save
The transfer is created in the Draft state with a generated reference such as
TRF-2026-00001(year-scoped, zero-padded, sequential). No stock is affected — creation only ever records the plan.
Header fields
| Field | Type | Required | Notes |
|---|---|---|---|
| Transaction type | Select | Yes | One of five TransferTransactionType values: StockTransfer (default), MinQtyReplenish, Correction, ReusableQty, ReturnToSource. Purely classificatory — it does not change how the lifecycle behaves. |
| Source location | Select (searchable) | Yes | The stock location stock will be dispatched from. Drawn from warehouse locations — see Warehouses. |
| Destination location | Select (searchable) | Yes | The stock location stock will arrive at. |
| Carrier | Select (searchable) | No | The delivery carrier moving the goods between locations, where one is used. |
| Scheduled date | Date picker | No | When the transfer is expected to move. |
| Requires attachment | Toggle | No | Flags that this transfer needs a supporting document (e.g. a challan). Off by default. |
| Notes | Textarea | No | Free text, full width. |
Items
| Field | Type | Required | Notes |
|---|---|---|---|
| Product | Select (searchable) | Yes | Shown as "code — name". One row per product; the repeater requires at least one item. |
| Requested quantity | Number | Yes | Minimum 0.01. This becomes each item's qty_requested — the starting point that Process will refine into a quantity actually being sent. |
The lifecycle actions
- Process Draft → Processing stock_transfer.approve
Shown on a Draft transfer. Opens a form pre-filled with every item on the transfer — its product, its requested quantity (read-only, for reference) and an editable quantity to send:
Field Type Required Notes Product Text — Read-only, shown for context. Requested Text — Read-only, shown for context — the quantity set when the transfer was created. Qty to send Number Yes Minimum 0. Becomes the item's qty_sent. Does not have to match the requested quantity.Rows cannot be added, removed or reordered in this form — you are only ever setting a quantity against the transfer's existing items. Submitting the form is the confirmation; there is no separate "are you sure?" step. No stock moves yet — this stage only records intent.
- Dispatch Processing → In Transit stock_transfer.approve Confirmation required
Shown on a Processing transfer. A confirmation dialog is all it asks for — no form. This is the point at which stock actually leaves the source location:
What Dispatch actually doesFor every item with a
qty_sentgreater than zero, the source location's on-hand quantity (see Stock on hand) is decremented by that amount, and anInternal-type stock move is created linking source to destination — created already in theShippedstate, since the goods are physically in transit rather than merely reserved. If the source location does not hold enough stock to cover an item's quantity, Dispatch is refused outright and nothing on the transfer changes. Items left at a zero quantity to send are skipped entirely — no stock move is created for them. The transfer'ssent_dateis stamped and its state becomes In Transit. - Receive In Transit → Received stock_transfer.receive
Shown on an In Transit transfer. Opens a form pre-filled with every item — its product, the quantity that was sent (read-only, for reference) and an editable quantity received:
Field Type Required Notes Product Text — Read-only, shown for context. Qty sent Text — Read-only, shown for context — the quantity Dispatch sent. Qty received Number Yes Minimum 0. Becomes the item's qty_received. Cannot exceed the quantity sent — the service rejects the whole submission if any item's received quantity is higher, with the modal staying open and an error notification explaining why.As with Process, submitting the form is the confirmation. The transfer's
received_dateis stamped, the confirming user is recorded, and the state becomes Received — but destination stock is not updated yet; that happens on Complete. - Complete Received → Done stock_transfer.receive Confirmation required
Shown on a Received transfer. A confirmation dialog is all it asks for — no form. This is the point at which stock actually arrives at the destination location:
What Complete actually doesFor every item with a
qty_receivedgreater than zero, the destination location's on-hand quantity is incremented by that amount. Each item's linked stock move (created back at Dispatch) is markedDonewith a completed timestamp. The transfer's state becomes Done — its final, successful state. - Cancel Draft or Processing → Cancelled stock_transfer.approve Confirmation required
Shown in danger red. Only available before stock has moved — once a transfer reaches In Transit, Received or Done it can no longer be cancelled from this screen, because either stock has already left the source location or the transfer has already finished. A confirmation dialog is all it asks for — no reason field.
Each action's button is only shown when the transfer is in the exact state that action requires and you hold the permission it needs — the same safety rule as everywhere else in the console (see Getting started). A Done or Cancelled transfer never shows any action at all; it has reached the end of its life.
Columns
| Column | Shows |
|---|---|
reference | The transfer's identifying number, shown as a badge. Searchable and sortable. |
| Transaction type | Badge showing type_of_transaction — one of the five TransferTransactionType values. |
| From | sourceLocation.location_name. |
| To | destLocation.location_name. |
| State | A coloured badge — see the state machine above for the colour key. |
| Items | A count of the transfer's item rows. |
scheduled_date | Date the transfer is expected to move. Toggleable. |
created_at | Date and time the transfer was created. Sortable, hidden by default. |
Filters
Select Filters to open two drop-downs: State (all six TransferState values) and Transaction type (all five TransferTransactionType values). The search box matches on reference, the only searchable column.
The View page
Selecting a transfer's reference opens its view page. The top section is read-only: reference, transaction type, from/to locations, the coloured state badge, carrier, scheduled date, sent date, received date and notes.
Below it, an Items tab lists every item on the transfer, also read-only — items are never added, edited or removed here; they only change through the lifecycle actions on the list.
| Column | Shows |
|---|---|
| Code | product.code. |
| Product | product.name. |
| Requested | qty_requested — set at creation. |
| Sent | qty_sent — set by Process, zero until then. |
| Received | qty_received — set by Receive, zero until then. |
| Move | A coloured badge showing the state of the item's linked stock move — blank until Dispatch creates one. |
Attachments tab Built
The view page also has an Attachments tab where you can attach supporting documents — a challan, a photo, a signed receipt — to the transfer. Unlike the lifecycle actions, this is available at every stage: draft, processing, in-transit, received or done. Each attachment records which stage the transfer was at when the file was added, who uploaded it, and its size.
- Add attachment (needs stock_transfer.create) uploads a file (up to 10 MB). Files are stored on the server's private disk — they are not publicly reachable by URL.
- Download (needs stock_transfer.view) streams the file back to you through the authenticated console.
- Delete (needs stock_transfer.create) removes the attachment and its file from disk.
This is where the Requires attachment flag set at creation is satisfied — the flag marks that a transfer should carry a document, and this tab is where you provide it.
Related pages
See Stock on hand to watch a location's qty_on_hand fall when a transfer is dispatched and rise when it is completed; Warehouses for how source and destination locations are set up; and Stock Moves for the internal move each dispatched item creates and how its state relates to the transfer.