Goods receipt and QC

The moment stock becomes real · count against expectation, then dispose · SHORT, EXCESS, WRONG-SKU and DAMAGED-ON-ARRIVAL are outcomes with their own lifecycles, not exceptions · a GRN posts stock only against what was physically counted

214 units have been in RECEIVED_NOT_INVOICED longer than the 72h QC SLA · oldest is GRN-2026-0402 at 118h · these units are physically at HQ_319, are not sellable, and cannot be invoiced until QC names a destination for every one of them Open QC queue →
Blind count active on GRN-2026-0418 · expected quantities are absent from the response payload, not hidden by CSS · counter subesh@minimalist.ae · recount by the same person needs a written override R-06.5

RNI units and RNI accrual are two different numbers and are shown side by side on purpose. The first is a warehouse queue in units, the second is a finance liability in AED. Every incumbent system calls both of them "RNI", which is why they never agree.

All 24
Counting 3
Counted 2
QC pending 2
Posted 17
Match 15
Short 5
Excess 2
Wrong SKU 1
Damaged on arrival 1
Unsolicited 1
HQ_319
HQ_325
MCC
Blind
Recount

Receipt outcome mix

14 days · units by outcome · a healthy week is not a week with no shorts, it is a week where every short became a claim

1,842 units today
2000 1500 1000 0 16 Jul · match · 520 units · 2 receipts 16 Jul · short · 78 units · claim CLM-2026-0161 17 Jul · match · 742 units · 3 receipts 17 Jul · excess · 42 units posted to QUARANTINE 18 Jul · match · 402 units · 1 receipt 19 Jul · match · 980 units · 4 receipts 19 Jul · short · 118 units · CIC container 2 of 3 19 Jul · damaged on arrival · 48 units · carrier attribution under FOB 20 Jul · match · 306 units 21 Jul · match · 660 units 21 Jul · short · 62 units 22 Jul · match · 838 units 23 Jul · match · 442 units 23 Jul · wrong SKU · 60 units MST-CRM-42-BLK received against MST-CRM-42-TAN 24 Jul · match · 1,042 units 25 Jul · match · 578 units 25 Jul · short · 96 units · VJ Jewels 26 Jul · match · 240 units · weekend 27 Jul · match · 780 units 27 Jul · excess · 58 units to QUARANTINE · over_ship claim open 28 Jul · match · 918 units 29 Jul · match · 1,684 units · today 29 Jul · short · 158 units · GRN-2026-0416 CIC
1617181920212223242526272829 Jul
Match Short Excess to quarantine Wrong SKU Damaged on arrival

RECEIVED_NOT_INVOICED aging

Units awaiting a QC disposition · the 72h SLA is drawn, not implied

214 past SLA
72h SLA 0 to 24h 0 to 24h · 486 units · AED 168K · 3 receipts · oldest GRN-2026-0418 at 6h 486 u · AED 168K 24 to 48h 24 to 48h · 288 units · AED 96K · 2 receipts 288 u · AED 96K 48 to 72h 48 to 72h · 118 units · AED 41K · GRN-2026-0413 Bursa Textiles 118 u · AED 41K 72h to 7d 72h to 7d · 166 units · AED 58K · 2 receipts past SLA 166 u · AED 58K over 7d over 7d · 48 units · AED 49K · GRN-2026-0402 at 118h · escalated to the inbound lead 48 u · AED 49K · escalated

This chart is the unit backlog: goods standing at HQ_319 that QC has not dispositioned. It is not the finance accrual. The AED alongside is the value of those units at landed cost, not the unmatched-invoice liability, which ages separately on the 3-way match register.

Receiving desk · GRN-2026-0418

CIC · PO-2026-0117 · PI-4412 · HQ_319 · count 1 of 1 · blind · counted by subesh@minimalist.ae · 14 cartons scanned

blindcounting
SKUItem typeExpectedCountedMethodEvidenceCartonOutcome
MST-CRM-42-BLK
Cremona 42 strap, black
stock blind 240 scan 2 photos CTN-9041..46 pending
MST-CRM-42-TAN
Cremona 42 strap, tan
stock blind 186 scan 1 photo CTN-9047..50 pending
MST-CRM-42-BLK
Cremona 42 strap, black
display blind 8 manual 0 CTN-9051 pending
MIN-2159
Signature card holder
stock blind 420 ocr 0.94 count sheet CTN-9052..61 pending
MIN-2160
Signature card holder, navy
stock blind 0 manual 1 photo not present zero counted
LBA15
Leather bracelet, 15mm
trial blind 16 manual 0 CTN-9062 pending
A zero is an assertion, not an absence. MIN-2160 reads 0 because a person looked and found none, and it posts a short-ship claim rather than vanishing. INV1 skipped the ledger entirely when a line received nothing, and coerced a malformed input to 0 in the same function, so a total short and a form-parse failure were indistinguishable while the PO still closed.

Discrepancy escalation

