feat(procurement): enhance purchase order and GRN functionalities
- Updated purchase order descriptions for clarity on draft and submission processes. - Implemented submit and delete functionalities for draft purchase orders, allowing users to manage their orders more effectively. - Added discount and VAT fields to GRN lines, enabling better cost tracking and reporting. - Enhanced validation for GRN lines to ensure discount and VAT percentages are within acceptable ranges. - Updated API to support new functionalities, including submitting and deleting purchase orders. - Improved UI components for better user experience in managing purchase orders and GRNs. - Documented changes in security and backend phase documentation to reflect new processes and requirements.
This commit is contained in:
+3
-3
@@ -83,9 +83,9 @@ These are known, deliberately-accepted Phase-1 exposures. Each has a compensatin
|
||||
- [ ] Note in review: **AR-01/AR-02/AR-03** apply to these endpoints
|
||||
|
||||
### C.3 GRN
|
||||
- [ ] `unitCost` **derived from the PO line server-side**; any client-supplied cost is ignored *(decision locked)*
|
||||
- [ ] `receivedValue` computed server-side (qty × PO-line cost), not accepted from client
|
||||
- [ ] Direct GRN (no PO) is the exception where cost is entered → extra scrutiny + review flag + audit (**AR-04**)
|
||||
- [ ] `unitCost` **defaults to the PO line price**; a per-line override **is now permitted** *(decision revised 2026-07-20 — was "locked, client cost ignored")*. When an override is entered it is used, and the PO price is snapshotted (`poUnitPrice`) so a **`priceVariance` is recorded** against it for review. Rationale: one PO legitimately spans batches received at different prices; the variance trail (plus the audit log) is the compensating control that replaces the old hard block.
|
||||
- [ ] **Derived figures stay server-computed** — `netUnitCost`/`receivedValue`/`vatAmount`/`lineTotal` are never accepted from the client, so the client cannot inflate stock value except by an *auditable* unit-cost override. Discount reduces inventory cost; **VAT is recoverable and never enters stock value**.
|
||||
- [ ] Direct GRN (no PO) remains the higher-scrutiny path where cost is entered with no PO to compare against → review flag + audit (**AR-04**)
|
||||
- [ ] Over-receipt tolerance enforced server-side → `OVER_RECEIPT_TOLERANCE`
|
||||
- [ ] On-hold stock is not issuable (FR-WH-07); expired batch blocked
|
||||
|
||||
|
||||
@@ -137,7 +137,7 @@ One base currency; invoicing/3-way match in Accounting (GRN carries data); users
|
||||
| FR-PROC-02 | Optional **RFQ**: issue to vendors, record quotations for comparison. | S |
|
||||
| FR-PROC-03 | Generate **PO** from PR/RFQ or directly (item, UOM, qty, price, tax, delivery date, warehouse). | M |
|
||||
| FR-PROC-04 | **[Phase 1: auto-approve]** Auto-approve PO on creation (status `Approved`). Config flag `approvalRequired` (default off) gates a future approval workflow (authorization matrix); when on, PO cannot issue until approved. `PendingApproval` state + approval fields retained in schema (no migration to enable). | M |
|
||||
| FR-PROC-05 | **[Phase 1: Option B — edit-while-open]** PO may be **freely edited while open** (not fully received/closed); changes take effect immediately with an audit entry. Versioned amendments deferred; schema must not preclude adding a version field later. | S |
|
||||
| FR-PROC-05 | **[Phase 1: Option B *superseded* 2026-07-20 — draft-lock]** A PO is **editable and deletable only while `Draft`**; **submitting locks it** (Draft → Approved) and no further edit/delete/add-line is allowed — an issued PO is corrected by Cancel-with-reason (blocked once receipts exist) or a reversing document, never edited. Create takes `saveAsDraft` (default `false` → auto-approve, preserving the Requisition→PO / RFQ→PO flows). *Why the reversal:* Option B ("freely edit while open") let an already-issued, vendor-facing PO change silently after the fact; the draft/submit boundary makes "issued to vendor" a real, immutable commitment. Versioned amendments still deferred; schema unchanged (reuses the existing `Draft` enum value). | S |
|
||||
| FR-PROC-06 | PO lifecycle: Draft → (PendingApproval →) Approved → PartiallyReceived → FullyReceived → Closed/Cancelled. Phase 1 bypasses PendingApproval via auto-approve. | M |
|
||||
| FR-PROC-07 | Support **partial receipt**; PO stays open until fully received or manually closed. | M |
|
||||
| FR-PROC-08 | Support **Purchase Return** referencing original GRN/PO line; generates outbound movement. | M |
|
||||
@@ -151,7 +151,7 @@ One base currency; invoicing/3-way match in Accounting (GRN carries data); users
|
||||
| FR-GRN-03 | Support **over/under-receipt tolerances** (per item or global); warn or block beyond tolerance. | S |
|
||||
| FR-GRN-04 | Capture **batch + expiry** and/or **serial numbers** for tracked items on receipt. | M |
|
||||
| FR-GRN-05 | Allow receipt into **inspection/quarantine hold** (not issuable) pending QC, before QC module exists. | M |
|
||||
| FR-GRN-06 | On confirm, each line **creates a FIFO cost layer** at unit cost (PO price + attributable charges; landed cost per §B.1.2.1) and posts an inbound ledger entry. | M |
|
||||
| FR-GRN-06 | On confirm, each line **creates a FIFO cost layer** at the **after-discount net unit cost** (`unitCost × (1 − discountPct/100)`) and posts an inbound ledger entry. **VAT never enters stock value** — it is recoverable input tax (revised 2026-07-20). PO price is the default unit cost; a per-line override is permitted and recorded as a variance (see 02-SECURITY C.3, revised). | M |
|
||||
| FR-GRN-07 | Record **received value per line** and PO reference for downstream matching. | M |
|
||||
| FR-GRN-08 | Assign received stock to a **bin/location** (putaway). | S |
|
||||
|
||||
|
||||
@@ -457,14 +457,15 @@ Query: `q`, `status` (`Open|Closed`), + paging. → list envelope of
|
||||
`GET /rfqs/{rfqId}/comparison` → vendor-by-line price matrix.
|
||||
|
||||
### 3.3 Purchase Orders
|
||||
> **Phase 1:** `approvalRequired` defaults `false` → PO **auto-approved on creation**. Approve endpoint exists but is a no-op unless enabled (FR-PROC-04). PO **freely editable while open** (Option B, FR-PROC-05).
|
||||
> **Phase 1:** `approvalRequired` defaults `false`. Create takes **`saveAsDraft`** (default `false` → **auto-approved on creation**; `true` → `Draft`). Approve endpoint exists but is a no-op unless enabled (FR-PROC-04). **A PO is editable/deletable only while `Draft`; submitting locks it** (FR-PROC-05, revised 2026-07-20 — Option B "freely edit while open" superseded).
|
||||
|
||||
#### `POST /purchase-orders`
|
||||
```json
|
||||
{ "vendorId": 5, "requisitionId": 210,
|
||||
{ "vendorId": 5, "requisitionId": 210, "saveAsDraft": false,
|
||||
"lines": [ { "itemId": 1001, "uomId": 1, "warehouseId": 1, "qty": 5000, "unitPrice": 12.50, "tax": 0.18 },
|
||||
{ "itemId": 1002, "uomId": 1, "warehouseId": 1, "qty": 8000, "unitPrice": 6.20, "tax": 0.18 } ] }
|
||||
```
|
||||
`saveAsDraft` optional (default `false`). When `true` the response `status` is `Draft`.
|
||||
**201 Created** — `Location: /api/v1/purchase-orders/342`
|
||||
```json
|
||||
{ "poId": 342, "docNo": "PO-2026-00342", "vendorId": 5, "requisitionId": 210,
|
||||
@@ -477,7 +478,11 @@ Query: `q`, `status` (`Open|Closed`), + paging. → list envelope of
|
||||
`GET /purchase-orders?status=Approved&vendorId=5` → list envelope of PO summaries.
|
||||
|
||||
#### `PUT /purchase-orders/{poId}`
|
||||
Edit while open (not FullyReceived/Closed/Cancelled); requires `If-Match`. → **200 OK** updated resource; `409 PO_NOT_EDITABLE` if closed.
|
||||
Edit a **Draft only**; requires `If-Match`. → **200 OK** updated resource; `409 PO_NOT_EDITABLE` once submitted (any non-Draft status).
|
||||
|
||||
#### `POST /purchase-orders/{poId}/submit` → **200 OK** — `Draft → Approved`. `409 PO_NOT_EDITABLE` if not Draft.
|
||||
|
||||
#### `DELETE /purchase-orders/{poId}` → **204 No Content** — permitted **only while Draft**; `409 PO_NOT_EDITABLE` once submitted.
|
||||
|
||||
#### `POST /purchase-orders/{poId}/approve` → **200 OK** (no-op in Phase 1; transitions PendingApproval→Approved when enabled).
|
||||
|
||||
@@ -524,15 +529,24 @@ Against a PO (lines default from open PO lines) or direct (`poId: null`, by perm
|
||||
```json
|
||||
{ "poId": 342, "warehouseId": 1,
|
||||
"lines": [ { "poLineId": 900, "itemId": 1001, "uomId": 1, "binId": 45, "qty": 5000,
|
||||
"unitCost": 12.50, "holdStatus": "OnHold",
|
||||
"unitCost": 12.50, "discountPct": 10, "vatPct": 18, "holdStatus": "OnHold",
|
||||
"batch": { "batchNo": "B-2607", "expiryDate": "2028-07-01" } } ] }
|
||||
```
|
||||
**201 Created** — status `Draft`
|
||||
`discountPct`/`vatPct` optional (default 0, range 0–100). `unitCost` on a **PO line** is an optional
|
||||
override: 0/omitted uses the PO price; a value wins and a variance is recorded (02-SECURITY C.3, revised).
|
||||
On a direct receipt `unitCost` is required.
|
||||
**201 Created** — status `Draft`. All derived figures are **server-computed**:
|
||||
`netUnitCost = unitCost × (1 − discountPct/100)`, `receivedValue = qty × netUnitCost` (after discount,
|
||||
**before** VAT — this is the stock value), `vatAmount = receivedValue × vatPct/100`,
|
||||
`lineTotal = receivedValue + vatAmount`, `priceVariance = (unitCost − poUnitPrice) × qty`.
|
||||
```json
|
||||
{ "grnId": 780, "docNo": "GRN-2026-00780", "poId": 342, "vendorId": 5, "warehouseId": 1,
|
||||
"status": "Draft", "createdBy": 17,
|
||||
"lines": [ { "grnLineId": 1300, "poLineId": 900, "itemId": 1001, "uomId": 1, "binId": 45,
|
||||
"qty": 5000, "unitCost": 12.50, "receivedValue": 62500.00, "holdStatus": "OnHold", "batchId": 410 } ] }
|
||||
"qty": 5000, "unitCost": 12.50, "poUnitPrice": 12.50, "discountPct": 10.0,
|
||||
"netUnitCost": 11.25, "vatPct": 18.0, "vatAmount": 10125.00,
|
||||
"receivedValue": 56250.00, "lineTotal": 66375.00, "priceVariance": 0.00,
|
||||
"holdStatus": "OnHold", "batchId": 410 } ] }
|
||||
```
|
||||
`422 OVER_RECEIPT_TOLERANCE` if qty exceeds open PO qty beyond tolerance.
|
||||
|
||||
|
||||
Reference in New Issue
Block a user