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

Context for managing internal orders (warehouse demand documents).

Internal orders bridge sub-order material sheets and supplier orders.
They do NOT affect stock — posting only flips status and sets timestamps.

# `add_source_ref`

Manually attaches a traceability reference to an internal order.

`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_internal_order`

Corrects the note of an internal order without changing status.
Works on documents in any status.

# `create`

```elixir
@spec create([map()], map() | nil, keyword()) ::
  {:ok, PhoenixKitWarehouse.InternalOrder.t()} | {:error, Ecto.Changeset.t()}
```

Generic entry point for creating a draft internal order from arbitrary
lines plus a single (optional) source ref — no domain knowledge of
"material sheets" or any other host concept required. A host app (e.g.
Andi's sub-order Show page, per the design doc's "one piece of business
logic that stays in Andi") builds its own `lines` from whatever concept
it has, builds a `%{"type" => kind, "uuid" => uuid}` source ref itself,
and calls this function — replacing the removed `create_from_material_sheet/3`.

`opts`:
  * `:created_by_uuid` — set programmatically, not via cast.
  * `:location_uuid` — defaults to `StockLedger.default_location_uuid/0`.

# `create_internal_order`

Creates a new draft internal order.

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

# `get_internal_order`

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

# `get_internal_order!`

Returns the internal order or raises.

# `import_from_sources`

Imports material lines from a list of source refs into an internal order.

`selected_refs` is a list of `%{"type" => "order" | "sub_order", "uuid" => uuid}`.
For each source, builds lines via the registered `SourceKinds.build_lines/3`
callback. Lines are merged into the order's current lines, summing
required_quantity by item_uuid. New source refs are appended to source_refs
(deduped). Saves via update_draft. Returns {:ok, order} or {:error, reason}.
Draft-only.

# `list_import_candidates`

Returns import candidates from every registered `PhoenixKitWarehouse.SourceKinds`
kind, merged, as `%{kind:, label_prefix:, uuid:, label:, extra:}` maps
(the exact shape `SourceKinds.search_candidates/1` returns). Returns `[]`
when no `source_kinds` are configured — this is what makes the module
usable standalone, with no host "order" concept at all.

`query` (optional) is forwarded to every registered kind's own `search`
callback — filtering is each kind's own responsibility, not this
function's, since only the host knows how to match against its own data
(order number, client name, etc).

# `list_internal_orders`

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

# `list_posted_internal_orders`

Lists non-deleted posted internal orders ordered by number descending.
Used for goods issue source picker — filters in SQL so only relevant rows
are loaded.

# `post_internal_order`

Posts an internal order in an `Ecto.Multi` transaction.

- Locks the row FOR UPDATE and re-checks status == "draft" (prevents
  double-posting of the same draft).
- Deduplicates lines by item_uuid.
- Flips status → "posted", sets posted_at and performed_by_uuid.
- Does NOT write any stock rows.

Returns `{:error, :not_draft}` for non-draft orders.

# `remove_source_ref`

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

# `soft_delete_internal_order`

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

# `update_draft`

Updates a draft internal order. 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 order that
was posted concurrently by another tab/user.

---

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