1Full detail. Every line with a variance, with expected, counted and the delta. Acknowledged 14:22.
2SKU only. The list again, without the numbers, to force a second look. Acknowledged 14:26.
3Submit anyway or recount. A manager override is captured with a name and a reason, never a silent bypass.

Blind-count boundary

GET /api/grn/418/receive?blind=1 returns no qty_expected, no variance and no derived field. There is nothing on the client to leak. The GRN app fixed four separate leaks in one release, a badge denominator, badge colours, a mismatch prompt and a blocker modal, because every one of them rendered a number the client should never have held.

Recount policy

The same person may not recount without a written override, and a handoff QR is offered instead. The override is recorded, not prevented: at 22:40 with one person on shift, blocking it only means the count does not happen.

Goods received notes

Expected vs counted vs encoded · 24 receipts · expected is a frozen snapshot of the PO balance at receipt time, so a later PO edit cannot rewrite a variance

R-06.4
GRNKindPO / PISupplierLoc ExpectedCountedEncodedVar OutcomeQCRNI ageClaim
GRN-2026-0418supplier PO-2026-0117CICHQ_319 87087000 countingnot started6h-
GRN-2026-0417supplier PO-2026-0115ORO LeatherHQ_319 4804770-3 shortpending18hCLM-0184
GRN-2026-0416supplier PO-2026-0112CICHQ_319 1,8001,6420-158 shortpending22hCLM-0183
GRN-2026-0415supplier PO-2026-0116LC StrapsHQ_319 3,2003,2003,2000 matchpass 3,2000-
GRN-2026-0414supplier PO-2026-0111VJ JewelsHQ_319 9601,018960+58 excess960 pass · 58 hold0CLM-0181
GRN-2026-0413supplier PO-2026-0109Bursa TextilesHQ_319 6006005400 wrong SKU540 pass · 60 hold48hCLM-0179
GRN-2026-0411supplier PO-2026-0108ORO LeatherHQ_319 144144960 damaged on arrival96 pass · 48 damaged62hCLM-0177
GRN-2026-0409unsolicited -Sharq MetalworksHQ_319 0720+72 unsolicited72 hold4dpurchasing
GRN-2026-0408transfer RT-MCC-0056internalMCC 9393930 matchpass 930-
GRN-2026-0406rts return RTS-2026-0171LC StrapsHQ_319 2424240 matchre-QC 22 pass · 2 reject0-
GRN-2026-0405sample PI-4408Ceramica MEAHQ_319 121200 matchSAMPLE_QC · 41h of 72h0-
GRN-2026-0404supplier PO-2026-0112CICHQ_319 1,8001,8001,8000 match1,792 pass · 8 minor0-
GRN-2026-0402supplier PO-2026-0104Yiwu HomeHQ_325 484800 match118h unstarted118h-
GRN-2026-0398supplier PO-2026-0102LC StrapsHQ_319 2,4002,4002,4000 matchpass 2,4000-
GRN-2026-0394supplier PO-2026-0099VJ JewelsHQ_319 720694680-26 short680 pass · 14 reject0CLM-0168 settled
GRN-2026-0391supplier PO-2026-0096Ceramica MEAHQ_319 30030000 match288 pass · 12 damaged0CLM-0161 settled
24 receipts · 17 posted · 7 open Encoded reads 0 only where the SKU is serialised and tags are outstanding; a non-serialised SKU reads -, never a zero Sorted by receipt date, newest first

QC disposition · the 5 buckets

Disposition answers where the units go. Remedy answers who fixes it and who pays. Two columns, not one enum, which is why nothing can be invisible.

1,106 units awaiting disposition
DispositionMovesUnits nowTypical remedyClaimRTSWhere it shows up next
pass RECEIVED_NOT_INVOICED → STOCK 742nonenono Sellable · encode queue
minor RECEIVED_NOT_INVOICED → DISPLAY_GOOD 86supplier_credit as a price deduction, or local_fixyes when supplier-attributedno Floor sellable · scorecard
damaged RECEIVED_NOT_INVOICED → DISPLAY_DAMAGED 108local_fix or scrapyes, counterparty from the incotermno Damages · Incidents
hold RECEIVED_NOT_INVOICED → QUARANTINE 130none, pending a decisionnono Held stock · purchasing queue
reject RECEIVED_NOT_INVOICED → RTS_PENDING 40supplier_credit or supplier_repairyesyes Return to supplier
Why not ops2's five. ops2 buckets by remedy: accepted, reject_local, reject_supplier, repair_local, repair_supplier. That answers who fixes it, which the ledger cannot use, so the two *_local buckets had nowhere to land and ops2 had to write a whole module (qc_local_outcomes.py) to make them visible again, then a second one to project five down to three because the floor could not use five. Here ops2's five map onto the pair losslessly: repair_local is damaged plus local_fix, rejected_supplier is reject plus supplier_credit. Every combination has a destination stock type, so it appears in every balance by construction.

RFID encode queue

After QC accept, never before

SKUProgressEncAcc
MST-CRM-42-BLK0240
MST-CRM-42-TAN0186
MIN-2159260420
LBA151616
MSR31-0796109
LDF014848

