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:
@@ -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