po pending approval list , tearms and notes

This commit is contained in:
Gowtham M 2026-07-17 09:44:00 +05:30
parent 355f5a9f6d
commit 1cd5826ea5
14 changed files with 566 additions and 3 deletions

View File

@ -560,6 +560,21 @@ model payment_terms {
vendors vendors[] vendors vendors[]
} }
/// Default terms & conditions / notes text per document type (one active row per type).
model terms_notes {
id BigInt @id @default(autoincrement())
type String @unique @db.VarChar(30)
notes String
is_active Boolean @default(true)
created_by BigInt?
updated_by BigInt?
created_at DateTime @default(now()) @db.Timestamptz(6)
updated_at DateTime @default(now()) @db.Timestamptz(6)
deleted_at DateTime? @db.Timestamptz(6)
users_terms_notes_created_byTousers users? @relation("terms_notes_created_byTousers", fields: [created_by], references: [id], onUpdate: NoAction)
users_terms_notes_updated_byTousers users? @relation("terms_notes_updated_byTousers", fields: [updated_by], references: [id], onUpdate: NoAction)
}
model permissions { model permissions {
id BigInt @id @default(autoincrement()) id BigInt @id @default(autoincrement())
module_id BigInt module_id BigInt
@ -855,6 +870,8 @@ model users {
password_reset_tokens password_reset_tokens[] password_reset_tokens password_reset_tokens[]
payment_terms_payment_terms_created_byTousers payment_terms[] @relation("payment_terms_created_byTousers") payment_terms_payment_terms_created_byTousers payment_terms[] @relation("payment_terms_created_byTousers")
payment_terms_payment_terms_updated_byTousers payment_terms[] @relation("payment_terms_updated_byTousers") payment_terms_payment_terms_updated_byTousers payment_terms[] @relation("payment_terms_updated_byTousers")
terms_notes_terms_notes_created_byTousers terms_notes[] @relation("terms_notes_created_byTousers")
terms_notes_terms_notes_updated_byTousers terms_notes[] @relation("terms_notes_updated_byTousers")
locations_locations_created_byTousers locations[] @relation("locations_created_byTousers") locations_locations_created_byTousers locations[] @relation("locations_created_byTousers")
locations_locations_updated_byTousers locations[] @relation("locations_updated_byTousers") locations_locations_updated_byTousers locations[] @relation("locations_updated_byTousers")
po_approvals po_approvals[] po_approvals po_approvals[]

View File

@ -0,0 +1,35 @@
-- terms_notes: default terms & conditions / notes per document type.
-- One row per type (PO, INVOICE, ...). Idempotent.
BEGIN;
CREATE TABLE IF NOT EXISTS terms_notes (
id BIGSERIAL PRIMARY KEY,
type VARCHAR(30) NOT NULL,
notes TEXT NOT NULL,
is_active BOOLEAN NOT NULL DEFAULT TRUE,
created_by BIGINT,
updated_by BIGINT,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
deleted_at TIMESTAMPTZ
);
CREATE UNIQUE INDEX IF NOT EXISTS terms_notes_type_key ON terms_notes(type);
ALTER TABLE terms_notes DROP CONSTRAINT IF EXISTS terms_notes_type_check;
ALTER TABLE terms_notes
ADD CONSTRAINT terms_notes_type_check
CHECK (type IN ('PO', 'INVOICE'));
ALTER TABLE terms_notes DROP CONSTRAINT IF EXISTS fk_terms_notes_created_by;
ALTER TABLE terms_notes
ADD CONSTRAINT fk_terms_notes_created_by FOREIGN KEY (created_by) REFERENCES users(id);
ALTER TABLE terms_notes DROP CONSTRAINT IF EXISTS fk_terms_notes_updated_by;
ALTER TABLE terms_notes
ADD CONSTRAINT fk_terms_notes_updated_by FOREIGN KEY (updated_by) REFERENCES users(id);
COMMIT;
-- Verify:
-- SELECT * FROM terms_notes;

View File

@ -8,6 +8,7 @@ tags:
- name: HSN Codes - name: HSN Codes
- name: Payment Terms - name: Payment Terms
- name: Delivery Terms - name: Delivery Terms
- name: Terms Notes
- name: Departments - name: Departments
- name: Designations - name: Designations
- name: Locations - name: Locations
@ -301,6 +302,23 @@ components:
name: { type: string, example: 'Free on Road' } name: { type: string, example: 'Free on Road' }
description: { type: string, example: 'Delivered to site' } description: { type: string, example: 'Delivered to site' }
is_active: { type: boolean, example: true } is_active: { type: boolean, example: true }
TermsNotesCreateBody:
type: object
required: [type, notes]
properties:
type: { type: string, enum: [PO, INVOICE], example: PO }
notes:
type: string
example: |
1. Payment as per agreed terms.
2. Goods must match PO specification.
is_active: { type: boolean, example: true }
TermsNotesUpdateBody:
type: object
minProperties: 1
properties:
notes: { type: string }
is_active: { type: boolean }
DeliveryTermsUpdateBody: DeliveryTermsUpdateBody:
type: object type: object
minProperties: 1 minProperties: 1
@ -1174,6 +1192,103 @@ paths:
description: Deleted description: Deleted
content: { application/json: { schema: { $ref: "#/components/schemas/ApiResponse" } } } content: { application/json: { schema: { $ref: "#/components/schemas/ApiResponse" } } }
"404": { description: Not found } "404": { description: Not found }
/masters/terms-notes/types:
get:
tags: [Terms Notes]
summary: List terms_notes type options (PO, INVOICE)
responses:
"200":
description: Types fetched
content: { application/json: { schema: { $ref: "#/components/schemas/ApiResponse" } } }
/masters/terms-notes/by-type/{type}:
get:
tags: [Terms Notes]
summary: Get terms notes by document type
parameters:
- in: path
name: type
required: true
schema: { type: string, enum: [PO, INVOICE] }
responses:
"200":
description: Fetched
content: { application/json: { schema: { $ref: "#/components/schemas/ApiResponse" } } }
"404": { description: Not found for type }
/masters/terms-notes:
get:
tags: [Terms Notes]
summary: List terms notes (one row per document type)
description: |
Default terms & conditions text used when creating documents.
On PO create, if `terms_and_conditions` is empty, the active `PO` notes are applied automatically.
parameters:
- in: query
name: page
schema: { type: integer, default: 1 }
- in: query
name: limit
schema: { type: integer, default: 20, maximum: 100 }
- in: query
name: type
schema: { type: string, enum: [PO, INVOICE] }
- in: query
name: search
schema: { type: string }
- in: query
name: is_active
schema: { type: boolean }
responses:
"200":
description: List fetched
content: { application/json: { schema: { $ref: "#/components/schemas/ApiResponse" } } }
post:
tags: [Terms Notes]
summary: Create terms notes for a type (one entry per type)
requestBody:
required: true
content:
application/json:
schema: { $ref: "#/components/schemas/TermsNotesCreateBody" }
responses:
"201":
description: Created
content: { application/json: { schema: { $ref: "#/components/schemas/ApiResponse" } } }
"409": { description: Type already exists }
/masters/terms-notes/{id}:
parameters:
- in: path
name: id
required: true
schema: { type: string }
get:
tags: [Terms Notes]
summary: Get terms notes by id
responses:
"200":
description: Fetched
content: { application/json: { schema: { $ref: "#/components/schemas/ApiResponse" } } }
"404": { description: Not found }
put:
tags: [Terms Notes]
summary: Update terms notes
requestBody:
required: true
content:
application/json:
schema: { $ref: "#/components/schemas/TermsNotesUpdateBody" }
responses:
"200":
description: Updated
content: { application/json: { schema: { $ref: "#/components/schemas/ApiResponse" } } }
"404": { description: Not found }
delete:
tags: [Terms Notes]
summary: Soft-delete terms notes
responses:
"200":
description: Deleted
content: { application/json: { schema: { $ref: "#/components/schemas/ApiResponse" } } }
"404": { description: Not found }
/masters/delivery-terms: /masters/delivery-terms:
get: get:
tags: [Delivery Terms] tags: [Delivery Terms]

View File

@ -110,6 +110,29 @@ paths:
content: content:
text/csv: text/csv:
schema: { type: string } schema: { type: string }
/purchase-orders/pending-approval:
get:
tags: [Purchase Orders]
summary: List POs pending approval (approver inbox)
description: |
Requires `PURCHASE_ORDER` → `approve`.
Returns only POs with status `PENDING_APPROVAL`.
Same filters as the main list except `status` is forced to PENDING_APPROVAL.
parameters:
- { name: page, in: query, schema: { type: integer, example: 1 } }
- { name: limit, in: query, schema: { type: integer, example: 20 } }
- { name: search, in: query, schema: { type: string, example: 'PO/2026' } }
- { name: vendor_type, in: query, schema: { type: string, example: RAW_MATERIAL } }
- { name: vendor_id, in: query, schema: { type: integer, example: 1 } }
- { name: billing_id, in: query, schema: { type: integer, example: 1 } }
- { name: shipping_id, in: query, schema: { type: integer, example: 2 } }
- { name: date_from, in: query, schema: { type: string, format: date } }
- { name: date_to, in: query, schema: { type: string, format: date } }
responses:
'200':
description: Pending approval POs fetched
content: { application/json: { schema: { $ref: '#/components/schemas/ApiResponse' } } }
'403': { description: Missing PURCHASE_ORDER approve permission }
/purchase-orders: /purchase-orders:
get: get:
tags: [Purchase Orders] tags: [Purchase Orders]

View File

@ -8,6 +8,7 @@ router.use('/gst-rates', require('./gst-rates/gst-rates.routes'));
router.use('/hsn-codes', require('./hsn-codes/hsn-codes.routes')); router.use('/hsn-codes', require('./hsn-codes/hsn-codes.routes'));
router.use('/payment-terms', require('./payment-terms/payment-terms.routes')); router.use('/payment-terms', require('./payment-terms/payment-terms.routes'));
router.use('/delivery-terms', require('./delivery-terms/delivery-terms.routes')); router.use('/delivery-terms', require('./delivery-terms/delivery-terms.routes'));
router.use('/terms-notes', require('./terms-notes/terms-notes.routes'));
router.use('/departments', require('./departments/departments.routes')); router.use('/departments', require('./departments/departments.routes'));
router.use('/designations', require('./designations/designations.routes')); router.use('/designations', require('./designations/designations.routes'));
router.use('/document-series', require('./document-series/document-series.routes')); router.use('/document-series', require('./document-series/document-series.routes'));

View File

@ -0,0 +1,3 @@
const TERMS_NOTE_TYPES = ['PO', 'INVOICE'];
module.exports = { TERMS_NOTE_TYPES };

View File

@ -0,0 +1,50 @@
const asyncHandler = require('../../../utils/asyncHandler');
const ApiResponse = require('../../../utils/ApiResponse');
const service = require('./terms-notes.service');
const create = asyncHandler(async (req, res) => {
const data = await service.createTermsNote(req.body, req.user?.id, req.id);
res.status(201).json(new ApiResponse(201, data, 'terms_notes created successfully'));
});
const list = asyncHandler(async (req, res) => {
const result = await service.listTermsNotes(req.query);
res.json(new ApiResponse(200, result.data, 'terms_notes list fetched', result.meta));
});
const exportCsv = asyncHandler(async (req, res) => {
const csv = await service.exportTermsNotes(req.query);
res.setHeader('Content-Type', 'text/csv; charset=utf-8');
res.setHeader('Content-Disposition', 'attachment; filename="terms-notes-export.csv"');
res.send(csv);
});
const listTypes = asyncHandler(async (_req, res) => {
const data = service.listTermsNoteTypes();
res.json(new ApiResponse(200, data, 'terms_notes types fetched'));
});
const getByType = asyncHandler(async (req, res) => {
const data = await service.getTermsNoteByType(req.params.type);
if (!data) {
return res.status(404).json(new ApiResponse(404, null, 'terms_notes not found for type'));
}
res.json(new ApiResponse(200, data, 'terms_notes fetched'));
});
const getOne = asyncHandler(async (req, res) => {
const data = await service.getTermsNoteById(req.params.id);
res.json(new ApiResponse(200, data, 'terms_notes fetched'));
});
const update = asyncHandler(async (req, res) => {
const data = await service.updateTermsNote(req.params.id, req.body, req.user?.id, req.id);
res.json(new ApiResponse(200, data, 'terms_notes updated successfully'));
});
const remove = asyncHandler(async (req, res) => {
await service.deleteTermsNote(req.params.id, req.user?.id, req.id);
res.json(new ApiResponse(200, null, 'terms_notes deleted successfully'));
});
module.exports = { create, list, exportCsv, listTypes, getByType, getOne, update, remove };

View File

@ -0,0 +1,35 @@
const express = require('express');
const authenticate = require('../../../middlewares/auth.middleware');
const authorize = require('../../../middlewares/rbac.middleware');
const validate = require('../../../middlewares/validate.middleware');
const controller = require('./terms-notes.controller');
const {
createSchema,
updateSchema,
listQuerySchema,
exportQuerySchema,
} = require('./terms-notes.validation');
const router = express.Router();
router.use(authenticate);
router.get('/types', authorize('MASTERS', 'view'), controller.listTypes);
router.get(
'/by-type/:type',
authorize('MASTERS', 'view'),
controller.getByType
);
router.get('/', authorize('MASTERS', 'view'), validate(listQuerySchema, 'query'), controller.list);
router.get(
'/export',
authorize('MASTERS', 'export'),
validate(exportQuerySchema, 'query'),
controller.exportCsv
);
router.get('/:id', authorize('MASTERS', 'view'), controller.getOne);
router.post('/', authorize('MASTERS', 'create'), validate(createSchema), controller.create);
router.put('/:id', authorize('MASTERS', 'edit'), validate(updateSchema), controller.update);
router.delete('/:id', authorize('MASTERS', 'delete'), controller.remove);
module.exports = router;

View File

@ -0,0 +1,222 @@
const prisma = require('../../../config/prisma');
const ApiError = require('../../../utils/ApiError');
const auditLog = require('../../../utils/auditLog');
const { getPagination, isDropdownCall } = require('../../../utils/pagination');
const { parseId } = require('../../../utils/parseId');
const { rowsToCsv } = require('../../../utils/csv');
const { TERMS_NOTE_TYPES } = require('./terms-notes.constants');
const TABLE_NAME = 'terms_notes';
const sanitize = (row) => {
if (!row) return null;
return { ...row };
};
const buildWhere = (query) => ({
deleted_at: null,
...(query.type ? { type: String(query.type).toUpperCase() } : {}),
...(query.is_active !== undefined ? { is_active: query.is_active } : {}),
...(query.search
? {
OR: [
{ type: { contains: query.search, mode: 'insensitive' } },
{ notes: { contains: query.search, mode: 'insensitive' } },
],
}
: {}),
});
const createTermsNote = async (payload, userId, requestId) => {
const type = String(payload.type).toUpperCase();
if (!TERMS_NOTE_TYPES.includes(type)) {
throw new ApiError(422, `type must be one of: ${TERMS_NOTE_TYPES.join(', ')}`);
}
const existing = await prisma.terms_notes.findFirst({
where: { type, deleted_at: null },
});
if (existing) {
throw new ApiError(409, `terms_notes already exists for type ${type}`);
}
// Revive soft-deleted row for the same type if present
const softDeleted = await prisma.terms_notes.findFirst({
where: { type, deleted_at: { not: null } },
});
let created;
if (softDeleted) {
created = await prisma.terms_notes.update({
where: { id: softDeleted.id },
data: {
notes: payload.notes,
is_active: payload.is_active ?? true,
deleted_at: null,
updated_by: userId ? BigInt(userId) : null,
created_by: userId ? BigInt(userId) : softDeleted.created_by,
},
});
} else {
created = await prisma.terms_notes.create({
data: {
type,
notes: payload.notes,
is_active: payload.is_active ?? true,
created_by: userId ? BigInt(userId) : null,
updated_by: userId ? BigInt(userId) : null,
},
});
}
const mapped = sanitize(created);
await auditLog({
tableName: TABLE_NAME,
recordId: created.id,
action: 'CREATE',
oldValue: null,
newValue: mapped,
userId,
requestId,
});
return mapped;
};
const listTermsNotes = async (query) => {
const where = buildWhere(query);
if (isDropdownCall(query)) {
where.is_active = true;
const rows = await prisma.terms_notes.findMany({
where,
orderBy: { type: 'asc' },
});
const data = rows.map(sanitize);
return { data, meta: { page: 1, limit: data.length, total: data.length, dropdown: true } };
}
const { page, limit, skip } = getPagination(query);
const [rows, total] = await Promise.all([
prisma.terms_notes.findMany({
where,
orderBy: { type: 'asc' },
skip,
take: limit,
}),
prisma.terms_notes.count({ where }),
]);
return { data: rows.map(sanitize), meta: { page, limit, total } };
};
const exportTermsNotes = async (query) => {
const rows = await prisma.terms_notes.findMany({
where: buildWhere(query),
orderBy: { type: 'asc' },
});
return rowsToCsv(
[
{ key: 'type', header: 'Type' },
{ key: 'notes', header: 'Notes' },
{ key: 'is_active', header: 'Active' },
{ key: 'created_at', header: 'Created At', type: 'datetime' },
],
rows.map(sanitize)
);
};
const getTermsNoteById = async (id) => {
const row = await prisma.terms_notes.findFirst({
where: { id: parseId(id), deleted_at: null },
});
if (!row) throw new ApiError(404, 'terms_notes not found');
return sanitize(row);
};
const getTermsNoteByType = async (type, { requireActive = true } = {}) => {
const normalized = String(type || '').toUpperCase();
if (!TERMS_NOTE_TYPES.includes(normalized)) {
throw new ApiError(422, `type must be one of: ${TERMS_NOTE_TYPES.join(', ')}`);
}
const row = await prisma.terms_notes.findFirst({
where: {
type: normalized,
deleted_at: null,
...(requireActive ? { is_active: true } : {}),
},
});
return sanitize(row);
};
/** Returns notes text for a type, or null if none configured. */
const getDefaultNotesText = async (type) => {
const normalized = String(type || '').toUpperCase();
if (!TERMS_NOTE_TYPES.includes(normalized)) return null;
const row = await prisma.terms_notes.findFirst({
where: { type: normalized, deleted_at: null, is_active: true },
});
return row?.notes || null;
};
const updateTermsNote = async (id, payload, userId, requestId) => {
const existing = await getTermsNoteById(id);
const updated = await prisma.terms_notes.update({
where: { id: parseId(id) },
data: {
...(payload.notes !== undefined ? { notes: payload.notes } : {}),
...(payload.is_active !== undefined ? { is_active: payload.is_active } : {}),
updated_by: userId ? BigInt(userId) : null,
},
});
const mapped = sanitize(updated);
await auditLog({
tableName: TABLE_NAME,
recordId: id,
action: 'UPDATE',
oldValue: existing,
newValue: mapped,
userId,
requestId,
});
return mapped;
};
const deleteTermsNote = async (id, userId, requestId) => {
const existing = await getTermsNoteById(id);
await prisma.terms_notes.update({
where: { id: parseId(id) },
data: {
deleted_at: new Date(),
is_active: false,
updated_by: userId ? BigInt(userId) : null,
},
});
await auditLog({
tableName: TABLE_NAME,
recordId: id,
action: 'DELETE',
oldValue: existing,
newValue: { deleted_at: new Date() },
userId,
requestId,
});
};
const listTermsNoteTypes = () =>
TERMS_NOTE_TYPES.map((value) => ({ value, label: value }));
module.exports = {
createTermsNote,
listTermsNotes,
exportTermsNotes,
getTermsNoteById,
getTermsNoteByType,
getDefaultNotesText,
updateTermsNote,
deleteTermsNote,
listTermsNoteTypes,
};

View File

@ -0,0 +1,32 @@
const Joi = require('joi');
const { listQuerySchema, toExportQuerySchema } = require('../_shared/masters.validation');
const { TERMS_NOTE_TYPES } = require('./terms-notes.constants');
const createSchema = Joi.object({
type: Joi.string()
.valid(...TERMS_NOTE_TYPES)
.required(),
notes: Joi.string().trim().min(1).required(),
is_active: Joi.boolean().default(true),
});
const updateSchema = Joi.object({
notes: Joi.string().trim().min(1).optional(),
is_active: Joi.boolean().optional(),
}).min(1);
const listTermsNotesQuerySchema = listQuerySchema.keys({
type: Joi.string()
.valid(...TERMS_NOTE_TYPES)
.optional(),
});
const exportQuerySchema = toExportQuerySchema(listTermsNotesQuerySchema);
module.exports = {
createSchema,
updateSchema,
listQuerySchema: listTermsNotesQuerySchema,
exportQuerySchema,
TERMS_NOTE_TYPES,
};

View File

@ -14,6 +14,13 @@ const list = asyncHandler(async (req, res) => {
res.json(new ApiResponse(200, result.data, 'Purchase orders fetched', result.meta)); res.json(new ApiResponse(200, result.data, 'Purchase orders fetched', result.meta));
}); });
const listPendingApproval = asyncHandler(async (req, res) => {
const result = await service.listPendingApprovalPurchaseOrders(req.query);
res.json(
new ApiResponse(200, result.data, 'Purchase orders pending approval fetched', result.meta)
);
});
const exportCsv = asyncHandler(async (req, res) => { const exportCsv = asyncHandler(async (req, res) => {
const csv = await service.exportPurchaseOrders(req.query); const csv = await service.exportPurchaseOrders(req.query);
res.setHeader('Content-Type', 'text/csv; charset=utf-8'); res.setHeader('Content-Type', 'text/csv; charset=utf-8');
@ -117,6 +124,7 @@ const removeAttachment = asyncHandler(async (req, res) => {
module.exports = { module.exports = {
create, create,
list, list,
listPendingApproval,
exportCsv, exportCsv,
getOne, getOne,
update, update,

View File

@ -24,6 +24,12 @@ router.get(
validate(exportPurchaseOrdersQuerySchema, 'query'), validate(exportPurchaseOrdersQuerySchema, 'query'),
controller.exportCsv controller.exportCsv
); );
router.get(
'/pending-approval',
authorize('PURCHASE_ORDER', 'approve'),
validate(listPurchaseOrdersQuerySchema, 'query'),
controller.listPendingApproval
);
router.get( router.get(
'/', '/',
authorize('PURCHASE_ORDER', 'view'), authorize('PURCHASE_ORDER', 'view'),

View File

@ -289,7 +289,7 @@ const isInterStateSupply = (billingLocation, vendor) => {
return placeOfSupply !== vendorState; return placeOfSupply !== vendorState;
}; };
const buildHeaderData = async (payload, builtItems, userId) => { const buildHeaderData = async (payload, builtItems, userId, { applyDefaultTerms = false } = {}) => {
const vendor = await assertReference('vendors', payload.vendor_id, 'vendor_id'); const vendor = await assertReference('vendors', payload.vendor_id, 'vendor_id');
const billing = await assertAnyLocation(payload.billing_id, 'billing_id'); const billing = await assertAnyLocation(payload.billing_id, 'billing_id');
await assertAnyLocation(payload.shipping_id, 'shipping_id'); await assertAnyLocation(payload.shipping_id, 'shipping_id');
@ -315,6 +315,16 @@ const buildHeaderData = async (payload, builtItems, userId) => {
const gstSplit = splitGst(totals.tax_total, isInterStateSupply(billing, vendor)); const gstSplit = splitGst(totals.tax_total, isInterStateSupply(billing, vendor));
let termsAndConditions =
payload.terms_and_conditions !== undefined && payload.terms_and_conditions !== null
? String(payload.terms_and_conditions).trim()
: '';
if (applyDefaultTerms && !termsAndConditions) {
const { getDefaultNotesText } = require('../masters/terms-notes/terms-notes.service');
const defaultNotes = await getDefaultNotesText('PO');
if (defaultNotes) termsAndConditions = defaultNotes;
}
return { return {
po_date: toDateOnly(payload.po_date), po_date: toDateOnly(payload.po_date),
vendor_id: BigInt(payload.vendor_id), vendor_id: BigInt(payload.vendor_id),
@ -325,7 +335,7 @@ const buildHeaderData = async (payload, builtItems, userId) => {
expected_delivery_date: payload.expected_delivery_date expected_delivery_date: payload.expected_delivery_date
? toDateOnly(payload.expected_delivery_date) ? toDateOnly(payload.expected_delivery_date)
: null, : null,
terms_and_conditions: payload.terms_and_conditions || null, terms_and_conditions: termsAndConditions || null,
remarks: payload.remarks || null, remarks: payload.remarks || null,
tds_applicable: tdsApplicable, tds_applicable: tdsApplicable,
tds_section_pct: tdsApplicable ? tdsSectionPct : null, tds_section_pct: tdsApplicable ? tdsSectionPct : null,
@ -449,7 +459,7 @@ const hasReceipts = (po) =>
const createPurchaseOrder = async (payload, userId, requestId) => { const createPurchaseOrder = async (payload, userId, requestId) => {
const builtItems = await validateAndBuildItems(payload.items); const builtItems = await validateAndBuildItems(payload.items);
const header = await buildHeaderData(payload, builtItems, userId); const header = await buildHeaderData(payload, builtItems, userId, { applyDefaultTerms: true });
const poNumber = await nextDocumentNumber('PO'); const poNumber = await nextDocumentNumber('PO');
header.po_number = poNumber; header.po_number = poNumber;
@ -521,6 +531,10 @@ const listPurchaseOrders = async (query) => {
return { data: rows.map(sanitizePo), meta: { page, limit, total } }; return { data: rows.map(sanitizePo), meta: { page, limit, total } };
}; };
/** Approver inbox — only POs in PENDING_APPROVAL. */
const listPendingApprovalPurchaseOrders = async (query) =>
listPurchaseOrders({ ...query, status: 'PENDING_APPROVAL' });
const exportPurchaseOrders = async (query) => { const exportPurchaseOrders = async (query) => {
const rows = await prisma.purchase_orders.findMany({ const rows = await prisma.purchase_orders.findMany({
where: buildPurchaseOrdersWhere(query), where: buildPurchaseOrdersWhere(query),
@ -915,6 +929,7 @@ const getPurchaseOrderPdf = async (id) => {
module.exports = { module.exports = {
createPurchaseOrder, createPurchaseOrder,
listPurchaseOrders, listPurchaseOrders,
listPendingApprovalPurchaseOrders,
exportPurchaseOrders, exportPurchaseOrders,
getPurchaseOrderById, getPurchaseOrderById,
updatePurchaseOrder, updatePurchaseOrder,

View File

@ -9,6 +9,7 @@ const PG_CODES = {
const CONSTRAINT_MESSAGES = { const CONSTRAINT_MESSAGES = {
item_categories_category_type_check: 'Invalid category type. Allowed values: STOCK, ASSET.', item_categories_category_type_check: 'Invalid category type. Allowed values: STOCK, ASSET.',
terms_notes_type_check: 'Invalid terms_notes type. Allowed values: PO, INVOICE.',
}; };
const TABLE_MESSAGES = {}; const TABLE_MESSAGES = {};