Reject reason added for PO

This commit is contained in:
Gowtham M 2026-07-22 18:39:00 +05:30
parent 70deacb969
commit 7516354ce5
6 changed files with 24 additions and 6 deletions

View File

@ -239,7 +239,7 @@ Each sub-master supports: `GET /` (list), `GET /:id`, `POST /`, `PUT /:id`, `DEL
| [x] | DELETE | `/purchase-orders/:id` | delete | Soft delete | | [x] | DELETE | `/purchase-orders/:id` | delete | Soft delete |
| [x] | POST | `/purchase-orders/:id/submit` | edit | Submit for approval | | [x] | POST | `/purchase-orders/:id/submit` | edit | Submit for approval |
| [x] | POST | `/purchase-orders/:id/approve` | approve | Approve PO | | [x] | POST | `/purchase-orders/:id/approve` | approve | Approve PO |
| [x] | POST | `/purchase-orders/:id/reject` | approve | Reject PO | | [x] | POST | `/purchase-orders/:id/reject` | approve | Reject PO (`reject_reason` required; stored on PO) |
| [x] | POST | `/purchase-orders/:id/amend` | edit | Amend PO | | [x] | POST | `/purchase-orders/:id/amend` | edit | Amend PO |
| [x] | POST | `/purchase-orders/:id/cancel` | edit | Cancel PO | | [x] | POST | `/purchase-orders/:id/cancel` | edit | Cancel PO |
| [x] | GET | `/purchase-orders/:id/pdf` | view | PDF export | | [x] | GET | `/purchase-orders/:id/pdf` | view | PDF export |

View File

@ -711,6 +711,7 @@ model purchase_orders {
parent_po_id BigInt? parent_po_id BigInt?
terms_and_conditions String? terms_and_conditions String?
remarks String? remarks String?
reject_reason String?
is_active Boolean @default(true) is_active Boolean @default(true)
created_by BigInt? created_by BigInt?
updated_by BigInt? updated_by BigInt?

View File

@ -0,0 +1,4 @@
-- Persist PO rejection reason on purchase_orders header
ALTER TABLE purchase_orders
ADD COLUMN IF NOT EXISTS reject_reason TEXT;

View File

@ -67,9 +67,10 @@ components:
remarks: { type: string, example: 'Approved for procurement' } remarks: { type: string, example: 'Approved for procurement' }
PurchaseOrdersRejectBody: PurchaseOrdersRejectBody:
type: object type: object
required: [remarks] required: [reject_reason]
properties: properties:
remarks: { type: string, example: 'Rates not competitive' } reject_reason: { type: string, example: 'Rates not competitive' }
remarks: { type: string, example: 'Rates not competitive', description: 'Legacy alias for reject_reason' }
PoAttachmentResponse: PoAttachmentResponse:
type: object type: object
properties: properties:
@ -247,6 +248,7 @@ paths:
post: post:
tags: [Purchase Orders] tags: [Purchase Orders]
summary: Reject purchase order summary: Reject purchase order
description: Requires reject_reason. Stored on purchase_orders.reject_reason and cleared on resubmit/approve.
parameters: parameters:
- { name: id, in: path, required: true, schema: { type: string, example: '1' } } - { name: id, in: path, required: true, schema: { type: string, example: '1' } }
requestBody: requestBody:

View File

@ -697,6 +697,7 @@ const submitPurchaseOrder = async (id, payload, userId, requestId) => {
where: { id: BigInt(id) }, where: { id: BigInt(id) },
data: { data: {
status: 'PENDING_APPROVAL', status: 'PENDING_APPROVAL',
reject_reason: null,
remarks: payload.remarks ?? existing.remarks, remarks: payload.remarks ?? existing.remarks,
updated_by: userId ? BigInt(userId) : null, updated_by: userId ? BigInt(userId) : null,
}, },
@ -739,6 +740,7 @@ const approvePurchaseOrder = async (id, payload, userId, requestId) => {
where: { id: BigInt(id) }, where: { id: BigInt(id) },
data: { data: {
status: 'APPROVED', status: 'APPROVED',
reject_reason: null,
updated_by: userId ? BigInt(userId) : null, updated_by: userId ? BigInt(userId) : null,
}, },
include: poDetailInclude, include: poDetailInclude,
@ -765,12 +767,15 @@ const rejectPurchaseOrder = async (id, payload, userId, requestId) => {
const pendingApproval = existing.po_approvals.find((row) => row.status === 'PENDING'); const pendingApproval = existing.po_approvals.find((row) => row.status === 'PENDING');
if (!pendingApproval) throw new ApiError(409, 'No pending approval step found'); if (!pendingApproval) throw new ApiError(409, 'No pending approval step found');
const rejectReason = String(payload.reject_reason || payload.remarks || '').trim();
if (!rejectReason) throw new ApiError(422, 'reject_reason is required');
const updated = await prisma.$transaction(async (tx) => { const updated = await prisma.$transaction(async (tx) => {
await tx.po_approvals.update({ await tx.po_approvals.update({
where: { id: pendingApproval.id }, where: { id: pendingApproval.id },
data: { data: {
status: 'REJECTED', status: 'REJECTED',
remarks: payload.remarks, remarks: rejectReason,
approver_user_id: userId ? BigInt(userId) : null, approver_user_id: userId ? BigInt(userId) : null,
acted_at: new Date(), acted_at: new Date(),
}, },
@ -780,6 +785,7 @@ const rejectPurchaseOrder = async (id, payload, userId, requestId) => {
where: { id: BigInt(id) }, where: { id: BigInt(id) },
data: { data: {
status: 'REJECTED', status: 'REJECTED',
reject_reason: rejectReason,
updated_by: userId ? BigInt(userId) : null, updated_by: userId ? BigInt(userId) : null,
}, },
include: poDetailInclude, include: poDetailInclude,

View File

@ -111,7 +111,12 @@ const workflowRemarksSchema = Joi.object({
}); });
const rejectPurchaseOrderSchema = Joi.object({ const rejectPurchaseOrderSchema = Joi.object({
remarks: Joi.string().trim().min(1).required(), reject_reason: Joi.string().trim().min(1).optional(),
remarks: Joi.string().trim().min(1).optional(), // legacy alias for reject_reason
})
.or('reject_reason', 'remarks')
.messages({
'object.missing': 'reject_reason is required',
}); });
module.exports = { module.exports = {