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
+13 -3
View File
@@ -20,10 +20,10 @@ export interface ListPurchaseOrdersParams {
sort?: string
}
/** Editable while open (FR-PROC-05, Option B). The server is authoritative — it returns
* 409 PO_NOT_EDITABLE regardless — this only drives UI affordances. */
/** Editable/deletable only while Draft (FR-PROC-05, revised — submitting locks the PO).
* The server is authoritative (409 PO_NOT_EDITABLE otherwise); this only drives UI affordances. */
export function isPoEditable(status: PurchaseOrderStatus): boolean {
return status !== "FullyReceived" && status !== "Closed" && status !== "Cancelled"
return status === "Draft"
}
export const purchaseOrdersApi = {
@@ -54,6 +54,16 @@ export const purchaseOrdersApi = {
return apiRequest<PurchaseOrder>(`/purchase-orders/${poId}/approve`, { method: "POST" })
},
/** Submit a Draft PO (Draft → Approved). 409 PO_NOT_EDITABLE if not Draft. */
submit(poId: number): Promise<PurchaseOrder> {
return apiRequest<PurchaseOrder>(`/purchase-orders/${poId}/submit`, { method: "POST" })
},
/** Delete a Draft PO. 409 PO_NOT_EDITABLE once submitted. */
remove(poId: number): Promise<void> {
return apiRequest<void>(`/purchase-orders/${poId}`, { method: "DELETE" })
},
/** 409 if any receipt exists against the PO. */
cancel(poId: number, request: CancelPurchaseOrderRequest): Promise<PurchaseOrder> {
return apiRequest<PurchaseOrder>(`/purchase-orders/${poId}/cancel`, { method: "POST", body: request })
@@ -16,6 +16,8 @@ export function validateLine(input: {
uomId: number | null
qty: string
unitCost: string
discountPct: string
vatPct: string
trackingMode: TrackingMode | null
batchNo: string
serialNumbersText: string
@@ -31,6 +33,14 @@ export function validateLine(input: {
const unitCost = Number(input.unitCost)
if (input.unitCost === "" || Number.isNaN(unitCost) || unitCost < 0) errors.unitCost = "Unit cost cannot be negative"
const discountPct = Number(input.discountPct)
if (input.discountPct !== "" && (Number.isNaN(discountPct) || discountPct < 0 || discountPct > 100))
errors.discountPct = "Discount must be 0100%"
const vatPct = Number(input.vatPct)
if (input.vatPct !== "" && (Number.isNaN(vatPct) || vatPct < 0 || vatPct > 100))
errors.vatPct = "VAT must be 0100%"
if (input.trackingMode === "Batch" && !input.batchNo.trim()) {
errors.batchNo = "Batch number is required for this item"
}