qty_encoded <= qty_counted is a check constraint, not a convention. A pre-printed tag with no unit behind it is a phantom serial, so a short receipt encodes only what arrived (S05), an excess encodes into quarantine (S06) and a wrong-SKU receipt encodes nothing at all (S07), because minting a serial for a unit we are about to send back orphans it the moment it leaves.

Four of the eight RFID inbound scenarios, S02, S05, S06 and S07, do not exist in rfid-app today, and S01 encodes with no PO or inflow link at all. That missing link is exactly why defect rate cannot currently be traced back to a supplier batch.

Gate feed3-way

The receipt authority rule, which this module owns for the whole estate

  1. GRN and IWMS own physical receipt. A ledger row with source_type in (grn_receipt, qc_disposition) must carry a grn_line_id whose receipt is posted. Any other system emitting a positive delta at a physical location is refused, recorded in grn_event_blocks, and opened as drift. Nothing else creates inbound stock.
  2. Only the counted quantity posts. Expected never posts. "Accept full" is a count method that writes counted equal to expected explicitly and names the person who pressed it, not an exemption from counting.
  3. Every receipt lands in RECEIVED_NOT_INVOICED first. No path writes STOCK directly from a receipt. The only way out of RNI is a QC disposition.
  4. Expected is frozen at receipt time. A later PO amendment changes future receipts only. A variance is a fact about a moment, not a running comparison.
  5. No discrepancy resolves by editing a quantity. There is no code path that updates a counted quantity after posting. A correction is a void plus a new receipt, both retained.

How a discrepancy reconciles against the PO line, and becomes a claim

  1. Short. Post the counted quantity only. Never a negative, a phantom or a reversal. Open a short_ship claim for the gap at the PO line's unit cost. The PO line stays open for the shortfall, so container 2 of 3 can close it.
  2. Excess. Post everything counted, but the portion above expected goes to QUARANTINE, not STOCK, with an over_ship claim. Excess never silently becomes free sellable stock.
  3. Wrong SKU. Post zero against the ordered SKU, post the SKU actually received to QUARANTINE at zero cost, open a wrong_sku claim, and leave the ordered line short. i3 never substitutes one SKU for another however similar the codes are.
  4. Damaged on arrival. Counted, because it is physically here. QC names the destination and the counterparty comes from the incoterm on the terms row effective on the PO date: EXW and FOB mean the carrier, DAP and DDP mean the supplier. Resolved by contract, not by judgement.
  5. Unsolicited. No PO at all: QUARANTINE at zero cost, queued for purchasing. Received because it is here, not sellable because nobody ordered it.
  6. The identity. qty_ordered = qty_received + qty_outstanding + qty_cancelled + qty_claimed_short, asserted nightly, zero rows may violate. Module 05 derives po_lines.qty_received from posted GRN lines, so the two numbers cannot disagree.
  7. The idempotency key is the receipt event (grn:<grn_number>:<line_id>), never the PO line. INV1 keyed on the PO line, so a second delivery computed the same key and was silently suppressed, and its state machine had no partially_received. A PO shipping in two containers could not be recorded at all.

States

Loading
Counting the receipts.

KPI tiles and both charts hold their exact heights so nothing reflows when the data lands.

Empty
Nothing is being received.

No open receipt at any location. The last one posted 4h ago. Scan a carton or an AWB to start.

Expected arrivals
Error
The count did not post.

The transaction rolled back and nothing was written. Your counted quantities are still here. ops2 once returned success on exactly this failure, so i3 derives this message from the committed row.

grn-post-9c14 · 29-07-2026 14:32 GST

Permission
You can see receipts, not count them.

Counting needs role >= manager plus the grn_count feature at a location your rota puts you at today.

apps.i3.role = viewer · rota locations: MCC

Zero-filter
No matches.

3 filters hide all 24 receipts.

outcome: wrong SKUloc: BASblind
Maintenance
Receiving is read-only for 25 minutes.

The nightly receipt-identity verification is running. Counts in progress are held locally and submit when it finishes.

21:00 to 21:25 GST · job verify-module-06

Gate-blocked
GATE 3 has held for 4 of 7 days.

Receipts post and reconcile, but inbound is still in shadow: on-hand value uses provisional costs until the gate closes. The numbers are correct and are not yet authoritative.

100% posted within 5 min · PO reconcile green · costs partial

Blocked, which is not unmatched
2 receipts classified and were still refused.

A QC outcome arrived after its PO closed. It is blocked, not dropped, and it sits on the event stream with the rule that refused it. A pipeline with zero unmatched and non-zero blocked is not healthy.

Blocked events
Keyboard J K move Enter open receipt S scan + - adjust count P photo O OCR count sheet Q open QC R recount / filter ⌘K palette All shortcuts ↗
Connected to GRN events POs and PI In transit Production Suppliers Supplier detail Return to supplier Cartons Damages Incidents RFID 3-way reconcile Drift SKU detail Locations Live inventory Stock health Samples Transfers Reports Audit log