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:
2026-07-21 10:08:32 +05:30
parent 951961b798
commit 03bc85b788
28 changed files with 594 additions and 66 deletions
+20 -6
View File
@@ -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 0100). `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.