Context for managing transfers (stock moved between two warehouses).
A transfer moves stock from a source_location_uuid warehouse to a
destination_location_uuid warehouse via two separate atomic postings —
not one shared transaction — because the goods physically leave the
source and arrive at the destination at different points in time:
ship_transfer/2(draft -> in_transit) DECREASES stock at the source (conditional decrement — the whole Multi rolls back if any line has insufficient stock, mirroringGoodsIssues.post_goods_issue/2).receive_transfer/2(in_transit -> done) INCREASES stock at the destination (additive delta, mirroringGoodsReceipts.post_goods_receipt/2).
Both locations must be chosen and distinct before a transfer can ship —
ship_transfer/2 and receive_transfer/2 return {:error, :locations_required} up front rather than silently falling back to the
configured default warehouse.
A transfer can also be cancelled via cancel_transfer/2, from draft
(no postings — nothing moved yet) or from in_transit (credits stock back
to the source, reversing ship_transfer/2). It cannot be cancelled once
done (received) or already cancelled.
Summary
Functions
Manually attaches a traceability reference to a transfer.
Cancels a transfer.
Corrects the note and/or storage_folder_uuid of a transfer without changing status or lines. Works on documents in any status.
Creates a new draft transfer.
Returns {:ok, transfer} or {:error, :not_found}.
Returns the transfer or raises.
Lists non-deleted transfers ordered by number descending (newest first).
Receives a transfer in an Ecto.Multi transaction (in_transit -> done).
INCREASES stock at destination_location_uuid.
Detaches a traceability reference from a transfer. No-op when the
{type, uuid} pair isn't present.
Sets the storage_folder_uuid on a transfer. Works on documents in any status.
Ships a transfer in an Ecto.Multi transaction (draft -> in_transit).
DECREASES stock at source_location_uuid.
Soft-deletes a draft transfer. Returns {:error, :not_draft} for shipped/received/cancelled transfers.
Updates a draft transfer. Returns {:error, :not_draft} when not in draft
status. Locations may be left/set to nil — see create_transfer/1.
Functions
Manually attaches a traceability reference to a transfer.
type must be a kind registered via PhoenixKitWarehouse.SourceKinds.
Pure metadata — does not touch lines and is not gated to draft status.
A duplicate {type, uuid} pair is a no-op.
Cancels a transfer.
- From
draft: NO stock postings — the goods never physically moved, so cancelling just locks the row FOR UPDATE (re-checking status == "draft", guarding against a concurrent ship) and flips status -> "cancelled" viaTransfer.cancel_changeset/2. - From
in_transit: reverses theship_transfer/2posting. Locks the row FOR UPDATE (re-checking status == "in_transit") and, for each line with transfer_quantity > 0, credits the quantity BACK tosource_location_uuidviaStockLedger.receive_quantity/3(additive — unlike issuing, this cannot fail on insufficient stock). Capturesreversed_source_quantity(the source's on-hand quantity immediately before the credit) on each line for audit, mirroringprevious_source_quantity/previous_destination_quantityon the other two legs. Does not touch the destination — nothing arrived there yet. - From
doneor alreadycancelled: returns{:error, :not_cancellable}— a completed transfer can't be un-received, and a cancelled transfer can't be cancelled twice.
Corrects the note and/or storage_folder_uuid of a transfer without changing status or lines. Works on documents in any status.
Creates a new draft transfer.
Unlike GoodsIssues.create_goods_issue/1 and friends, source_location_uuid
and destination_location_uuid do NOT default to the configured default
warehouse — a transfer is meaningless without two specific, distinct
warehouses, so both are left nil when not supplied in attrs. The UI
requires both to be chosen before the transfer can be shipped (see
ship_transfer/2).
created_by_uuid is set programmatically — not via cast.
Returns {:ok, transfer} or {:error, :not_found}.
Returns the transfer or raises.
Lists non-deleted transfers ordered by number descending (newest first).
Receives a transfer in an Ecto.Multi transaction (in_transit -> done).
INCREASES stock at destination_location_uuid.
- Returns
{:error, :locations_required}when either location isnilor they're equal to each other. Both are guaranteed to already be set at this stage (the transfer went throughship_transfer/2first), but the check is cheap and guards against manually-corrupted data. - Locks the row FOR UPDATE and re-checks status == "in_transit" (prevents double-receiving).
- Deduplicates lines by item_uuid.
- For each line with transfer_quantity > 0:
- Captures
previous_destination_quantity= current on-hand at the destination for audit. - Calls
StockLedger.receive_quantity/3(additive stock delta — does NOT touch the source again).
- Captures
- Lines with transfer_quantity == 0 contribute no stock change.
- Flips status -> "done", sets received_at and performed_by_uuid.
Returns {:error, :not_in_transit} for transfers not in in_transit status.
Detaches a traceability reference from a transfer. No-op when the
{type, uuid} pair isn't present.
Sets the storage_folder_uuid on a transfer. Works on documents in any status.
Ships a transfer in an Ecto.Multi transaction (draft -> in_transit).
DECREASES stock at source_location_uuid.
- Returns
{:error, :locations_required}BEFORE touching the database whensource_location_uuidordestination_location_uuidisnil, or when they're equal to each other —StockLedger.issue_quantity/3would otherwise silently fall back to the configured default warehouse for anillocation, a materially different (and wrong) outcome from "no location chosen yet". - Locks the row FOR UPDATE and re-checks status == "draft" (prevents double-shipping).
- Deduplicates lines by item_uuid.
- For each line with transfer_quantity > 0:
- Captures
previous_source_quantity= current on-hand at the source for audit. - Calls
StockLedger.issue_quantity/3(conditional decrement). - If ANY line returns
{:error, {:insufficient_stock, _}}, the WHOLE Multi rolls back: stock is unchanged and the document stays draft.
- Captures
- Lines with transfer_quantity == 0 contribute no stock change.
- Flips status -> "in_transit", sets shipped_at and performed_by_uuid.
Returns {:error, :not_draft} for non-draft transfers.
Soft-deletes a draft transfer. Returns {:error, :not_draft} for shipped/received/cancelled transfers.
Updates a draft transfer. Returns {:error, :not_draft} when not in draft
status. Locations may be left/set to nil — see create_transfer/1.