The Goods Received Note (GRN) is the legal record of raw-material entering the PalmStat factory. Whenever a supplier truck arrives at the receiving dock with paper, ink, board, thread, glue, mech, plastic film or any other RM line, the storeman generates a GRN to confirm exactly what was unloaded, accepted and rejected. The GRN closes the supplier's delivery note, opens the supplier-invoice three-way match (PO ↔ GRN ↔ invoice), and posts the on-hand quantity into the warehouse via an inventory_transactions row of type RECEIPT.
The PalmStat GRN matches the Sage X3 "Goods Receipt", Syspro "Receipt" / "Inspection & Receipt", SAP S/4HANA MIGO 101 goods-movement, and NetSuite "Item Receipt". It is the upstream document that every later production-paper trail (Material Requisition, Job Card, FGRN) ultimately ties back to via lot number and supplier reference.
Without a captured GRN, no RM is visible in the v_inventory_summary view, so no batch can validate against stock. This makes the GRN a blocking step for the entire production lifecycle.
QUARANTINE warehouse for 48-hour QC hold.BLOCKED inventory status until consumed.The GrnDoc spec (frontend/js/erp-templates/grn-doc.js) is built from the following endpoints + DB fields. Each row below is wired into a real column — not boilerplate.
| Field on document | API endpoint | DB table.column | Example value |
|---|---|---|---|
| GRN Ref No. | gen.previewRef('GRN') | documents.grn_no / generated | GRN-2026-0001 |
| GRN date | req.body.date | inventory_transactions.txn_date | 2026-05-08 |
| Supplier name | POST /api/inventory/transactions | inventory_transactions.notes → parsed | Mondi Paper SA |
| Delivery note no. | req.body.reference_no | inventory_transactions.reference_no | DN-MOND-44218 |
| P.O. ref | req.body.reference_no | documents.po_no | PO-2026-0142 |
| Item code | GET /api/items?id=:id | items.code | MONDI 1200 |
| Item description | GET /api/items | items.description | Mondi 1200mm reel 80gsm offset |
| UoM | master data | items.uom | KG / REEL / EA |
| Qty received | req.body.qty | inventory_transactions.qty | 2 540.00 |
| Qty accepted | derived (recv − rej) | computed in GrnDoc.build() | 2 510.00 |
| Qty rejected | req.body.qty_rejected | captured on row notes | 30.00 |
| Lot number | req.body.lot_number | inventory_transactions.lot_number | LOT-MND-260508 |
| Unit cost | req.body.unit_cost | inventory_transactions.unit_cost | R 18.4500 |
| Warehouse | GET /api/warehouses | warehouses.code via to_warehouse_id | PSTAT |
| Bin location | GET /api/inventory/lots | warehouse_locations.bin_code | RM-A12-03 |
| Captured by | req.user | users.email (FK user_id) | warehouse@palmstat.co.za |
| Reason code | req.body.reason_code | inventory_transactions.reason_code | DELIVERY |
| Document attachment (delivery note PDF) | POST /api/documents | documents.s3_key · document_links | s3://…/grn-2026-0001.pdf |
documents.po_no)./rm-intake. The view lists open POs with predicted line totals.POST /api/inventory/transactions with txn_type=RECEIPT. Validation enforces to_warehouse_id mandatory and qty > 0.QUARANTINE warehouse and the line is recorded with status=BLOCKED in the inventory snapshot.DAMAGE routed to QC quarantine.GrnDoc.openFromInventoryTxns(txns, opts), which builds a spec via GrnDoc.build() and dispatches to ErpDoc.print(). The browser opens a print preview using the same gradient-header template used everywhere in PalmStat.POST /api/documents with kind=GRN · document_links tie it to the underlying inventory_transactions rows.CONFIRMED and stock is visible in v_inventory_summary. A realtime broadcast on the inventory channel notifies any open BOM-validation pages.document_links.entity_type = 'inventory_transaction' for the 7-year SARS / VAT retention window. documents.retention_until is auto-set 2033-05-08 for a 2026 GRN.| Action | Admin | Warehouse | Planner | QC / Supervisor | Finance |
|---|---|---|---|---|---|
| Generate this doc | ✓ | ✓ | ✓ | ✓ | – |
| Approve / sign off | ✓ | ✓ (Storeman) | – | ✓ (QC line) | – |
| Edit before posting | ✓ | ✓ | – | – | – |
| Reprint after archive | ✓ | ✓ | ✓ | ✓ | ✓ |
| Email externally | ✓ | ✓ | – | – | ✓ |
| Delete / void | ✓ (with audit reason) | – | – | – | – |
Permission gate enforced by requireRole('admin','warehouse','planner','supervisor','operator') on POST /api/inventory/transactions.
Happy path: Supplier delivers 2 540 KG of paper against an order for 2 600 KG. Storeman records qty_received = 2 540, qty_rejected = 0, and adds a note "60 KG short · supplier to credit". GRN posts; v_inventory_summary reflects 2 540 KG. Finance receives an alert via notifications channel and chases the credit-note.
Sad path: Storeman captures 2 600 KG by mistake. Stock count overstated. Recovery: admin voids the transaction (POST /api/inventory/transactions with txn_type=ADJUST and a balancing negative qty) and records the correct figure. Audit trail preserves both rows.
Happy path: 12 reels arrive; 1 reel has a torn outer wrap. Storeman captures 11 reels into PSTAT at AVAILABLE status, and 1 reel into PSTAT-QC at QUARANTINE status with reason DAMAGE. QC officer inspects within 48h, either releases or scraps via txn_type=SCRAP.
Sad path: Storeman lumps all 12 reels into AVAILABLE. Damaged reel makes it onto a Job Card, defective product is run, batch fails QC. Recovery requires a back-dated SCRAP transaction and root-cause investigation.
Happy path: Driver brings TNPL 1200 instead of MONDI 1200. Storeman queries the open PO — if substitute is approved by Procurement, captures against the correct item code. If not, refuses delivery and adds a note on the source PO; no GRN is generated.
Sad path: Storeman captures against MONDI 1200 anyway. BOM validation later picks up zero MONDI 1200 stock once the wrong reels are issued, and a TNPL stockout shows as a hidden surplus. Recovery: ADJUST out of MONDI 1200, ADJUST into TNPL 1200, with linked audit reasons.
Happy path: Adhesive lot has a manufacturer expiry of 2027-05-08. Storeman captures expiry_date on the inventory row. Inventory reservation FIFO honours the expiry, so older lot is consumed first.
Sad path: Expiry not captured. Stock ageing not visible in v_inventory_summary. Recovery: cycle-count event creates a fresh inventory row with expiry data; old row scrapped.
documents.po_nodocuments.kind=SUPPLIER_DOCrm_stock_snapshots)