Refactor production and stock tests to remove UOM dependency

- Updated production.spec.ts, stock-adjustments.spec.ts, and stock-transfers.spec.ts to eliminate UOM references in API seeder and test cases.
- Adjusted ApiSeeder methods to remove UOM parameters from stock receiving and production template creation.
- Revised documentation to reflect changes in UOM handling, emphasizing that stock is counted in base UOM only.
- Introduced new enums for MeasureUnit and StageQtyUnit to clarify content size and stage input quantities.
- Implemented ItemContent service to validate and normalize content sizes.
- Updated smoke tests to validate production stage inputs expressed in content units, ensuring correct consumption calculations.
- Modified frontend UOM label handling to reflect the removal of per-line UOMs in document lines.
This commit is contained in:
2026-08-11 11:32:40 +05:30
parent d37824cecc
commit 15ddac178c
108 changed files with 1221 additions and 987 deletions
+2 -1
View File
@@ -245,7 +245,8 @@ All are `POST /production-runs/{id}/stages/{sid}/…`, transactional, and return
> **AS BUILT — clarifications and deviations, all verified by `Backend/smoke/`:**
>
> - **`Idempotency-Key` is accepted and ignored**, matching the Phase-1 posture exactly (`GrnsController` takes the header, `GrnService.ConfirmAsync` ignores it). Replay safety comes from the status guards this section already specifies: a double-fire finds the stage already moved on and gets a `409`, which `21-FRONTEND-PHASE2 §6` already tells the client to treat as a silent refetch. No key store was built. `RunStage` additionally carries an `xmin` `RowVersion` so two genuinely concurrent terminal approves cannot both read `Done` and post two receipts.
> - **UOM conversion on Stock inputs.** Not mentioned anywhere in this doc, but `STAGE_INPUT.uom_id` is a free FK while the FIFO engine works exclusively in the item's **base** UOM. All consumption therefore converts through the shared `IUomConverter` (extracted from `GrnService.ToBaseAsync`). Consequence for the contract: **`plannedQty` is in the input's declared UOM, while `consumedQty`/`consumedValue`/`returnedQty`/`returnedValue` are in the item's base UOM.** An input whose UOM has no conversion defined for the item is refused with `422`, never assumed 1:1.
> - **Content units on Stock inputs (revised 2026-08-11).** `STAGE_INPUT.uom_id` is gone, along with the per-item UOM conversion table it depended on. An input now carries `qtyUnit` — `Pack` or `Content` — and the FIFO engine still works exclusively in the item's base UOM (packs). A `Content` quantity is an amount of the item's content in its base content unit (ml or g) and is divided by `Item.ContentBaseQty` to get packs: 2000 ml of a 500 ml bottle consumes 4, and 300 ml consumes **0.6 — fractional packs are legal**, which is why the quantity columns are `(18,4)`. Consequence for the contract, unchanged in spirit: **`plannedQty` is in `qtyUnit`, while `consumedQty`/`consumedValue`/`returnedQty`/`returnedValue` are always a pack count.** `Content` on an item with no content size is refused with `422` at template save, never assumed 1:1. Upstream inputs must be `Pack` — WIP has no content size.
> - **Output units.** Outputs are **always** pack counts, so scrap is recorded in whole broken bottles rather than in millilitres. An output that references an item takes its unit from that item and must send `uomId: null`; an intermediate WIP output has no item and therefore **must** name its own `uomId` as a display label (`422 WIP_UNIT_REQUIRED` otherwise). That label is never converted — WIP touches neither stock nor the ledger.
> - **Transfers route by input, not by edge.** FR-MFG-12 says "per outbound edge", but the model connects an output to a specific *input* (`RUN_STAGE_INPUT.from_run_output_id`): an edge can exist with no input drawing from it, and one output can feed inputs on several children. Delivery therefore routes by `fromRunOutputId`; `RUN_EDGE` is display and validation only. When one output feeds several inputs and no explicit target is given, they fill in `runInputId` order up to each one's outstanding need with any overflow to the last; the request accepts an optional `runInputId` to remove the ambiguity.
> - **A re-complete overwrites, it does not accumulate.** Completing a stage that already has `producedQty > 0` (a rework re-complete) *replaces* the figures. Adding would double the produced quantity on every rework pass. A re-complete that would drop the good quantity below what has already been transferred is refused with `422 TRANSFER_EXCEEDS_AVAILABLE`.
> - **Start consumes only the delta.** Every start consumes `max(0, plannedBase consumedQty)`. A rework restart with an unchanged planned quantity therefore makes **no FIFO call at all**, and one after a raise consumes only the increase. This is what makes FR-MFG-16's "edit planned Stock-input qty upward" work.