PhoenixKitWarehouse.Transfers (PhoenixKitWarehouse v0.2.2)

Copy Markdown View Source

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, mirroring GoodsIssues.post_goods_issue/2).
  • receive_transfer/2 (in_transit -> done) INCREASES stock at the destination (additive delta, mirroring GoodsReceipts.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.

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

add_source_ref(transfer, type, uuid)

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.

cancel_transfer(transfer, performed_by_uuid)

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" via Transfer.cancel_changeset/2.
  • From in_transit: reverses the ship_transfer/2 posting. Locks the row FOR UPDATE (re-checking status == "in_transit") and, for each line with transfer_quantity > 0, credits the quantity BACK to source_location_uuid via StockLedger.receive_quantity/3 (additive — unlike issuing, this cannot fail on insufficient stock). Captures reversed_source_quantity (the source's on-hand quantity immediately before the credit) on each line for audit, mirroring previous_source_quantity/ previous_destination_quantity on the other two legs. Does not touch the destination — nothing arrived there yet.
  • From done or already cancelled: returns {:error, :not_cancellable} — a completed transfer can't be un-received, and a cancelled transfer can't be cancelled twice.

correct_transfer(transfer, attrs)

Corrects the note and/or storage_folder_uuid of a transfer without changing status or lines. Works on documents in any status.

create_transfer(attrs)

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.

get_transfer(uuid)

Returns {:ok, transfer} or {:error, :not_found}.

get_transfer!(uuid)

Returns the transfer or raises.

list_transfers(opts \\ [])

Lists non-deleted transfers ordered by number descending (newest first).

receive_transfer(transfer, performed_by_uuid)

Receives a transfer in an Ecto.Multi transaction (in_transit -> done). INCREASES stock at destination_location_uuid.

  • Returns {:error, :locations_required} when either location is nil or they're equal to each other. Both are guaranteed to already be set at this stage (the transfer went through ship_transfer/2 first), 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).
  • 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.

remove_source_ref(transfer, type, uuid)

Detaches a traceability reference from a transfer. No-op when the {type, uuid} pair isn't present.

set_storage_folder(transfer, storage_folder_uuid)

Sets the storage_folder_uuid on a transfer. Works on documents in any status.

ship_transfer(transfer, performed_by_uuid)

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 when source_location_uuid or destination_location_uuid is nil, or when they're equal to each other — StockLedger.issue_quantity/3 would otherwise silently fall back to the configured default warehouse for a nil location, 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.
  • 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_delete_transfer(transfer, actor_uuid)

Soft-deletes a draft transfer. Returns {:error, :not_draft} for shipped/received/cancelled transfers.

update_draft(transfer, attrs)

Updates a draft transfer. Returns {:error, :not_draft} when not in draft status. Locations may be left/set to nil — see create_transfer/1.