# `PhoenixKitWarehouse.GoodsIssues`
[🔗](https://github.com/BeamLabEU/phoenix_kit_warehouse/blob/0.4.0/lib/phoenix_kit_warehouse/goods_issues.ex#L1)

Context for managing goods issues (materials written off to production).

A goods issue registers materials leaving the warehouse into production.
It DECREASES warehouse stock when posted. There is NO repost —
posting a goods issue is a conditional decrement operation. If any line has
insufficient stock the ENTIRE Multi rolls back. Correction after posting is
limited to note and storage_folder only.

# `add_source_ref`

Manually attaches a traceability reference to a goods issue.

`"internal_order"` is always valid (intra-module). Any other `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.

# `correct_goods_issue`

Corrects the note and/or storage_folder_uuid of a goods issue without
changing status or lines. Works on documents in any status.
Lines are immutable once posted.

# `create_from_internal_order`

Creates a draft goods issue from a posted internal order.

Lines are copied from the internal order with:
- `issued_quantity` defaulting to the internal order line's `required_quantity`
  (the keeper adjusts down if fewer are actually available/needed).

Sets `internal_order_uuid`, `location_uuid`, and propagates `source_refs`
(the internal order's own registered-kind refs plus an `"internal_order"` ref
for the source itself) so traceability is preserved.

# `create_goods_issue`

Creates a new draft goods issue.

`location_uuid` defaults to the configured default warehouse when not given.
`created_by_uuid` is set programmatically — not via cast.

# `get_goods_issue`

Returns `{:ok, issue}` or `{:error, :not_found}`.

# `get_goods_issue!`

Returns the goods issue or raises.

# `import_from_internal_orders`

Imports lines from one or more posted internal orders into an existing draft
goods issue.

Lines are merged by `item_uuid` (quantities summed). The primary internal
order is taken as the first UUID in `io_uuids`; its own UUID is set on the
goods issue when it currently has no `internal_order_uuid` set. Source
references (`source_refs`) are enriched with the traceability chain — each
selected internal order plus the customer order(s)/sub-order(s) that fed it
(from its own `source_refs`) — deduplicating by `{type, uuid}`.

`issued_quantity` is set to `required_quantity` (same as `create_from_internal_order/2`).
`unit_value` is left nil.

Returns `{:ok, updated_issue}` or `{:error, reason}`.

# `list_goods_issues`

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

# `post_goods_issue`

Posts a goods issue in an `Ecto.Multi` transaction. DECREASES warehouse stock.

- Locks the row FOR UPDATE and re-checks status == "draft" (prevents double-posting).
- Deduplicates lines by item_uuid.
- For each line with issued_quantity > 0:
  - Captures `previous_quantity` = current on-hand for audit.
  - Calls `StockLedger.issue_quantity/3` (conditional decrement — WHERE quantity >= qty).
  - If ANY line returns `{:error, {:insufficient_stock, _}}`, the WHOLE Multi
    rolls back: stock is unchanged and the document stays draft.
- Lines with issued_quantity == 0 contribute no stock change.
- Merges `previous_quantity` into the persisted lines (audit trail).
- Flips status → "posted", sets posted_at and performed_by_uuid.

Returns `{:error, :not_draft}` for non-draft issues.
Returns `{:error, {:insufficient_stock, item_uuid}}` when any line cannot be
fulfilled; the entire transaction is rolled back.

# `remove_source_ref`

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

# `set_storage_folder`

Sets the `storage_folder_uuid` on a goods issue.
Works on documents in any status.

# `soft_delete`

Soft-deletes a draft goods issue. Returns {:error, :not_draft} for posted documents.

# `update_draft`

Updates a draft goods issue. Returns `{:error, :not_draft}` when not in
draft status.

Locks the row FOR UPDATE and re-checks status == "draft" in the DB (not
just the in-memory struct) so a stale tab cannot overwrite an issue that
was posted concurrently by another tab/user.

---

*Consult [api-reference.md](api-reference.md) for complete listing*
