At a Glance
Rule families:ADJ_VAL_*validation ·ADJ_AUTH_*permission ·ADJ_CALC_*calc ·ADJ_POST_*posting ·ADJ_XMOD_*cross-module
Rule count: approximately 96 rules
Audience: Test author + developer — every rule ID is anchored from04-test-scenarios*pages
Status lifecycle: Section 5.1 (where present) carries the Live UI vs BRD discrepancy callouts
This page catalogues the operational rules governing the inventory-adjustment module — the two-table document layer (tb_stock_in / tb_stock_out) that records manual corrections to inventory quantities and values. The rules below sit above the ledger-level rules in inventory/02-business-rules — they govern the document lifecycle (draft → in_progress → completed → cancelled / voided), the per-direction reason-code restriction, the document-level totals roll-up (a derived value, not a persisted header column), the threshold-based approval routing (Store Keeper → Inventory Controller → Finance), and the cross-module hooks into physical-count / spot-check for variance rollup. The actual ledger writes (tb_inventory_transaction + tb_inventory_transaction_detail + tb_inventory_transaction_cost_layer), the FIFO / weighted-average cost picks, the no-negative-balance guard, and the period-containment check fire at posting time on the inventory side and are documented under INV_VAL_* / INV_CALC_* / INV_POST_* / INV_AUTH_* in inventory — this page references them rather than duplicating.
Two structural notes from inventory-adjustment/01-data-model colour every rule below: first, there is no single tb_inventory_adjustment model — every rule that references "the adjustment document" applies symmetrically to tb_stock_in and tb_stock_out unless the direction is called out, and the enum_adjustment_type on tb_adjustment_type constrains which document tree a reason can be used on. Second, the rule ID catalogue carmen/docs uses (ADJ_CRT_, ADJ_VAL_, ADJ_PRC_, ADJ_UI_, ADJ_CALC_*) is the source for this page's ADJ_* IDs, but several rules are realigned to the Prisma reality: the carmen/docs three-state lifecycle (Draft → Posted → Void) maps to the five-state Prisma lifecycle (draft → in_progress → completed → cancelled / voided); the "total cost must match header sum" check (ADJ_VAL_005) is degenerate because no header total column exists; the "department mandatory" check (ADJ_CRT_009) is enforced via dimension JSON not a column.
Rule IDs follow ADJ_VAL_NNN. Validation runs at two boundaries: at draft creation / edit (form-level structural checks that don't yet touch the ledger), and at submit (draft → in_progress if a workflow stage exists, or draft → completed if auto-approve below threshold) — the submit-time check is where the inventory-side INV_VAL_* rules from inventory/02-business-rules fire as a transactional unit.
| Rule ID | Condition | When enforced | Error / behaviour |
|---|---|---|---|
ADJ_VAL_001 |
The document number si_no (stock-in) / so_no (stock-out) is unique within deleted_at. New documents auto-generate SI-YYMM-NNNNN / SO-YYMM-NNNNN from the tenant numbering service; user-edited numbers are rejected on collision. Carmen-docs ADJ_CRT_001. |
Save / submit | Reject with "Stock-in/-out number <si_no/so_no> already exists for this tenant." Auto-numbering retries up to 3x in the rare race; manual edits fall through to user. |
ADJ_VAL_002 |
tb_stock_in.adjustment_type_id (or tb_stock_out.adjustment_type_id) is non-null, points to an active (is_active = true, deleted_at IS NULL) tb_adjustment_type row, and the reason's type matches the document direction (stock_in for tb_stock_in, stock_out for tb_stock_out). Carmen-docs ADJ_CRT_004 + ADJ_VAL_006. |
Save / submit | Reject with "Adjustment reason is required and must match the document direction (stock_in reasons cannot be used on stock-out documents and vice versa)." Picker filters by direction at the UI layer, but the rule is re-checked server-side. |
ADJ_VAL_003 |
location_id is non-null, references an active inventory- or consignment-type location (enum_location_type ∈ {inventory, consignment}, is_active = true, deleted_at IS NULL). Direct-cost locations are rejected per inventory INV_VAL_009 (direct locations carry no balance). Carmen-docs ADJ_CRT_003 + ADJ_VAL_008. |
Save / submit | Reject with "Location is required and must be an inventory- or consignment-type location." for null; "Direct-cost locations cannot be the target of an adjustment — direct locations bypass inventory." for direct-type. |
ADJ_VAL_004 |
description is non-empty on the header (used for audit context). Carmen-docs ADJ_CRT_005. |
Save / submit | Reject with "Description is required for audit purposes." Soft-fail at save (warning); hard-fail at submit. |
ADJ_VAL_005 |
Department / cost-centre is populated in dimension JSON (typical shape [{type: "department", id: "<uuid>", code: "<code>"}, ...]). Carmen-docs ADJ_CRT_009. |
Save / submit | Reject at submit with "Department / cost-centre is required (set via dimension)." Soft-fail at save (warning). Tenant config may relax this for specific reason codes. |
ADJ_VAL_006 |
Every detail line has a non-null product_id referencing an active tb_product (is_active = true, deleted_at IS NULL), and the product is enabled at the chosen location (tb_product_location row exists, deleted_at IS NULL). Carmen-docs ADJ_CRT_006 + ADJ_VAL_009. |
Save / submit | Reject with "Product <product_code> is not active or not enabled at location <location_code>." Atomic rejection of the whole document. |
ADJ_VAL_007 |
Every detail line has qty > 0 (sign is direction-implicit — both documents store positive qty in the detail; the inventory-side detail negates for stock-out per inventory INV_VAL_004). Carmen-docs ADJ_VAL_002. |
Save / submit | Reject with "Quantity must be greater than zero on every line." Zero or negative qty rejected. |
ADJ_VAL_008 |
Every detail line has cost_per_unit ≥ 0 on stock-in (zero allowed for FOC / vendor-replacement; never negative). For stock-out, the user-entered cost_per_unit is a preview — the authoritative cost is picked at post time by the costing engine per inventory INV_CALC_005 / INV_CALC_006, so the user-entered value is informational. Carmen-docs ADJ_VAL_003 + ADJ_VAL_007. |
Save / submit | Reject with "Cost per unit must be non-negative." |
ADJ_VAL_009 |
For lot-tracked products: lot identity is well-formed on the line's info.lotNo (existing lot must exist at (location_id, product_id) for stock-out and stock-in to existing lot; new-lot lot_no must be unique within (product_id, location_id) for stock-in new-lot). Perishable products require info.expiryDate on new-lot lines. Carmen-docs ADJ_CRT_007 + ADJ_VAL_004. |
Save / submit | Reject with "Lot <lot_no> at location <location> is not available for consumption." for stock-out missing-lot; "Lot <lot_no> already exists for product <product> at location <location>; lot identity must be unique." for stock-in new-lot collision; "Expiry date is required for perishable product <product> on new lot <lot_no>." for missing expiry. |
ADJ_VAL_010 |
When the adjustment-type's info.requiresDocument = true (e.g. theft write-off, large recall write-off, large breakage), at least one comment row (tb_stock_in_comment / tb_stock_out_comment) with non-empty attachments JSON array must exist. Carmen-docs ADJ_CRT_008. |
Submit | Reject with "Supporting document attachment is required for this adjustment reason." Form-side check uploads via drag-drop; server re-checks at submit. |
ADJ_VAL_011 |
Document date (si_date / so_date, or created_at fallback) falls within an open tb_period. Closed and locked periods reject per inventory INV_VAL_008. Carmen-docs ADJ_CRT_010 + ADJ_PRC_009. |
Submit | Reject with "Cannot post into period <YYMM>: period is <closed/locked>. Re-open the period (closed only) or post a current-period restatement (locked)." |
ADJ_VAL_012 |
Outbound (tb_stock_out) quantity at (location, product, lot) does not drive on-hand below zero — checked against the live cost-layer ledger at submit time per inventory INV_VAL_005. The adjustment document itself does not store available-balance; the check reads the ledger. Carmen-docs ADJ_VAL_001 + ADJ_VAL_004 + ADJ_PRC_010. |
Submit | Reject with "Outbound movement would drive on-hand at (location, product, lot) below zero. Available: <X>, requested: <Y>." Document stays draft; user reduces qty, picks different lot, or escalates. |
ADJ_VAL_013 |
Modification of a completed document is rejected — the document is immutable once posted. Edits route through a void + new-document compensating pattern per ADJ_POST_004. Carmen-docs ADJ_PRC_007 + ADJ_PRC_008. |
Edit | Reject with "Cannot edit a completed adjustment. Void and create a new compensating adjustment." |
ADJ_VAL_014 |
Soft-delete of a posted adjustment requires a compensating reversal first per inventory INV_VAL_013. Direct soft-delete without a compensating inventory transaction is rejected. |
Soft-delete | Reject with "Cannot soft-delete a posted adjustment without a compensating reversal." |
All monetary fields are stored as Decimal(20, 5) on tb_stock_in_detail.total_cost / tb_stock_out_detail.total_cost and the parallel cost-layer fields. Display rounding follows tenant policy (typically 2dp for currency, 3dp for quantity). The document header carries no total-cost column — totals are derived at read time. Rule IDs follow ADJ_CALC_NNN.
| Rule ID | Formula |
|---|---|
ADJ_CALC_001 (line total_cost) |
total_cost = qty × cost_per_unit on each tb_stock_in_detail / tb_stock_out_detail row. Stored at 5dp; displayed at 2dp. Carmen-docs ADJ_CALC_001. |
ADJ_CALC_002 (document inQty roll-up) |
header.inQty = Σ detail.qty over all tb_stock_in_detail rows for the parent tb_stock_in.id, scoped to non-soft-deleted rows. Derived at read time; no persisted column. Carmen-docs ADJ_CALC_002. |
ADJ_CALC_003 (document outQty roll-up) |
header.outQty = Σ detail.qty over all tb_stock_out_detail rows for the parent tb_stock_out.id. Derived at read time. Carmen-docs ADJ_CALC_003. |
ADJ_CALC_004 (document totalCost roll-up) |
header.totalCost = Σ detail.total_cost across the document's detail rows. Derived at read time. The carmen-docs ADJ_VAL_005 "total cost must match" check is degenerate against this derivation — there is no header value to mismatch. |
ADJ_CALC_005 (Weighted-average refresh on inbound adjustment) |
When posting tb_stock_in to a WA product: new_average = (prior_on_hand × prior_average + adj_qty × adj_cost) / (prior_on_hand + adj_qty), written onto the new inbound cost-layer row per inventory INV_CALC_007. Carmen-docs ADJ_CALC_005. |
ADJ_CALC_006 (FIFO outbound cost pick) |
When posting tb_stock_out for a FIFO product: iterate tb_inventory_transaction_cost_layer at (location_id, product_id) ordered by lot_seq_no ascending until out_qty is satisfied. Each consumed layer produces its own outbound cost-layer row; one detail line can span multiple cost-layer rows per inventory INV_CALC_005. |
ADJ_CALC_007 (Weighted-average outbound cost pick) |
When posting tb_stock_out for a WA product: cost_per_unit = current_average_cost(location, product) at post time. The current average is the most recent average_cost_per_unit at (location_id, product_id) on tb_inventory_transaction_cost_layer per inventory INV_CALC_006. |
ADJ_CALC_008 (Variance %) |
When the source of the adjustment is a physical / spot count: variance_% = ((physical_count − system_qty) / system_qty) × 100. Used by the count-rollup engine to flag oversize variances for Inventory Controller investigation. Carmen-docs ADJ_CALC_006. |
ADJ_CALC_009 (Cost variance) |
When the adjustment reason captures a per-line cost change (rare — typically used for re-classification adjustments): cost_variance = (new_cost − old_cost) × qty. Carmen-docs ADJ_CALC_007. |
ADJ_CALC_010 (Period impact) |
period_impact = Σ adjustment_qty × cost_per_unit over all completed tb_stock_in / tb_stock_out rows in the period, scoped to a location / reason-code / department for reporting. Read-model only; not persisted. Carmen-docs ADJ_CALC_009. |
ADJ_CALC_011 (Rounding) |
Half-up. Stored values use the column precision (Decimal(20, 5)); intermediate computations carry 5dp; on-the-fly aggregations re-read rounded values from each step. Mirrors inventory INV_CALC_012. |
A FIFO product P-1 at LOC-A with two existing lots in cost-layer order:
LOT-1 — 5 units remaining at ฿10.00 (lot_seq_no = 1)LOT-2 — 3 units remaining at ฿12.00 (lot_seq_no = 2)The Store Keeper raises a tb_stock_out with reason BREAKAGE (type = stock_out), one line: product_id = P-1, qty = 6. On submit, post fires:
tb_inventory_transaction (inventory_doc_type = stock_out, inventory_doc_no = tb_stock_out.id); one tb_inventory_transaction_detail with qty = -6, total_cost = -฿60 + -฿12 = -฿72.00, from_lot_no set (split — see below).ADJ_CALC_006 / INV_CALC_005): two outbound rows because consumption spans lots.
out_qty = 5, cost_per_unit = ฿10.00, total_cost = ฿50.00, lot_no = LOT-1, transaction_type = adjustment_out. LOT-1 remaining = 0.out_qty = 1, cost_per_unit = ฿12.00, total_cost = ฿12.00, lot_no = LOT-2, transaction_type = adjustment_out. LOT-2 remaining = 2.tb_stock_out_detail.total_cost = ฿62.00 (the application-layer roll-up of the cost-layer total, written at post for reporting); tb_stock_out_detail.cost_per_unit is informational (the FIFO-picked weighted average 62 / 6 = ฿10.33333).Dr <BREAKAGE expense GL account from tb_adjustment_type.info.glAccount> ฿62.00 / Cr Inventory ฿62.00.A WA product P-2 at LOC-A with current on-hand 100 at average ฿11.33333 (from prior receipts). Store Keeper finds 10 surplus units of an existing lot during a bin check, raises tb_stock_in with reason FOUND_STOCK (type = stock_in), one line: product_id = P-2, qty = 10, lot_no = LOT-X (existing), cost_per_unit = ฿11.33333 (auto-filled from lot's most recent cost-layer).
On submit (below threshold, auto-approve):
tb_inventory_transaction (inventory_doc_type = stock_in); tb_inventory_transaction_detail with qty = 10, cost_per_unit = ฿11.33333, total_cost = ฿113.33, current_lot_no = LOT-X.ADJ_CALC_005 / INV_CALC_007): one inbound row. in_qty = 10, cost_per_unit = ฿11.33333, transaction_type = adjustment_in, lot_no = LOT-X. New average: (100 × 11.33333 + 10 × 11.33333) / (100 + 10) = ฿11.33333 (unchanged because the found-stock cost matches the existing average). The new average is written onto the new layer row.(LOC-A, P-2, LOT-X) advances by 10 per inventory INV_CALC_004.Dr Inventory ฿113.33 / Cr <FOUND_STOCK gain / recovery GL account> ฿113.33.If the found stock had been declared at a different cost (say ฿12.00), the WA refresh would have computed (100 × 11.33333 + 10 × 12.00) / 110 = ฿11.39394 — and the cost-difference would represent a cost-revaluation captured on the new layer's average_cost_per_unit.
Rule IDs follow ADJ_AUTH_NNN. Authorization layers RBAC (role check) on top of tb_user_location scope (per-user location whitelist) plus a tenant-configured threshold ladder that routes large-cost-impact documents upward. The thresholds are tenant-configurable; defaults in the table below reflect typical hotel-chain configuration.
| Rule ID | Subject | Right | Constraint |
|---|---|---|---|
ADJ_AUTH_001 |
Store Keeper | Create tb_stock_in / tb_stock_out documents at draft |
Within tb_user_location scope for the document's location_id. Read / list scope filtered to the user's locations. |
ADJ_AUTH_002 |
Store Keeper | Submit (draft → in_progress or draft → completed) below the auto-approve threshold |
Auto-approve threshold typically ฿500 aggregate document cost. Below threshold: document auto-advances to completed, inventory transaction posts immediately. At or above threshold: routes to Inventory Controller's queue. SoD restriction per ADJ_AUTH_010. Mirrors inventory INV_AUTH_001 / INV_AUTH_002. |
ADJ_AUTH_003 |
Store Keeper | Submit a stock-in creating a new lot | Always routes for Controller approval regardless of cost impact, because new-lot creation by a Store Keeper is a sensitive event (it can mask cost manipulation). Below-threshold rule does not apply. |
ADJ_AUTH_004 |
Inventory Controller | Approve / post above-Store-Keeper-threshold documents | Inventory Controller fires the in_progress → completed transition for above-auto-approve documents within their scope. Above the Controller threshold (typically ฿10,000 aggregate cost), the document routes further to Finance per ADJ_AUTH_005. Mirrors inventory INV_AUTH_003. |
ADJ_AUTH_005 |
Finance | Approve above-Controller-threshold documents | Large recall write-offs, large damage write-offs, large theft write-offs route Controller → Finance. Finance fires the final approval in_progress → completed. Cost-impact preview shown alongside reason-code and GL-account mapping. Mirrors inventory INV_AUTH_005. |
ADJ_AUTH_006 |
Department Manager | Review of department-scoped adjustments | Read-only role variant for departmental oversight — does not approve, but receives notification on documents affecting their cost-centre (resolved via dimension JSON department entry). May add comments / flag for Controller / Finance investigation. |
ADJ_AUTH_007 |
Inventory Controller | Cancel a draft or in_progress document; void a completed document |
Cancel pre-post is no inventory effect — doc_status moves to cancelled and is terminal. Void post-completed requires a compensating reversal inventory transaction per ADJ_POST_004; doc_status moves to voided after the compensating transaction posts. |
ADJ_AUTH_008 |
System Administrator | Configure tb_adjustment_type reason codes (CRUD), set GL-account mapping in info.glAccount, set requiresDocument / requiresQualityCheck flags, set tenant thresholds for auto-approve / Controller / Finance |
Configuration-only role; cannot raise or approve adjustments. Changes apply prospectively; historical documents retain their reason-code snapshot per inventory-adjustment/01-data-model § 3 notes. Mirrors inventory INV_AUTH_008. |
ADJ_AUTH_009 |
Auditor | Read-only across all tb_stock_in / tb_stock_out documents, their detail rows, comments, attachments, and the resulting tb_inventory_transaction rows |
Full read scope including soft-deleted (deleted_at non-null) and voided documents. Lot-recall trace combines adjustment documents with good-receive-note and store-requisition data via the shared tb_inventory_transaction join. Sensitive-field export (cost-per-unit, vendor terms via the joined source documents) requires secondary approval per the audit-pattern. Mirrors inventory INV_AUTH_009. |
ADJ_AUTH_010 |
Segregation of duties — Receiver / Adjuster | A user submitting a tb_stock_out write-off MUST NOT be the same user who created the originating receipt (tb_good_received_note.created_by_id) for the lot being written off when cost impact exceeds the SoD threshold. Routine count-shortage write-offs below threshold are exempt. Mirrors inventory INV_AUTH_010. |
Enforced at submit. Server returns "You created the receipt for this lot; an independent adjuster must initiate the write-off (SoD)." |
The adjustment document has the five-state Prisma lifecycle draft → in_progress → completed → cancelled / voided (per enum_doc_status). The fan-out effects below fire on the in_progress → completed transition. Rule IDs follow ADJ_POST_NNN.
| Rule ID | Transition / Event | Effects |
|---|---|---|
ADJ_POST_001 |
draft → in_progress (submit) |
Validation ADJ_VAL_001–ADJ_VAL_011 run. If auto-approve (below threshold and not new-lot), transition cascades to completed and ADJ_POST_002 fires immediately. If above threshold or new-lot, document stays in_progress awaiting Controller / Finance approval. workflow_history appended with {stage: 'submitted', by: <actor>, at: <now>}; last_action = submitted. |
ADJ_POST_002 |
in_progress → completed (auto or approval) |
Posting fires. For each detail line, write to the inventory ledger: (1) one tb_inventory_transaction row with inventory_doc_type = stock_in / stock_out, inventory_doc_no = tb_stock_in.id / tb_stock_out.id; (2) one tb_inventory_transaction_detail row (qty signed by direction per inventory INV_VAL_004; cost_per_unit from the document for stock-in, picked at post for stock-out; total_cost; from_lot_no / current_lot_no resolved per direction); (3) one or more tb_inventory_transaction_cost_layer rows depending on costing method (single inbound row for stock-in; FIFO multi-row or WA single row for stock-out per ADJ_CALC_005 / ADJ_CALC_006 / ADJ_CALC_007); (4) GL entry — Dr/Cr per the adjustment-type's info.glAccount and the document's dimension.department. The detail row's inventory_transaction_id is stamped with the new transaction id. workflow_history appended {stage: 'completed', by: <actor>, at: <now>}. Mirrors inventory INV_POST_001 (stock-in) and INV_POST_002 (stock-out). |
ADJ_POST_003 |
draft / in_progress → cancelled (pre-post abandon) |
Document doc_status = cancelled. No inventory effect; no compensating transaction needed. Reason text required on the cancel action; recorded in workflow_history. The document remains in the database (soft-delete optional via tenant policy) for audit. |
ADJ_POST_004 |
completed → voided (post-fact void) |
Two-step. First, a compensating tb_stock_in (if voiding a stock-out) or tb_stock_out (if voiding a stock-in) is raised with the same lines and info.voidsAdjustmentId = <original_id>; it submits and posts per ADJ_POST_002, writing a reversal tb_inventory_transaction. Only after the compensating post succeeds does the original document's doc_status move to voided. The original inventory transaction is not edited per inventory INV_POST_012. workflow_history records both actions. |
ADJ_POST_005 |
Period-rollover guard | If the document's si_date / so_date falls in a closed / locked tb_period at submit time, ADJ_VAL_011 rejects per inventory INV_VAL_008. Re-opening a closed period to allow a back-post is a Finance Manager privilege per inventory INV_AUTH_006. |
ADJ_POST_006 |
Count-rollup auto-post | When physical-count / spot-check confirms variance lines and the Inventory Controller commits the count, the system auto-creates one tb_stock_in (overage rollup) and/or one tb_stock_out (shortage rollup) with info.countId = <count_uuid>, auto-advances to completed under Controller authority (skipping the explicit Store Keeper queue), and writes the inventory transaction per ADJ_POST_002. Per inventory INV_XMOD_003 / INV_XMOD_004. |
ADJ_POST_007 |
Direct-cost location gate | Posting to a direct-cost location is rejected at submit per ADJ_VAL_003 and inventory INV_VAL_009. The screen filters the location picker to inventory- / consignment-type; direct API submission with a direct-cost location_id returns the validation error. |
ADJ_POST_008 |
Consignment location handling | Posting tb_stock_in to a consignment location writes the tb_inventory_transaction with a consignment-flagged cost-layer row (per dimension / info) and does not debit Inventory or credit AP — memo-only per inventory INV_POST_004. Posting tb_stock_out from consignment posts COGS + AP simultaneously per inventory INV_POST_005. Consignment-to-inventory ownership transfers require an explicit re-classification stock-in document per inventory INV_VAL_010. |
ADJ_POST_009 |
Soft-delete sequencing | Soft-delete on a completed tb_stock_in / tb_stock_out is allowed only after the compensating reversal posts per ADJ_VAL_014 and inventory INV_VAL_013. Direct soft-delete bypassing the void flow is rejected. |
ADJ_POST_010 |
UI status colour coding | Per carmen-docs ADJ_UI_002: draft amber; in_progress blue; completed green; cancelled grey; voided red. Status badge renders on list and detail views per inventory-adjustment/01-data-model § 2 enum_doc_status reality. |
State diagram (per Prisma enum_doc_status):
[*] → draft ──submit──> in_progress ──approve──> completed (terminal active)
│ │ │
│ │ └──void (compensating)──> voided (terminal)
│ │
└──cancel──> cancelled (terminal)
│
└──cancel──> cancelled (terminal)
The Prisma enum enum_doc_status documented above is what the live UI uses. The carmen/docs set (INV-ADJ-Business-Requirements.md) describes the intended status set as a three-state lifecycle (Draft → Posted → Void). The table below maps every observable live-UI status to its BRD equivalent so testers and developers can reconcile the two without ambiguity. Sources: Test_case/System_Process/tx-06-stock-in-adj.md (capture date 2026-04-27) and Test_case/System_Process/tx-07-stock-out-adj.md (capture date 2026-04-27).
Diff legend: ✅ match · 🟡 renamed · 🔴 new in live UI · 🔵 BRD only.
| Live UI status | BRD equivalent | Diff | Notes |
|---|---|---|---|
draft |
Draft |
✅ match | Initial editable state. Both stock-in and stock-out documents start here; creator enters lines, reason code, department, attachments. No inventory effect. |
in_progress |
— (collapsed into Draft in BRD three-state model) |
🔴 new in live UI | Submitted for approval; document is locked to the creator while awaiting Controller or Finance review. Both stock-in and stock-out variants use this state for the above-threshold or new-lot routing gate. BRD's three-state model has no equivalent — it collapses this into Draft. |
completed |
Posted |
🟡 renamed | Terminal active state; the in_progress → completed transition fires the posting event: inventory transaction written, cost-layer rows created (new lot for stock-in; oldest-lot-first consumption for stock-out), GL entry generated. Immutable after posting. Applies to both stock-in (direction IN) and stock-out (direction OUT). |
cancelled |
— (no BRD equivalent) | 🔴 new in live UI | Document cancelled before posting — creator or reviewer abandoned. No inventory effect. Terminal. BRD three-state model has no explicit pre-post cancel; documents in BRD either advance to Posted or are Void. |
voided |
Void |
🟡 renamed | Post-fact void via compensating reversal pattern (ADJ_POST_004). The original completed document moves to voided only after a compensating tb_stock_in (if voiding a stock-out) or tb_stock_out (if voiding a stock-in) has posted. Original inventory transaction is not edited per inventory INV_POST_012. Applies to both directions. |
| — | Draft (editable + submitted, pre-post) |
🔵 BRD only | The BRD Draft state covers both the carmen-wiki draft and in_progress states — BRD does not distinguish the editable pre-submit from the submitted-awaiting-approval window. |
⚠️ Discrepancy — Two parallel document trees vs single
InventoryAdjustmententity: The carmen/docs BRD (INV-ADJ-Business-Requirements.md) and interface definitions describe a singleInventoryAdjustmententity with atype: 'IN' | 'OUT'discriminator and a three-state lifecycle (Draft → Posted → Void). The live implementation uses two parallel document tables —tb_stock_in(direction IN) andtb_stock_out(direction OUT) — each with the same five-stateenum_doc_status, joined by the shared reason-code classifiertb_adjustment_typewhoseenum_adjustment_type(stock_in/stock_out) gates which document tree a given reason can appear on. The BRD single-entity view is a read-model union; the two-table split is the canonical persistence layer. Sources:Test_case/System_Process/tx-06-stock-in-adj.md(capture date 2026-04-27) andTest_case/System_Process/tx-07-stock-out-adj.md(capture date 2026-04-27).
⚠️ Discrepancy — Status flow TBC markers in Test_case sources: Both
Test_case/System_Process/tx-06-stock-in-adj.mdandTest_case/System_Process/tx-07-stock-out-adj.mdannotate the status flow withTBC — verify live UI statuses. The five-state lifecycle (draft → in_progress → completed → cancelled / voided) documented in inventory-adjustment/01-data-model § 4 is the canonical Prisma schema reality; the TBC markers indicate the live UI label phrasing for each state has not yet been verified against the running system. Testers should confirm the displayed badge text matches the enum values above. Sources:Test_case/System_Process/tx-06-stock-in-adj.md(capture date 2026-04-27) andTest_case/System_Process/tx-07-stock-out-adj.md(capture date 2026-04-27).
⚠️ Discrepancy — Lot impact differs by direction: Stock-in adjustments (
tb_stock_in) always create a new lot for the adjusted-in quantity (new lot created at the inventory location; lot number and metadata may be entered manually or system-generated — TBC perTest_case/System_Process/tx-06-stock-in-adj.md, capture date 2026-04-27). Stock-out adjustments (tb_stock_out) consume existing lots oldest-first (FIFO lot consumption; if adjustment qty spans multiple lots, each is reduced in chronological order until adjustment qty is satisfied — perTest_case/System_Process/tx-07-stock-out-adj.md, capture date 2026-04-27). This directional asymmetry means new-lot stock-in always routes for Inventory Controller approval perADJ_AUTH_003regardless of cost, whereas stock-out's lot selection is engine-driven (not user-entered) at post time.
ℹ️ Note — Unit cost entry differs by direction: For stock-in,
cost_per_unitis user-entered on the document line (required; used for the new lot's cost layer and WA/FIFO calculation). For stock-out, the user-enteredcost_per_unitis a draft preview only — the authoritative cost is picked at post time by the costing engine (ADJ_CALC_006/ADJ_CALC_007). BRDADJ_VAL_003applies to stock-in; for stock-out, the validation is on the preview value only. Sources:Test_case/System_Process/tx-06-stock-in-adj.md(capture date 2026-04-27) andTest_case/System_Process/tx-07-stock-out-adj.md(capture date 2026-04-27).
Rule IDs follow ADJ_XMOD_NNN.
| Rule ID | Related module | Rule |
|---|---|---|
ADJ_XMOD_001 |
inventory | Every completed tb_stock_in / tb_stock_out writes one tb_inventory_transaction row per detail line with inventory_doc_type = stock_in / stock_out and inventory_doc_no = tb_stock_in.id / tb_stock_out.id. The detail's inventory_transaction_id is stamped with the new transaction id (no Prisma @relation; application-resolved). The cost-layer ledger receives enum_transaction_type = adjustment_in / adjustment_out rows. Per inventory INV_XMOD_005. |
ADJ_XMOD_002 |
physical-count | A completed physical count generates count-difference lines per (location_id, product_id, lot_no). On Controller commit, the system auto-creates: (a) one tb_stock_in for overage lines (if any) with reason COUNT_OVERAGE; (b) one tb_stock_out for shortage lines (if any) with reason COUNT_SHORTAGE. Both auto-advance to completed under Controller authority. The originating count document's tb_count_stock.status transitions to completed_posted. Cross-references inventory INV_XMOD_003. |
ADJ_XMOD_003 |
spot-check | Same posting path as physical-count but scoped to a subset of products / locations. Spot-check variances post under the same tb_stock_in / tb_stock_out rollup pattern with reason COUNT_OVERAGE / COUNT_SHORTAGE (or tenant-configured spot-check-specific reasons). Cross-references inventory INV_XMOD_004. |
ADJ_XMOD_004 |
good-receive-note | Vendor-replacement of damaged stock (e.g. vendor sends a free replacement outside the credit-note path) is captured as tb_stock_in with reason VENDOR_FREE_REPLACEMENT. The originating GRN is not edited; the replacement is a new lot on the inventory ledger. Where the vendor concedes a price reduction post-receipt, the credit-note path is preferred (lives in good-receive-note credit-note flow per inventory INV_XMOD_007), not an adjustment. |
ADJ_XMOD_005 |
costing | Costing reads the adjustment-driven cost-layer rows for FIFO consumption ordering and weighted-average refresh: transaction_type = adjustment_in rows create new layers (FIFO) or refresh the moving average (WA) per ADJ_CALC_005; transaction_type = adjustment_out rows consume FIFO layers or read the current WA per ADJ_CALC_006 / ADJ_CALC_007. Adjustment-driven cost changes feed COGS and inventory valuation in the same way as GRN / SR movements per inventory INV_XMOD_006. |
ADJ_XMOD_006 |
product | The product's costing_method (FIFO vs WEIGHTED_AVERAGE) determines the post-time cost-pick on tb_stock_out per ADJ_CALC_006 / ADJ_CALC_007. Changing a product's costing method with non-zero on-hand is blocked per inventory INV_XMOD_009 — the same constraint applies whether the on-hand was built up via GRN, SR transfer-in, or stock-in adjustment. |
ADJ_XMOD_007 |
Finance / GL | Adjustment-generated journal entries reconcile to the inventory sub-ledger at period close per inventory INV_XMOD_008. The adjustment-type's info.glAccount determines the expense / gain account (e.g. BREAKAGE → 6510 — Breakage & Damage Expense; FOUND_STOCK → 4905 — Inventory Recovery; EXPIRY_WRITE_OFF → 6520 — Expiry Write-Off; THEFT_WRITE_OFF → 6530 — Shrinkage Loss). Net inventory-adjustment impact on the period is summed by reason for the variance report. |
ADJ_XMOD_008 |
inventory (cross-link) | The list of valid enum_inventory_doc_type values that an adjustment can produce is {stock_in, stock_out}. The list of valid enum_transaction_type cost-layer values from an adjustment is {adjustment_in, adjustment_out}. The adjustment module does not produce any of the other inventory transaction or cost-layer types (good_received_note, store_requisition, transfer_in, transfer_out, issue, credit_note_*, eop_*, close_period, open_period). |
ADJ_XMOD_009 |
All movement-generating modules | Every adjustment writes via the same tb_inventory_transaction API as GRN / SR / count / credit-note posts. There is no adjustment-specific ledger bypass; the polymorphic inventory_doc_type is the only differentiator at the ledger level. This is the chokepoint that makes the adjustment audit trail consistent with other movement sources per inventory INV_XMOD_010. |
../carmen/docs/inventory-adjustment/INV-ADJ-Business-Requirements.md — source of the ADJ_CRT_*, ADJ_VAL_*, ADJ_PRC_*, ADJ_UI_*, ADJ_CALC_* rule catalogue this page realigns to the Prisma reality; the InventoryAdjustment / AdjustmentReason interfaces grounding § 5 of inventory-adjustment/01-data-model.../carmen/docs/inventory-adjustment/INV-ADJ-Business-Logic.md — process flows including the "Adjust Inventory" Mermaid diagram in § 4.4; cross-checked into the validation, calculation, and posting flow above (note: carmen/docs's three-state Draft → Posted → Void was realigned to the five-state Prisma reality).../carmen/docs/inventory-adjustment/INV-ADJ-PRD.md — UI / functional requirements (status colour-coding, lot selection dialog, FIFO override) referenced under ADJ_POST_010.../carmen/docs/inventory-adjustment/INV-ADJ-Overview.md — module roles & responsibilities; cross-referenced for the persona scope under ADJ_AUTH_* (note: 6 personas collapsed to 4 groups — Store Keeper, Inventory Controller, Finance, Audit/Config).tb_stock_in, tb_stock_out, tb_adjustment_type, etc.), enums (enum_adjustment_type, enum_doc_status, enum_last_action, enum_comment_type), and the divergence catalogue that Section 1 (structural notes), Section 2 (validation), Section 5 (posting state diagram) rely on.INV_VAL_005 no-negative-balance, INV_VAL_008 period gate, INV_VAL_009 direct-cost gate, INV_CALC_005 / INV_CALC_006 cost picks, INV_CALC_007 WA refresh, INV_POST_001 / INV_POST_002 inbound/outbound posting, INV_AUTH_001–INV_AUTH_010 authorization, INV_XMOD_003 / INV_XMOD_004 / INV_XMOD_005 count and adjustment cross-module rules) this page references rather than duplicates.../carmen-turborepo-backend-v2/apps/ — the stock-in / stock-out service module is the implementation hook for these rules (validation, threshold-based routing, transactional post, count-rollup auto-creation, GL-mapping resolution).../carmen-inventory-frontend-e2e/tests/031-adjustment-type.spec.ts — adjustment-type reason-code admin (CRUD, validation, code uniqueness) — exercises ADJ_AUTH_008 configuration scope.