At a Glance
Persona: Receiver (Store Keeper + Inventory Manager) · Module: purchase-order · Scenarios: ~23
Categories: Happy Path · Permission · Validation · Edge Case
E2E coverage: no dedicated Receiver spec yet; shared coverage intests/401-po.spec.ts(TC-PO-340001..340003 GRN sync, TC-PO-060501..060504 Close) in../carmen-inventory-frontend-e2e/
This page captures the test scenarios that the Receiver persona (the Store Keeper at the dock plus the Inventory Manager who oversees closure for the location) directly drives in the purchase-order module. The Receiver's involvement begins when the PO reaches po_status = sent and ends when the PO leaves the receipt zone — either through completed (full receipt), partial (open balance carried forward), or closed (Inventory Manager early-terminates a partial PO). The GRN posting itself lives in the downstream [good-receive-note](/en/inventory/good-receive-note) module; this page captures only the PO-side effects — how a GRN flips tb_purchase_order.po_status via PO_POST_006 (sent → partial) and PO_POST_007 (* → completed), how the Inventory Manager closes a partial PO with the remainder written to cancelled_qty via PO_POST_011, and how the PO line counters (received_qty, cancelled_qty) advance against order_qty (03-user-flow-receiver.md Section 2). Scenarios are grouped into the happy paths described in the user flow, the RBAC boundary enforced by PO_AUTH_008 (Receiver / Inventory Manager scope) and PO_AUTH_010 (segregation of duties — GRN poster ≠ PO buyer / transmitter) versus PO_AUTH_001–PO_AUTH_007 (Purchaser / Manager scope), the validation rules around received_qty, accepted_qty, over-receipt tolerance, and voided / closed-PO state guards, and a small set of edge cases around tolerance boundaries, decimal precision, concurrency, and multi-currency context. Cross-persona handoffs that pivot off the Receiver (X-PO-03, X-PO-04, X-PO-06, X-PO-08) live in the parent overview, not here.
| # | Scenario | Pre-condition | Steps | Expected |
|---|---|---|---|---|
| RCV-HP-01 | GRN full receipt against a single-line sent PO drives sent → completed |
PO PO-X at po_status = sent with one line: order_qty = 10, received_qty = 0, cancelled_qty = 0; vendor delivery matches PO line exactly; Receiver receiver@blueledgers.com logged in with enum_stage_role = inventory; Receiver is NOT the PO buyer / transmitter (PO_AUTH_010 satisfied) |
1. Open PO-X detail → click Receive (deep-link into [good-receive-note](/en/inventory/good-receive-note)). 2. GRN header inherits vendor_id, currency_id, delivery location from the PO; GRN detail pre-populates with pending_qty = 10. 3. Enter received_qty = 10, accepted_qty = 10 on the line. 4. Review variance summary (no short, no over, no quality reject). 5. Click Post. |
GRN detail row written; tb_purchase_order_detail.received_qty rises 0 → 10; bridge tb_purchase_order_detail_tb_purchase_request_detail.received_qty updated proportionally; inventory on-hand incremented by accepted_qty = 10 (handled by [good-receive-note](/en/inventory/good-receive-note) / [inventory](/en/inventory/inventory)). Every active line now satisfies received_qty + cancelled_qty ≥ order_qty → po_status = sent → completed per PO_POST_007; history appended { po_status: 'completed', action: 'received' }; PO becomes read-only for receipt; matched-but-unbilled position handed off to Finance for three-way match per PO_POST_008. |
| RCV-HP-02 | GRN partial receipt against a single-line sent PO drives sent → partial |
PO PO-Y at po_status = sent with one line: order_qty = 10, received_qty = 0, cancelled_qty = 0; vendor delivers only 6 of 10 today |
1. Open PO-Y → Receive. 2. Enter received_qty = 6, accepted_qty = 6. 3. Review variance: short by 4, no quality reject. 4. Post. |
tb_purchase_order_detail.received_qty rises 0 → 6; pending balance order_qty − received_qty − cancelled_qty = 10 − 6 − 0 = 4 carried forward. Since at least one line still has received_qty < order_qty − cancelled_qty, po_status = sent → partial per PO_POST_006; PO stays open for further GRN posts; history appended { po_status: 'partial', action: 'received' }; activity log flags short delivery for Purchaser follow-up (vendor chase). |
| RCV-HP-03 | Partial then remainder — multi-step GRN cycle reaches completed via partial |
PO PO-Z at po_status = partial from RCV-HP-02 (order_qty = 10, received_qty = 6, cancelled_qty = 0, pending balance 4); vendor delivers the remaining 4 in a follow-up shipment |
1. Open PO-Z (filter Partial) → Receive. 2. GRN detail pre-populates with pending_qty = 4. 3. Enter received_qty = 4, accepted_qty = 4. 4. Post. |
tb_purchase_order_detail.received_qty rises 6 → 10; pending balance = 0. Every line now satisfies received_qty + cancelled_qty ≥ order_qty → po_status = partial → completed per PO_POST_007; full receipt history is sent → partial → completed; two GRN rows persist against the same PO line (idempotent multi-receipt model — TC-PO-340003); no further GRNs accepted. |
| RCV-HP-04 | Quality issue logged — accepted_qty < received_qty |
PO PO-Q at po_status = sent, line L1: order_qty = 10, received_qty = 0; vendor delivers 10 but 2 fail inspection (damaged cartons) |
1. Open PO-Q → Receive. 2. Enter received_qty = 10, accepted_qty = 8. 3. Variance summary shows quality-reject = 2. 4. Post. |
GRN posts both values; tb_purchase_order_detail.received_qty rises 0 → 10 (line counted as physically received); inventory on-hand rises by accepted_qty = 8 only (handled in [inventory](/en/inventory/inventory)); the variance received_qty − accepted_qty = 2 is the return / credit-note quantity tracked on the GRN, not on the PO. Since received_qty + cancelled_qty = 10 ≥ order_qty, po_status = sent → completed per PO_POST_007. The PO does not auto-correct for the quality variance — resolution path (amendment, return, credit note) is initiated by the Purchaser, not the Receiver. |
| RCV-HP-05 | Inventory Manager closes a partial PO with remainder written to cancelled_qty |
PO PO-C at po_status = partial, line L1: order_qty = 10, received_qty = 6, cancelled_qty = 0; vendor confirms they cannot supply the remaining 4 (out of stock / discontinued); Inventory Manager logged in with role on the PO's delivery location |
1. Open PO-C → click Close on the PO header (TC-PO-060503, TC-PO-060504). 2. Enter required reason text in tb_purchase_order_comment (e.g. "vendor cannot supply remainder — discontinued SKU"). 3. Confirm in the close dialog. |
For each line still pending, the application writes the remainder to cancelled_qty so that received_qty + cancelled_qty = order_qty: L1.cancelled_qty = 4, received_qty = 6, order_qty = 10. po_status = partial → closed per PO_POST_011; closed is terminal — no further GRNs accepted, no return to partial. Reason text persisted in tb_purchase_order_comment; history appended { po_status: 'closed', action: 'closed' }; close-out handed off to Finance for any already-posted GRN reconciliation. |
| RCV-HP-06 | GRN against a multi-line PO with mixed full / partial line outcomes | PO PO-M at po_status = sent with three lines: L1 (order_qty = 10, pending 10), L2 (order_qty = 5, pending 5), L3 (order_qty = 8, pending 8); vendor delivers L1 = 10 (full), L2 = 3 (short by 2), L3 = 8 (full) |
1. Open PO-M → Receive. 2. Per-line entry: L1 received_qty = 10, accepted_qty = 10; L2 received_qty = 3, accepted_qty = 3; L3 received_qty = 8, accepted_qty = 8. 3. Variance summary: L2 short by 2. 4. Post. |
L1.received_qty = 0 → 10, L2.received_qty = 0 → 3, L3.received_qty = 0 → 8. Line-wise evaluation: L1 and L3 are full but L2 still has received_qty (3) < order_qty − cancelled_qty (5). Header rule: since at least one line is open, po_status = sent → partial per PO_POST_006 — NOT completed. PO stays open for a follow-up GRN against L2 only (Receiver may also escalate to Inventory Manager to close on the L2 remainder if vendor cannot supply). |
| # | Scenario | Expected behaviour (allow/deny + reason) |
|---|---|---|
| RCV-PERM-01 | Receiver posts a GRN against a PO at po_status = sent |
Allow per PO_AUTH_008 (po_status ∈ {sent, partial}). The [good-receive-note](/en/inventory/good-receive-note) Create flow accepts the PO selection; PO-side counters update; po_status transitions per PO_POST_006 / PO_POST_007 based on line-wise evaluation. |
| RCV-PERM-02 | Receiver attempts a GRN against a PO at po_status = draft |
Deny per PO_AUTH_008 (status not in {sent, partial}). The PO does not appear in the GRN module's vendor / PO picker; a direct API call returns "GRN cannot be posted against PO at status draft — PO must be sent or partial."; counters are unchanged. |
| RCV-PERM-03 | Receiver attempts to modify PO header (vendor, currency, delivery date) | Deny — Purchaser-only. Per PO_AUTH_010 (segregation of duties) and PO_AUTH_002 (Purchaser scope), header / line edits are reserved to the assigned buyer; the Receiver may neither edit the header nor the line price / qty / discount. The PO detail page in the Receiver's view is read-only for header / lines — only the Receive and (for Inventory Manager) Close buttons are interactive. A direct API edit call returns "You are not authorised to edit this PO at the current stage". |
| RCV-PERM-04 | GRN poster = PO buyer / transmitter (segregation of duties) | Deny — segregation of duties. Per PO_AUTH_010, the user identified by tb_purchase_order.buyer_id or as last_action_by_id on the sent transition MUST NOT be the same user who posts the GRN. The GRN module rejects the post at create-time with "You created or transmitted this PO; another user must post the GRN against it."; no GRN row is written; PO counters unchanged. |
| RCV-PERM-05 | Receiver (Store Keeper) attempts to close a partial PO (early termination) |
Deny when role = Store Keeper only; allow when role = Inventory Manager. Per PO_AUTH_008 and PO_POST_011, the Close (partial → closed) action requires the Inventory Manager role (or Procurement Manager override via PO_AUTH_007). The Store Keeper's UI hides / disables the Close button on the PO header; a direct API call returns "Close from status partial requires the Inventory Manager role." |
| RCV-PERM-06 | Receiver attempts a GRN against a PO at po_status = completed or closed |
Deny — terminal status. Per PO_POST_007 / PO_POST_011, completed and closed are terminal for receipt purposes; no further GRNs are accepted. The PO does not appear in the GRN module's open-PO list; a direct API call returns "PO is at terminal receipt status <status> — no further receipts can be posted." |
| RCV-PERM-07 | Inventory Manager closes a partial PO they did NOT raise |
Allow per PO_AUTH_008. The Inventory Manager's authority is tied to the delivery location, not to PO authorship; segregation of duties (PO_AUTH_010) applies to GRN posting, not to PO closure. The close succeeds with reason text appended to tb_purchase_order_comment. |
| # | Scenario | Trigger | Expected error |
|---|---|---|---|
| RCV-VAL-01 | Over-receipt without tolerance — received_qty > pending balance |
PO line L1: order_qty = 10, received_qty = 0, cancelled_qty = 0; tenant over-receipt tolerance disabled or set to 0%; Receiver enters received_qty = 12 |
Reject — PO_XMOD_004. Server returns "Received quantity exceeds the pending balance and tenant over-receipt tolerance is not configured."; the GRN line is capped at the pending qty (10) or the post is blocked entirely; PO counters unchanged. |
| RCV-VAL-02 | Zero received_qty on a GRN line |
Receiver enters received_qty = 0 on a line and clicks Post |
Reject — PO_XMOD_003 / GRN-side validation. Server returns "Received quantity must be greater than zero on every GRN line; remove the line if nothing was received against it."; the GRN cannot post with a zero-qty line; PO counters unchanged. |
| RCV-VAL-03 | GRN against a voided PO |
PO PO-V at po_status = voided (voided by Procurement Manager per PO_AUTH_007); Receiver attempts to open the GRN flow from the GRN module directly |
Reject — PO_AUTH_007 (voided is terminal). Server returns "PO is at status voided — no GRN can be posted."; the PO does not appear in the open-PO list; a direct API call is blocked at the GRN create step. |
| RCV-VAL-04 | accepted_qty > received_qty (quality exceeds physical receipt) |
Receiver enters received_qty = 8, accepted_qty = 10 on a line and clicks Post |
Reject — GRN-side validation. Server returns "Accepted quantity cannot exceed received quantity on any GRN line."; the inequality accepted_qty ≤ received_qty is enforced at save and post; the GRN cannot post; PO counters unchanged. |
| RCV-VAL-05 | Partial GRN when remaining qty is unreceivable (negative pending) | PO line L1: order_qty = 10, received_qty = 8, cancelled_qty = 2 (so pending balance = 0); Receiver attempts to post a GRN with received_qty = 1 on L1 |
Reject — PO_XMOD_003. Server returns "PO line has no remaining open balance — received_qty (8) + cancelled_qty (2) = order_qty (10)."; the GRN line cannot post on L1; if other lines on the PO are still open, those may post independently. |
| RCV-VAL-06 | GRN against a PO whose vendor was flipped to is_active = false after sent |
PO PO-W at po_status = sent; tb_vendor.is_active = false on the PO's vendor (vendor blacklisted post-transmission) |
Reject — vendor-state guard. Server returns "Vendor is no longer active and cannot deliver further goods. Escalate to the Procurement Manager."; the GRN create step is blocked; the resolution path is Procurement Manager void (PO_AUTH_007) or vendor master fix-up. |
| # | Scenario | Condition | Expected |
|---|---|---|---|
| RCV-EDGE-01 | Exact tolerance boundary on over-receipt | PO line L1: order_qty = 10, received_qty = 0, cancelled_qty = 0; tenant over-receipt tolerance = 5%; Receiver enters received_qty = 10.5 (exactly at the upper bound) |
Accepted — PO_XMOD_004. received_qty = 10.5 is within tolerance (10 × 1.05 = 10.5); GRN posts; tb_purchase_order_detail.received_qty = 10.5; line evaluation: received_qty (10.5) ≥ order_qty − cancelled_qty (10) → fully received; po_status = sent → completed per PO_POST_007. Setting received_qty = 10.51 would breach the bound and be rejected per RCV-VAL-01. |
| RCV-EDGE-02 | Decimal precision on accepted_qty |
Receiver enters received_qty = 5.123, accepted_qty = 4.998 on a line whose order UoM allows 3-dp precision; conversion factor 1.234567, base_qty computed in storage UoM |
Accepted at storage; received_qty and accepted_qty stored at line's UoM precision (3 dp); base_qty recomputed on the line as Round(received_qty × conv_factor, 3) for inventory roll-up; quality variance received_qty − accepted_qty = 0.125 tracked on the GRN; PO received_qty aggregates without precision loss; all stored Decimals fit within Decimal(20, 5) for money and Decimal(15, 5) for rates. |
| RCV-EDGE-03 | Concurrent GRN posts on the same PO from two Receiver sessions | Receiver A and Receiver B both open PO-X (po_status = sent, received_qty = 0, order_qty = 10); A posts received_qty = 6 at T; B posts received_qty = 4 at T + 500ms against the same line, both with doc_version = 3 snapshots |
First post wins atomically: A's GRN posts, tb_purchase_order_detail.received_qty = 0 → 6, po_status = sent → partial, doc_version → 4. B's post is reconciled against the new state — either the GRN module re-reads pending_qty = 4 and posts B's received_qty = 4 cleanly (advancing received_qty → 10, po_status → completed), or the optimistic-concurrency guard rejects B with "This PO was modified by another user. Please refresh and re-apply your changes." and B retries. No double-write, no lost-update, no received_qty > order_qty overflow outside the over-receipt tolerance. |
| RCV-EDGE-04 | Multi-currency context — PO base currency matters for valuation | PO PO-F with currency_id = USD, exchange_rate = 36.50 (against base THB); line L1: order_qty = 10, price = $50.00; Receiver posts GRN received_qty = 10, accepted_qty = 10 |
GRN posts at the PO's transaction currency (USD); inventory valuation is dual-posted — transaction value $500.00 and base value Round($500.00 × 36.50, 2) = ฿18,250.00 per PO_CALC_008–PO_CALC_011; the PO's exchange_rate snapshot at the time of PO submission is the rate used for the GRN-driven inventory accrual, not the current spot rate; po_status = sent → completed per PO_POST_007; downstream three-way match (Finance) reconciles transaction-currency invoice against transaction-currency PO / GRN. |
X-PO-03 (partial receipt cycle), X-PO-04 (full receipt to completed), X-PO-06 (Inventory Manager close-out), X-PO-08 (quality variance → return / credit note feeding Finance).sent → partial → completed line-wise evaluation, the decision branches (short / over / quality / wrong-item / partial-then-remainder / close-with-remainder-cancelled), and the Finance handoff after GRN post.PO_AUTH_007 Manager-only void; PO_AUTH_008 Receiver / Inventory Manager scope for GRN and close; PO_AUTH_010 segregation of duties — GRN poster ≠ PO buyer / transmitter), Section 5 (PO_POST_006 GRN partial receipt → sent → partial / partial → partial; PO_POST_007 GRN full receipt → * → completed; PO_POST_008 three-way match — PO not status-changed by the match; PO_POST_011 close partial → closed with remainder written to cancelled_qty), Section 7 (PO_XMOD_003 GRN may only be created against sent / partial PO; PO_XMOD_004 over-receipt gated by tenant tolerance; PO_XMOD_008 on-order pipeline qty = order_qty − received_qty − cancelled_qty).received_qty, accepted_qty, quality-variance tracking, and over-receipt tolerance enforcement; the PO module owns the resulting po_status transitions and the line-counter roll-ups.accepted_qty is owned by the inventory module on GRN post; the PO contributes only the on-order pipeline quantity (order_qty − received_qty − cancelled_qty) per PO_XMOD_008.../carmen-inventory-frontend-e2e/tests/401-po.spec.ts — shared / mixed-persona coverage relevant to the Receiver: TC-PO-340001..TC-PO-340003 (GRN sync backend behaviour — sent → completed, GRN without PO line items, multiple GRNs per PO line); TC-PO-200001..TC-PO-200004 (QR code for mobile receiving); TC-PO-060501..TC-PO-060504 (Close action on approved / partial PO). No dedicated Receiver E2E spec exists at this time — the receipt cycle is covered backend-side and via the shared 401 spec; a dedicated 402-po-receiver-journey.spec.ts is a roadmap item.