At a Glance
Rule families:SR_VAL_*validation ·SR_AUTH_*permission ·SR_CALC_*calc ·SR_POST_*posting ·SR_XMOD_*cross-module
Rule count: approximately 60 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 captures the operational business rules that govern a Store Requisition (SR) document through its lifecycle: input validation at create / edit / submit / approve / issue time, the small set of calculation rules (quantity invariants and the source-costed unit cost feed; the SR is a quantity document, not a price document — see store-requisition/01-data-model § 5 items 3 and 4), authorization gates by role and document status, posting effects on each transition of enum_doc_status, and cross-module rules with inventory, costing, recipe, and good-receive-note. The SR is the system of record for internal stock movement between locations: until it commits (in_progress → completed), no inventory is decremented at source and no expense / inventory entry is raised at destination; once committed, the source location's on-hand falls by issued_qty per line, the destination either receives the same quantity (sr_type = transfer) or absorbs the cost on its cost-centre (sr_type = issue), and journal entries are written through the linked tb_inventory_transaction.
Two structural points colour every rule below and are worth restating up front. First, the three quantities on tb_store_requisition_detail — requested_qty, approved_qty, issued_qty — together carry the full story across the lifecycle, with the strict invariant 0 ≤ issued_qty ≤ approved_qty ≤ requested_qty enforced at the value level on every save and transition; this is the line-level analogue of GRN's received_qty / accepted_qty. Second, unlike GRN where approved / rejected / committed are separate header statuses, the SR collapses both approval and fulfilment under the single in_progress status — the workflow_current_stage field is what distinguishes "awaiting approver" from "awaiting store keeper". So rules referencing "the approval phase" or "the issue phase" gate on (doc_status = 'in_progress', workflow_current_stage = <approval-stage-slug>) rather than on a top-level status alone.
Rule IDs follow SR_VAL_NNN. Header rules (001–005) run on every save and on submit; line rules (006–010) run per line on save, on submit, and on each approval / issue action; aggregate / at-commit rules (011–014) run only at the in_progress → completed transition (the single posting event).
| Rule ID | Condition | When enforced | Error / behaviour |
|---|---|---|---|
SR_VAL_001 |
tb_store_requisition.sr_no is non-null and uniquely held against non-soft-deleted SRs (@@unique([sr_no, deleted_at])). Unlike GRN's grn_no which is nullable, SR requires a reference number from draft onward. |
Create, save, submit, commit | Reject with "SR reference number is required and must be unique." |
SR_VAL_002 |
tb_store_requisition.from_location_id and to_location_id are both non-null at submit time, reference non-soft-deleted tb_location rows, and differ (from_location_id ≠ to_location_id — no SR from a location to itself). |
Save (warn for nulls), submit (block) | Reject with "Source and destination locations are required and must differ." |
SR_VAL_003 |
Movement-type ↔ destination-type compatibility: when sr_type = 'issue', tb_location[to_location_id].location_type = 'direct'; when sr_type = 'transfer', tb_location[to_location_id].location_type = 'inventory'. Source must be inventory for both. |
Save line, submit, commit | Reject with "Movement type <sr_type> requires a <expected> destination; selected destination is <actual>." Maps to the enum_location_type / enum_sr_type pairing. |
SR_VAL_004 |
sr_date is non-null and not in the future beyond tenant tolerance; expected_date (if set) is on or after sr_date. |
Save, submit | Reject with "Requisition date is required; expected date must be on or after requisition date." |
SR_VAL_005 |
tb_store_requisition.requestor_id and department_id are non-null at submit, reference active users and departments, and the requester is a member of the named department (RBAC join). |
Submit | Reject with "Requester and requesting department are required and the requester must belong to that department." |
SR_VAL_006 |
Each tb_store_requisition_detail row has non-null product_id referencing an active, non-soft-deleted tb_product whose status permits issue (not discontinued / inactive at the source location). |
Save line, submit | Reject the line with "Product is required and must be active for issue from the source location." |
SR_VAL_007 |
The (store_requisition_id, product_id, dimension) triple is unique across non-soft-deleted detail rows on the same SR (per the SRT1_* index). The same product on the same SR with a different dimension (split cost-centre allocation) is permitted as two rows; identical product+dimension is a duplicate. |
Save line, submit | Reject the line with "This product is already on the SR with the same cost-dimension allocation; edit the existing line or use a different dimension to split." |
SR_VAL_008 |
Quantity invariant on every line: requested_qty ≥ 0 always; once submitted, 0 ≤ approved_qty ≤ requested_qty; once issued, 0 ≤ issued_qty ≤ approved_qty. Negative quantities are rejected at all stages; requested_qty = 0 is rejected at submit (a zero-line is not a request). |
Save line (warn for 0 / null), submit (block), approve, issue | Reject the line with "Quantities must satisfy 0 ≤ issued_qty ≤ approved_qty ≤ requested_qty; requested quantity must be greater than zero at submit." |
SR_VAL_009 |
Source availability at submit: requested_qty ≤ tb_inventory_status[from_location_id, product_id].quantity_on_hand minus reservations from other open SRs. Tenant config controls whether this is enforced as a hard block or a soft warning ("BR-Stock-01" in PRD). |
Submit, commit | When hard: reject the line with "Requested quantity <requested_qty> exceeds available stock <on_hand − reserved> at source location <from_location_name>." When soft: warn and allow. |
SR_VAL_010 |
Approval invariant: approved_qty ≤ requested_qty is the value cap; additionally an approver MUST NOT raise approved_qty above requested_qty (only downward trim or zero is allowed). Rejection sets approved_qty = 0 and requires non-empty reject_message. |
Approve action | Reject the approval with "Approved quantity cannot exceed requested quantity; to grant more, the requester must amend and resubmit." Reject the line-rejection if reject_message is empty. |
SR_VAL_011 |
At commit, the SR has at least one non-soft-deleted tb_store_requisition_detail row with approved_qty > 0. A document with no approved lines cannot commit. |
Commit | Reject with "SR must contain at least one approved line (approved quantity > 0) before it can be committed." |
SR_VAL_012 |
At commit, for every line with issued_qty > 0 and product.product_type = 'inventory', the row's inventory_transaction_id is populated and the linked tb_inventory_transaction_detail carries lot_no (system-generated or store-keeper-selected for lot-controlled items) and cost_per_unit (sourced from the source location's costing method per [costing](/en/inventory/costing)). Lot data is required for lot-controlled / perishable items; cost_per_unit is required for all inventory items. |
Commit | Reject with "Issue posting requires lot information for lot-controlled items and a valid unit cost on every issued line; line <seq> is missing data on the linked inventory transaction." |
SR_VAL_013 |
At commit, the source location has sufficient on-hand to cover every issued_qty. The check re-runs against the live tb_inventory_status at the moment of issue (not against the snapshot taken at submit) to prevent posting negative stock. |
Commit | Reject with "Source stock-out at issue: line <seq> requires <issued_qty> but only <on_hand> is available at <from_location_name>. Reduce issued_qty to the available quantity or cancel the line." |
SR_VAL_014 |
At commit, the posting date falls in an open accounting period (the [costing](/en/inventory/costing) / finance period table is not closed). Closed-period commits are rejected; the SR remains at in_progress for re-attempt after period reopen or with a different posting date. |
Commit | Reject with "Cannot commit SR <sr_no>: posting date falls in a closed accounting period." |
The SR is a quantity document, not a price document, so the calculation surface is small. All quantities are stored as Decimal(20, 5) at the row level; display rounding is half-up to 3 decimals for quantities. There are no monetary columns on tb_store_requisition or tb_store_requisition_detail — unit cost and line total are read from the linked inventory transaction at display time (see store-requisition/01-data-model § 5 items 3 and 4).
Rule IDs follow SR_CALC_NNN.
| Rule ID | Formula |
|---|---|
SR_CALC_001 (quantity invariant) |
0 ≤ issued_qty ≤ approved_qty ≤ requested_qty on every active (deleted_at IS NULL) detail row. The invariant is enforced at write time per SR_VAL_008; it is also a read-time integrity check used by the costing roll-up and the variance dashboards. |
SR_CALC_002 (variance — requested vs issued) |
variance_qty = requested_qty − issued_qty per line; positive variance is a short-fulfilment (approver trim, source stock-out, or partial issue). The variance does not write back to the line — it is computed for the variance dashboard (inventory / outlet variance reporting). |
SR_CALC_003 (variance — approved vs issued) |
fulfilment_gap = approved_qty − issued_qty per line; positive gap is an at-issue shortfall (the store keeper could not release the full approved amount). This is the metric the store keeper closes out (full fulfilment when fulfilment_gap = 0). |
SR_CALC_004 (unit cost feed from source) |
unit_cost = tb_inventory_transaction_cost_layer.cost_per_unit at the moment of issue, picked per the source location's costing method (FIFO or moving-average per costing). The SR does not compute or store the unit cost itself — the cost is owned by the costing module on the inventory-transaction side; the SR line reaches it via inventory_transaction_id. |
SR_CALC_005 (line total — display only) |
line_total = issued_qty × unit_cost — computed for display in the detail view and for the journal-entry posting on commit, but not persisted on tb_store_requisition_detail. |
SR_CALC_006 (header total — display only) |
header_total = Σ (issued_qty × unit_cost) across active issued lines — computed for display and for the journal-entry total, but not persisted on tb_store_requisition. |
SR_CALC_007 (rounding mode) |
Half-up to 3 decimals for quantities (Decimal(20, 5) storage). For the display-only monetary fields, half-up to the source location's currency precision (typically 2 dp). The inventory transaction is responsible for storing the rounded monetary values. |
Outlet Main Kitchen (tb_location.location_type = 'direct') raises an SR against Central Store (tb_location.location_type = 'inventory'); sr_type = issue. Three lines.
requested_qty = 25.000, source on-hand = 100.000.
approved_qty = 25.000.issued_qty = 25.000.cost_per_unit = ฿42.50 per kg.requested − issued = 0; fulfilment gap = 0.25.000 × ฿42.50 = ฿1,062.50.requested_qty = 15.000, source on-hand = 12.000.
approved_qty = 12.000; approved_message = "trimmed to source on-hand".issued_qty = 12.000.cost_per_unit = ฿28.00 per kg.requested − issued = 3.000 (short by 3; outlet asked for 15, got 12).12.000 × ฿28.00 = ฿336.00.requested_qty = 10.000, source on-hand at submit = 10.000 but at issue = 6.000 (another SR consumed 4).
approved_qty = 10.000.SR_VAL_013): can only release 6; records issued_qty = 6.000; writes a system comment "issued 6 of 10; 4 short due to concurrent consumption".cost_per_unit = ฿31.50 per kg.requested − issued = 4.000; fulfilment gap = 4.000.6.000 × ฿31.50 = ฿189.00.Header roll-up (display only):
Σ line_total = 1,062.50 + 336.00 + 189.00 = ฿1,587.50 — this is the cost moved out of source inventory and either onto Main Kitchen's cost-centre expense (sr_type = issue, as here) or into Main Kitchen's inventory account (had this been sr_type = transfer).inventory_doc_type = store_requisition, all outQty rows at source. For sr_type = transfer (not this case), three paired inQty rows would land at the destination too.Rule IDs follow SR_AUTH_NNN. Authorization is enforced by RBAC at the API layer plus workflow-stage gating via tb_store_requisition.user_action.execute. Role names mirror the carmen/docs RBAC table (Requester / Approver / Fulfiller / Receiver / Inventory Controller / Finance Manager / System Administrator). Segregation-of-duties is enforced between Requester and Approver, and between Approver and Fulfiller; the Requester ≠ Approver rule is the formal SoD gate.
| Rule ID | Subject | Right | Constraint |
|---|---|---|---|
SR_AUTH_001 |
Requester (Outlet Manager) | Create SR (doc_status = draft) |
Requester must be a member of department_id; from_location_id and to_location_id must be locations the requester is permitted to act between. |
SR_AUTH_002 |
Requester | Edit SR, add / edit / delete lines | Only while doc_status = draft and the document has not been submitted. Once at in_progress, the document is no longer in the requester's hands. |
SR_AUTH_003 |
Requester | Submit (draft → in_progress) |
Passes Section 2 validation through SR_VAL_001–SR_VAL_009. On submit, the workflow engine routes the document to the first approval stage and populates user_action.execute from that stage's permitted users. |
SR_AUTH_004 |
Requester | Withdraw / cancel own draft | draft → cancelled is allowed for the requester on their own draft with a reason. in_progress → cancelled is allowed for the requester only when the workflow is still at the first approval stage (no approver has yet acted); past that point, only an approver can reject the SR. |
SR_AUTH_005 |
Approver (Department Head) | Approve / trim / reject line, send back for correction | Only while doc_status = in_progress and workflow_current_stage is an approval stage where the approver is in user_action.execute. May set approved_qty ∈ [0, requested_qty] (per SR_VAL_010), set reject_message on a zeroed line, or review_message to send back for correction (returns the document to an earlier stage, typically the requester). Approver MUST NOT be the same user as the requester (SR_AUTH_011). |
SR_AUTH_006 |
Approver | Split & reject (partially approve at the SR level) | Some lines approved, others rejected — both happen in the same approve action; the workflow advances if at least one line has approved_qty > 0, otherwise the document moves to cancelled via the all-lines-rejected path. |
SR_AUTH_007 |
Fulfiller (Store Keeper) | Pick, record issued_qty, select lots, commit (in_progress → completed) |
Only while doc_status = in_progress and workflow_current_stage is the fulfilment stage where the fulfiller is in user_action.execute. issued_qty ≤ approved_qty per line (SR_VAL_008). Lot selection writes onto the linked tb_inventory_transaction_detail. On commit, the cross-module fan-out fires per Section 5. Fulfiller MUST NOT be the same user as the approver on the same SR (SR_AUTH_012). |
SR_AUTH_008 |
Receiver (destination outlet representative) | Confirm physical receipt, flag discrepancies, close out | Available once the SR is completed (or at the destination-acknowledgement workflow stage if the tenant has one before completed). The Receiver does not change doc_status but can append user / system comments on header / line and raise a discrepancy event that escalates to the Inventory Controller. |
SR_AUTH_009 |
Inventory Controller | Variance review, period-end signoff, void administrative | Read across all statuses; may void a draft or in_progress SR with a reason (* → voided) on audit grounds (SR_AUTH_013). May not edit lines or change quantities directly; corrections go through [inventory-adjustment](/en/inventory/inventory-adjustment). |
SR_AUTH_010 |
Finance Manager | Cost-centre / journal-entry verification, period close | Read across all statuses. May reject a commit attempt at SR_VAL_014 (closed-period block) or after-the-fact dispute a posting via the inventory-adjustment path. Period-end signoff confirms that all completed SRs in the period have valid journal entries. |
SR_AUTH_011 |
Segregation of duties — Requester ≠ Approver | The user approving an SR (approved_by_id on any line at the approve action) MUST NOT be the same user as tb_store_requisition.requestor_id. |
Enforced at the approve action. The system rejects the action with "You raised this requisition; another user must approve it."; no approved_qty write, no workflow advance. |
SR_AUTH_012 |
Segregation of duties — Approver ≠ Fulfiller | The user committing the SR (last_action_by_id on the in_progress → completed transition) MUST NOT be the same user as any line's approved_by_id. |
Enforced at commit. The system rejects with "You approved a line on this requisition; another user must issue the goods.". Tenant config may relax this for low-value SRs below threshold. |
SR_AUTH_013 |
Inventory Controller / System Administrator | Void SR (draft → voided or in_progress → voided) |
Allowed only before commit. A completed SR cannot be voided — corrections after commit go through [inventory-adjustment](/en/inventory/inventory-adjustment). voided is terminal; cancelled is also terminal but reserved for the user-initiated retraction path (requester withdrawal or approver full-rejection). |
SR_AUTH_014 |
Workflow-derived authorization | Stage-gated action | The set of users in tb_store_requisition.user_action.execute at the current workflow_current_stage is the only set permitted to advance the document; all other action attempts are rejected with "You are not assigned to act on this requisition at the current stage.". |
Status values are the literal members of the shared enum_doc_status documented in store-requisition/01-data-model § 4: draft, in_progress, completed, cancelled, voided. The full lifecycle is therefore draft → in_progress → completed, with cancelled and voided as the two terminal cancellation paths from any pre-commit state. The single posting event is the in_progress → completed transition; nothing posts at draft or while the workflow is still in approval / pre-fulfilment stages of in_progress. There is no submitted, approved, partially_approved, fulfilled, or rejected value at the Prisma level — those are workflow-stage labels under in_progress, not header statuses (legacy carmen/docs 6-state model is divergent — see store-requisition/01-data-model § 5 item 1).
Rule IDs follow SR_POST_NNN.
| Rule ID | Transition / Event | Effects |
|---|---|---|
SR_POST_001 |
Create (→ draft) |
Insert tb_store_requisition with doc_status = draft, doc_version = 0, sr_no assigned per tenant numbering policy. Append to workflow_history: { stage: 'draft', action: 'created', by, at }. No inventory, no GL, no reservation. |
SR_POST_002 |
Submit (draft → in_progress) |
Set doc_status = in_progress, last_action = submitted, last_action_at_date = now(), last_action_by_id = user. Initialise workflow_current_stage to the first approval stage; populate user_action.execute from that stage. Append workflow_history. Append per-line history: { seq: N, name: '<stage>', status: 'submit', by_id, by_name, at_date }. Optionally raise a soft reservation against source on-hand (tenant-configurable); no hard inventory write. |
SR_POST_003 |
Approve line (within in_progress) |
On the per-line approve action: set approved_qty, approved_by_id, approved_by_name, approved_date_at, approved_message. Append per-line history. When all lines on the current stage have been actioned, advance workflow_current_stage to the next stage (next approval level, or fulfilment stage if approvals are complete); refresh user_action.execute. Header doc_status remains in_progress throughout. |
SR_POST_004 |
Reject line / send back (within in_progress) |
Per-line reject: set approved_qty = 0, reject_by_id, reject_by_name, reject_date_at, reject_message (mandatory). Send-back: set review_by_id, review_by_name, review_date_at, review_message; the workflow returns the SR to an earlier stage (typically the requester) and the document remains in_progress. If all active lines are rejected (Σ approved_qty = 0 across active lines), the document moves in_progress → cancelled automatically (SR_POST_009). |
SR_POST_005 |
Commit (in_progress → completed) — the posting event |
Set doc_status = completed, last_action = approved (or submitted per workflow), last_action_at_date = now(). Append workflow_history. Then the cross-module fan-out fires atomically: see SR_POST_006–SR_POST_008. |
SR_POST_006 |
Commit — inventory side (cross-ref inventory) | For each line with issued_qty > 0 and inventory-type product, insert a tb_inventory_transaction row (inventory_doc_type = store_requisition) plus its tb_inventory_transaction_detail child(ren) carrying lot_no, expiry_date, cost_per_unit. Stamp the inserted id on tb_store_requisition_detail.inventory_transaction_id. For sr_type = issue: write a single OUT row at from_location_id; source on-hand at (from_location_id, product_id) decrements by issued_qty (and issued_base_qty where the UoM differs). For sr_type = transfer: write a paired OUT at source + IN at destination; source on-hand decrements, destination on-hand increments by the same quantity. Cost-layer rows are consumed at source per the source location's costing method (costing FIFO consumes the oldest layer first; moving-average leaves the per-unit cost unchanged but decrements quantity). Lot selection is preserved on the transaction detail for traceability. |
SR_POST_007 |
Commit — GL side (cross-ref Finance) | Post journal entries through the linked inventory transactions. For sr_type = issue: Dr the destination's consumption expense account on its cost-centre (per tb_store_requisition.dimension / to_location.cost_centre) at Σ issued_qty × cost_per_unit; Cr the source location's inventory account at the same amount. For sr_type = transfer: Dr the destination location's inventory account; Cr the source location's inventory account. Both legs are stamped with the SR sr_no for traceability. Entries must balance (Σ Dr = Σ Cr); imbalanced postings are blocked by the finance module. |
SR_POST_008 |
Commit — variance / vendor performance feed | Per-line requested_qty − issued_qty and approved_qty − issued_qty are emitted as variance events that feed outlet variance reporting and supply-planning. The SR module itself does not store the variance — it surfaces from a join across the three columns on the detail row. |
SR_POST_009 |
Cancel (draft → cancelled or in_progress → cancelled) |
Set doc_status = cancelled, last_action_at_date = now(). Triggered by: (a) requester withdrawal of own draft / own SR at the first approval stage (SR_AUTH_004); (b) automatic when all lines are rejected by approvers (SR_POST_004 tail); (c) explicit "reject SR" by an authorised approver at any approval stage. No inventory or GL impact (the SR never posted). Lines and per-line signatures remain readable for audit. Terminal. |
SR_POST_010 |
Void (draft → voided or in_progress → voided) |
Set doc_status = voided, last_action_at_date = now(). Administrative path — Inventory Controller or System Administrator only (SR_AUTH_013). Permitted only before commit. No inventory or GL impact. Lines remain readable for audit. Terminal. Voiding a completed SR is not allowed; post-commit corrections use [inventory-adjustment](/en/inventory/inventory-adjustment). |
SR_POST_011 |
Soft delete | deleted_at = now(), deleted_by_id = user. Permitted only at draft (per SR_AUTH_013 spirit). Row remains in the database; the @@unique([sr_no, deleted_at]) index lets a new SR reuse the same sr_no. |
SR_POST_012 |
At-issue stock-out short fulfilment | When SR_VAL_013 would fail because live on-hand at issue time is less than approved_qty, the fulfiller may either (a) reduce issued_qty to min(approved_qty, on_hand), leave a per-line system comment, and commit the partial — leaving fulfilment_gap = approved_qty − issued_qty > 0 recorded against the closed SR; or (b) cancel the line (approved_qty → 0 is not allowed post-approval, so the line stays with its approved quantity but with issued_qty = 0 and a system comment "could not fulfil — source stock-out"). The SR still moves to completed with the partial fulfilment recorded. |
SR_POST_013 |
Receiver discrepancy flag | After commit, the destination's Receiver may flag a discrepancy ("received less than issued" or "wrong lot"); the flag writes a system comment on the SR but does NOT move the status. Resolution is via [inventory-adjustment](/en/inventory/inventory-adjustment) between the two locations; the SR itself stays completed. |
State diagram (Prisma-canonical):
[*] → draft → in_progress → completed
↓ ↓
cancelled / voided (completed is terminal save via inventory-adjustment path)
completed, cancelled, and voided are terminal. draft accepts soft-delete.
The Prisma enum enum_doc_status documented above is what the live UI uses. The BRD (BR-store-requisitions v1.5.0) and SR-User-Experience.md describe a 6-state lifecycle. The table below maps every observable live-UI status to its BRD equivalent so testers and developers can reconcile the two without ambiguity. Source: Test_case/System_Process/tx-03-sr.md (capture date 2026-04-27).
Diff legend: ✅ match · 🟡 renamed · 🔵 BRD only.
| Live UI status | BRD equivalent | Diff | Notes |
|---|---|---|---|
draft |
Draft |
✅ match | Initial editable state; requester entering line data. |
in_progress |
Submitted, UnderReview, Approved, PartiallyApproved, InProcess, Fulfilled |
🟡 renamed | BRD 6-state model collapses into a single Prisma status; actual sub-stage is tracked via workflow_current_stage. Stage flow per BR-SR-014: Draft → Submit → Approve → Issue → Complete. |
completed |
Completed / Fulfilled |
🟡 renamed | The single posting event (in_progress → completed). Source on-hand decremented; cost-layer consumed; inventory transactions written. |
cancelled |
Rejected / Reject |
🟡 renamed | Triggered by requester withdrawal, approver full-rejection, or automatic Σ approved_qty = 0 path. |
voided |
Voided / Void |
🟡 renamed | Administrative path; Inventory Controller or Sysadmin only; pre-commit only. |
| — | PartiallyApproved |
🔵 BRD only | Not a separate Prisma status; represented by mixed per-line approval outcomes while doc_status = in_progress. |
⚠️ Discrepancy — 3-variant nature not reflected in
enum_doc_status: The live UI supports three destination variants (INV → INV / INV → DIR / INV → CONS) that produce different GL effects, but all three share the samedraft → in_progress → completedstatus path. Thesr_typeenum (issue/transfer) distinguishes the two live variants (DIR and CONS are bothsr_type = issue; INV → INV issr_type = transfer). BRDBR-store-requisitions v1.5.0andTest_case/System_Process/tx-03-sr.mddescribe three variants; the live system collapses DIR and CONS under a singlesr_type = issuevalue, differentiating them only by the destinationtb_location.location_type. Source:Test_case/System_Process/tx-03-sr.md(capture date 2026-04-27).
⚠️ Discrepancy — Stock Transfer view vs separate TRF transaction code: BRD
BR-period-end v2.0.0listsTRFas a peer transaction code alongsideSRin Stage 1 of the period-close validation gate. The implementation (BR-stock-transfers.md) confirms that Stock Transfers are not a separate entity — they are SRs with INVENTORY destinations (sr_type = transfer). For period-close Stage 1 validation, SR records with INV → INV destinations satisfy both theSRandTRFbuckets. The Stock Transfer page is a read-only filtered view of SR records, not a separate document. Source:Test_case/System_Process/tx-03-sr.md(capture date 2026-04-27).
ℹ️ Note — Variant A auto-complete: For
sr_type = transfer(INV → INV), the Issue and Complete stages are treated as the same stage — auto-complete with no separate receiver confirmation step. For Variants B and C (sr_type = issue), the Fulfiller explicitly completes the document after issue.
Rule IDs follow SR_XMOD_NNN.
| Rule ID | Related module | Rule |
|---|---|---|
SR_XMOD_001 |
inventory | Inventory on-hand is decremented at the source only at SR commit (SR_POST_006) — not at submit, not at approve. The decrement is via insert into tb_inventory_transaction / tb_inventory_transaction_detail, reached from the SR side through tb_store_requisition_detail.inventory_transaction_id. For sr_type = transfer, the destination's on-hand is incremented in the same atomic transaction; for sr_type = issue, no destination on-hand increment occurs (the value lands as a consumption expense, not as held stock). |
SR_XMOD_002 |
inventory | Lot number, expiry date, and per-lot quantity for issued lines live on tb_inventory_transaction_detail (and tb_inventory_transaction_cost_layer.lot_no), not on the SR line. The SR line reaches lot data through the inventory_transaction_id link. For lot-controlled items, the fulfiller selects which lot(s) to release before commit; the selection is preserved on the transaction detail so recall / expiry / lot-cost traceability stays intact (PRD BR-Lot-01). |
SR_XMOD_003 |
inventory | The enum_inventory_doc_type value stamped on the linked inventory transaction is store_requisition; downstream inventory queries filtering by document type pick the SR-driven movements via this stamp. The same stamp is what [inventory-adjustment](/en/inventory/inventory-adjustment) reads when reversing a post-commit error. |
SR_XMOD_004 |
costing | Valuation at commit is per the source location's costing method (FIFO or moving-average per the tenant's per-location config) — not the destination's. SR_CALC_004 reads cost_per_unit from the source's cost-layer table at the moment of issue. FIFO consumption picks the oldest active layer; moving-average uses the location's current weighted-average cost. The costing module owns the calculation; the SR module is responsible only for triggering the consumption. |
SR_XMOD_005 |
costing | At the destination, the value-receipt accounting depends on sr_type: transfer lands in the destination's inventory account at the same cost_per_unit (cost moves intact between inventory accounts); issue lands as an expense on the destination's cost-centre at the same cost_per_unit (cost moves from inventory to expense). The cost-per-unit at the destination after a transfer enters the destination's cost layers per its own costing method — typically appended as a new FIFO layer or absorbed into moving-average. |
SR_XMOD_006 |
recipe | Recipe demand from a production / banquet event may auto-generate an SR for the ingredient pulls: the recipe module computes ingredient quantities at the destination outlet's BoM and posts an SR in draft for the outlet's requester to review and submit. The SR thereafter follows the normal flow (SR_POST_001 onward). The recipe-originated SR carries a back-reference in info.recipe_id for traceability. |
SR_XMOD_007 |
good-receive-note | Inter-warehouse transfers may pair an SR-OUT at the source warehouse with a GRN-IN at the destination warehouse — the SR records the issue from source perspective, the GRN records the receipt from destination perspective. Both reference the same tb_inventory_transaction family but from opposite sides; reconciliation between the two is a finance / inventory-controller responsibility. Single-step transfer SRs (sr_type = transfer) record both legs on one document and do not require a paired GRN; the paired-GRN pattern is reserved for cases where the destination needs to acknowledge receipt with its own document (typically when the destination is a different legal entity or a remote facility). |
SR_XMOD_008 |
Approval workflow | The approval routing — number of stages, the role / threshold at each stage, delegation rules — is owned by tb_workflow (referenced from the SR header via workflow_id with an explicit Prisma @relation). The SR module is responsible for consulting the workflow on each transition and populating user_action.execute from the current stage; it is not responsible for the routing rules themselves. Approval delegation (a user assigning their approval right to another) is handled at the workflow layer. |
SR_XMOD_009 |
inventory-adjustment | Post-commit corrections — wrong lot picked, miscounted issue, destination short-receipt — flow through inventory-adjustment, not through SR editing. The adjustment carries a back-reference to the originating tb_store_requisition.id for audit. The SR itself stays completed. |
SR_XMOD_010 |
Vendor performance / outlet variance | Variance feedback (SR_POST_008) feeds outlet-level food-cost variance and supply-discipline reporting. Chronic over-requesting (large requested_qty − approved_qty gaps), chronic at-issue shortfall (approved_qty − issued_qty), and seasonal request patterns are surfaced from joins across the three quantity columns. The metric set is owned by the reporting layer; the SR module is responsible only for persisting the columns that feed it. |
../carmen/docs/store-requisitions/SR-Technical-Specification.md — Functional requirements SR_CRT_001–SR_CRT_010 (creation), SR_APR_001–SR_APR_008 (approval), SR_FUL_001–SR_FUL_008 (fulfilment), SR_RPT_001–SR_RPT_005 (reporting). Validation rules feed SR_VAL_*; cost calculations feed SR_CALC_* (with the caveat that the SR is a quantity document — see Section 3 intro). Note: the Tech Spec's RequisitionItem.approvalStatus enum and Requisition.totalAmount field are divergent from Prisma; the rules above use the canonical schema.../carmen/docs/store-requisitions/SR-Overview.md — §Business Rules (Creation Rules, Approval Rules, Movement Rules, Financial Rules) mapped above onto SR_VAL_*, SR_AUTH_*, SR_POST_*, SR_XMOD_*. Note: the Overview's 5-state header (Draft / In Process / Complete / Reject / Void) collapses into the Prisma 5-state enum_doc_status (draft / in_progress / completed / cancelled / voided).../carmen/docs/store-requisitions/SR-User-Experience.md — User journeys (Create / Approve / Process), state diagram (6-state legacy — not canonical), persona descriptions. Approval branch logic and partial-approval / send-back patterns feed SR_AUTH_005–SR_AUTH_006 and SR_POST_003–SR_POST_004.../carmen/docs/store-requisitions/Store Requisitions.md — Use cases UC-64 (Approve), UC-65 (Deny), UC-66 (Modify), UC-67 (Monitor), UC-68 (Create and Manage), UC-69 (Approve and Record Stock as Issued) — together they define the approver and store-keeper interaction patterns used in the authorization and posting tables above.en/store-requisition/01-data-model.md — canonical Prisma model, enum values (in particular the shared 5-value enum_doc_status and the two-value enum_sr_type), and the divergence catalogue that Section 1, Section 3, and Section 6 rely on.../carmen-turborepo-backend-v2/apps/ — the store-requisition service module is the implementation hook for these rules (status guards, three-quantity invariant check, source-availability check, segregation-of-duties guard, inventory-transaction creation, journal-entry posting).