pdf,excell related changes
This commit is contained in:
parent
c37ff28833
commit
95cbdcf7c5
@ -18,6 +18,13 @@ const list = asyncHandler(async (req, res) => {
|
|||||||
res.json(new ApiResponse(200, result.data, 'Assets fetched', result.meta));
|
res.json(new ApiResponse(200, result.data, 'Assets fetched', result.meta));
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const exportCsv = asyncHandler(async (req, res) => {
|
||||||
|
const csv = await service.exportAssets(req.query);
|
||||||
|
res.setHeader('Content-Type', 'text/csv; charset=utf-8');
|
||||||
|
res.setHeader('Content-Disposition', 'attachment; filename="assets-export.csv"');
|
||||||
|
res.send(csv);
|
||||||
|
});
|
||||||
|
|
||||||
const getOne = asyncHandler(async (req, res) => {
|
const getOne = asyncHandler(async (req, res) => {
|
||||||
const data = await service.getAssetById(req.params.id);
|
const data = await service.getAssetById(req.params.id);
|
||||||
res.json(new ApiResponse(200, data, 'Asset fetched'));
|
res.json(new ApiResponse(200, data, 'Asset fetched'));
|
||||||
@ -276,6 +283,7 @@ const removeAttachment = asyncHandler(async (req, res) => {
|
|||||||
module.exports = {
|
module.exports = {
|
||||||
create,
|
create,
|
||||||
list,
|
list,
|
||||||
|
exportCsv,
|
||||||
getOne,
|
getOne,
|
||||||
update,
|
update,
|
||||||
remove,
|
remove,
|
||||||
|
|||||||
@ -8,6 +8,7 @@ const {
|
|||||||
createAssetSchema,
|
createAssetSchema,
|
||||||
updateAssetSchema,
|
updateAssetSchema,
|
||||||
listAssetsQuerySchema,
|
listAssetsQuerySchema,
|
||||||
|
exportAssetsQuerySchema,
|
||||||
transferAssetSchema,
|
transferAssetSchema,
|
||||||
amcContractSchema,
|
amcContractSchema,
|
||||||
updateAmcContractSchema,
|
updateAmcContractSchema,
|
||||||
@ -28,6 +29,12 @@ const router = express.Router();
|
|||||||
|
|
||||||
router.use(authenticate);
|
router.use(authenticate);
|
||||||
|
|
||||||
|
router.get(
|
||||||
|
'/export',
|
||||||
|
authorize('ASSET', 'export'),
|
||||||
|
validate(exportAssetsQuerySchema, 'query'),
|
||||||
|
controller.exportCsv
|
||||||
|
);
|
||||||
router.get(
|
router.get(
|
||||||
'/',
|
'/',
|
||||||
authorize('ASSET', 'view'),
|
authorize('ASSET', 'view'),
|
||||||
|
|||||||
@ -3,6 +3,7 @@ const ApiError = require('../../utils/ApiError');
|
|||||||
const auditLog = require('../../utils/auditLog');
|
const auditLog = require('../../utils/auditLog');
|
||||||
const { getPagination } = require('../../utils/pagination');
|
const { getPagination } = require('../../utils/pagination');
|
||||||
const { nextDocumentNumber } = require('../../utils/generateCode');
|
const { nextDocumentNumber } = require('../../utils/generateCode');
|
||||||
|
const { rowsToCsv } = require('../../utils/csv');
|
||||||
const { DISPOSAL_STATUSES, getAssetDropdownOptions } = require('./assets.constants');
|
const { DISPOSAL_STATUSES, getAssetDropdownOptions } = require('./assets.constants');
|
||||||
const { assertPlant, assertWarehouse } = require('../../utils/locations');
|
const { assertPlant, assertWarehouse } = require('../../utils/locations');
|
||||||
const repository = require('./assets.repository');
|
const repository = require('./assets.repository');
|
||||||
@ -307,9 +308,7 @@ const createAsset = async (payload, userId, requestId) => {
|
|||||||
return sanitizeAsset(created);
|
return sanitizeAsset(created);
|
||||||
};
|
};
|
||||||
|
|
||||||
const listAssets = async (query) => {
|
const buildAssetsWhere = (query) => ({
|
||||||
const { page, limit, skip } = getPagination(query);
|
|
||||||
const where = {
|
|
||||||
deleted_at: null,
|
deleted_at: null,
|
||||||
...(query.status ? { status: query.status } : {}),
|
...(query.status ? { status: query.status } : {}),
|
||||||
...(query.condition ? { condition: query.condition } : {}),
|
...(query.condition ? { condition: query.condition } : {}),
|
||||||
@ -330,7 +329,11 @@ const listAssets = async (query) => {
|
|||||||
],
|
],
|
||||||
}
|
}
|
||||||
: {}),
|
: {}),
|
||||||
};
|
});
|
||||||
|
|
||||||
|
const listAssets = async (query) => {
|
||||||
|
const { page, limit, skip } = getPagination(query);
|
||||||
|
const where = buildAssetsWhere(query);
|
||||||
|
|
||||||
const [rows, total] = await Promise.all([
|
const [rows, total] = await Promise.all([
|
||||||
prisma.assets.findMany({
|
prisma.assets.findMany({
|
||||||
@ -346,6 +349,39 @@ const listAssets = async (query) => {
|
|||||||
return { data: rows.map(sanitizeAsset), meta: { page, limit, total } };
|
return { data: rows.map(sanitizeAsset), meta: { page, limit, total } };
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const exportAssets = async (query) => {
|
||||||
|
const rows = await prisma.assets.findMany({
|
||||||
|
where: buildAssetsWhere(query),
|
||||||
|
include: assetInclude,
|
||||||
|
orderBy: { created_at: 'desc' },
|
||||||
|
});
|
||||||
|
|
||||||
|
return rowsToCsv(
|
||||||
|
[
|
||||||
|
{ key: 'asset_code', header: 'Asset Code' },
|
||||||
|
{ key: 'asset_name', header: 'Asset Name' },
|
||||||
|
{ key: (row) => row.item_category?.name || '', header: 'Category' },
|
||||||
|
{ key: (row) => row.item_subcategory?.name || '', header: 'Subcategory' },
|
||||||
|
{ key: 'serial_number', header: 'Serial Number' },
|
||||||
|
{ key: 'condition', header: 'Condition' },
|
||||||
|
{ key: 'status', header: 'Status' },
|
||||||
|
{ key: (row) => row.plant?.name || '', header: 'Plant' },
|
||||||
|
{ key: (row) => row.department?.name || '', header: 'Department' },
|
||||||
|
{ key: (row) => row.warehouse?.name || '', header: 'Warehouse' },
|
||||||
|
{ key: (row) => row.assigned_to_user?.full_name || '', header: 'Assigned To' },
|
||||||
|
{ key: (row) => row.vendor?.vendor_name || '', header: 'Vendor' },
|
||||||
|
{ key: (row) => row.purchase_order?.po_number || '', header: 'PO Number' },
|
||||||
|
{ key: (row) => row.grn?.grn_number || '', header: 'GRN Number' },
|
||||||
|
{ key: 'purchase_date', header: 'Purchase Date', type: 'date' },
|
||||||
|
{ key: 'purchase_cost', header: 'Purchase Cost' },
|
||||||
|
{ key: 'warranty_expiry_date', header: 'Warranty Expiry', type: 'date' },
|
||||||
|
{ key: 'is_active', header: 'Active' },
|
||||||
|
{ key: 'created_at', header: 'Created At', type: 'datetime' },
|
||||||
|
],
|
||||||
|
rows.map(sanitizeAsset)
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
const getAssetById = async (id) => sanitizeAsset(await getAssetOrThrow(id));
|
const getAssetById = async (id) => sanitizeAsset(await getAssetOrThrow(id));
|
||||||
|
|
||||||
const updateAsset = async (id, payload, userId, requestId) => {
|
const updateAsset = async (id, payload, userId, requestId) => {
|
||||||
@ -566,6 +602,7 @@ const previewDepreciation = (payload) => {
|
|||||||
module.exports = {
|
module.exports = {
|
||||||
createAsset,
|
createAsset,
|
||||||
listAssets,
|
listAssets,
|
||||||
|
exportAssets,
|
||||||
getAssetById,
|
getAssetById,
|
||||||
updateAsset,
|
updateAsset,
|
||||||
deleteAsset,
|
deleteAsset,
|
||||||
|
|||||||
@ -108,6 +108,11 @@ const listAssetsQuerySchema = Joi.object({
|
|||||||
is_active: Joi.boolean().optional(),
|
is_active: Joi.boolean().optional(),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const exportAssetsQuerySchema = listAssetsQuerySchema.keys({
|
||||||
|
page: Joi.strip(),
|
||||||
|
limit: Joi.strip(),
|
||||||
|
});
|
||||||
|
|
||||||
const transferAssetSchema = Joi.object({
|
const transferAssetSchema = Joi.object({
|
||||||
transfer_date: Joi.date().iso().required(),
|
transfer_date: Joi.date().iso().required(),
|
||||||
to_plant_id: Joi.number().integer().positive().allow(null).optional(),
|
to_plant_id: Joi.number().integer().positive().allow(null).optional(),
|
||||||
@ -305,6 +310,7 @@ module.exports = {
|
|||||||
createAssetSchema,
|
createAssetSchema,
|
||||||
updateAssetSchema,
|
updateAssetSchema,
|
||||||
listAssetsQuerySchema,
|
listAssetsQuerySchema,
|
||||||
|
exportAssetsQuerySchema,
|
||||||
transferAssetSchema,
|
transferAssetSchema,
|
||||||
amcContractSchema,
|
amcContractSchema,
|
||||||
updateAmcContractSchema,
|
updateAmcContractSchema,
|
||||||
|
|||||||
@ -14,6 +14,13 @@ const list = asyncHandler(async (req, res) => {
|
|||||||
res.json(new ApiResponse(200, result.data, 'GRNs fetched', result.meta));
|
res.json(new ApiResponse(200, result.data, 'GRNs fetched', result.meta));
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const exportCsv = asyncHandler(async (req, res) => {
|
||||||
|
const csv = await service.exportGrns(req.query);
|
||||||
|
res.setHeader('Content-Type', 'text/csv; charset=utf-8');
|
||||||
|
res.setHeader('Content-Disposition', 'attachment; filename="grn-export.csv"');
|
||||||
|
res.send(csv);
|
||||||
|
});
|
||||||
|
|
||||||
const getOne = asyncHandler(async (req, res) => {
|
const getOne = asyncHandler(async (req, res) => {
|
||||||
const data = await service.getGrnById(req.params.id);
|
const data = await service.getGrnById(req.params.id);
|
||||||
res.json(new ApiResponse(200, data, 'GRN fetched'));
|
res.json(new ApiResponse(200, data, 'GRN fetched'));
|
||||||
@ -85,6 +92,7 @@ const removeAttachment = asyncHandler(async (req, res) => {
|
|||||||
module.exports = {
|
module.exports = {
|
||||||
create,
|
create,
|
||||||
list,
|
list,
|
||||||
|
exportCsv,
|
||||||
getOne,
|
getOne,
|
||||||
update,
|
update,
|
||||||
cancel,
|
cancel,
|
||||||
|
|||||||
@ -8,6 +8,7 @@ const {
|
|||||||
createGrnSchema,
|
createGrnSchema,
|
||||||
updateGrnSchema,
|
updateGrnSchema,
|
||||||
listGrnQuerySchema,
|
listGrnQuerySchema,
|
||||||
|
exportGrnQuerySchema,
|
||||||
cancelGrnSchema,
|
cancelGrnSchema,
|
||||||
} = require('./grn.validation');
|
} = require('./grn.validation');
|
||||||
|
|
||||||
@ -15,6 +16,12 @@ const router = express.Router();
|
|||||||
|
|
||||||
router.use(authenticate);
|
router.use(authenticate);
|
||||||
|
|
||||||
|
router.get(
|
||||||
|
'/export',
|
||||||
|
authorize('GRN', 'export'),
|
||||||
|
validate(exportGrnQuerySchema, 'query'),
|
||||||
|
controller.exportCsv
|
||||||
|
);
|
||||||
router.get('/', authorize('GRN', 'view'), validate(listGrnQuerySchema, 'query'), controller.list);
|
router.get('/', authorize('GRN', 'view'), validate(listGrnQuerySchema, 'query'), controller.list);
|
||||||
router.post('/', authorize('GRN', 'create'), validate(createGrnSchema), controller.create);
|
router.post('/', authorize('GRN', 'create'), validate(createGrnSchema), controller.create);
|
||||||
|
|
||||||
|
|||||||
@ -7,6 +7,7 @@ const { generatePdf } = require('../../utils/pdf/pdfGenerator');
|
|||||||
const { generateGrnHtml } = require('../../utils/pdf/templates/grn.template');
|
const { generateGrnHtml } = require('../../utils/pdf/templates/grn.template');
|
||||||
const { formatDate } = require('../../utils/pdf/helpers/formatDate');
|
const { formatDate } = require('../../utils/pdf/helpers/formatDate');
|
||||||
const { getCompanyForDocuments } = require('../settings/settings.service');
|
const { getCompanyForDocuments } = require('../settings/settings.service');
|
||||||
|
const { rowsToCsv } = require('../../utils/csv');
|
||||||
const repository = require('./grn.repository');
|
const repository = require('./grn.repository');
|
||||||
const { assertWarehouse } = require('../../utils/locations');
|
const { assertWarehouse } = require('../../utils/locations');
|
||||||
const { sanitizeAttachment } = require('./grn.attachments.service');
|
const { sanitizeAttachment } = require('./grn.attachments.service');
|
||||||
@ -445,9 +446,7 @@ const createGrn = async (payload, userId, requestId) => {
|
|||||||
return sanitizeGrn(detail);
|
return sanitizeGrn(detail);
|
||||||
};
|
};
|
||||||
|
|
||||||
const listGrns = async (query) => {
|
const buildGrnsWhere = (query) => ({
|
||||||
const { page, limit, skip } = getPagination(query);
|
|
||||||
const where = {
|
|
||||||
deleted_at: null,
|
deleted_at: null,
|
||||||
...(query.status ? { status: query.status } : {}),
|
...(query.status ? { status: query.status } : {}),
|
||||||
...(query.po_id ? { po_id: BigInt(query.po_id) } : {}),
|
...(query.po_id ? { po_id: BigInt(query.po_id) } : {}),
|
||||||
@ -462,7 +461,11 @@ const listGrns = async (query) => {
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
: {}),
|
: {}),
|
||||||
};
|
});
|
||||||
|
|
||||||
|
const listGrns = async (query) => {
|
||||||
|
const { page, limit, skip } = getPagination(query);
|
||||||
|
const where = buildGrnsWhere(query);
|
||||||
|
|
||||||
const [rows, total] = await Promise.all([
|
const [rows, total] = await Promise.all([
|
||||||
prisma.grn.findMany({
|
prisma.grn.findMany({
|
||||||
@ -478,6 +481,35 @@ const listGrns = async (query) => {
|
|||||||
return { data: rows.map(sanitizeGrn), meta: { page, limit, total } };
|
return { data: rows.map(sanitizeGrn), meta: { page, limit, total } };
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const exportGrns = async (query) => {
|
||||||
|
const rows = await prisma.grn.findMany({
|
||||||
|
where: buildGrnsWhere(query),
|
||||||
|
include: grnListInclude,
|
||||||
|
orderBy: { created_at: 'desc' },
|
||||||
|
});
|
||||||
|
|
||||||
|
return rowsToCsv(
|
||||||
|
[
|
||||||
|
{ key: 'grn_number', header: 'GRN Number' },
|
||||||
|
{ key: 'grn_date', header: 'GRN Date', type: 'date' },
|
||||||
|
{ key: 'status', header: 'Status' },
|
||||||
|
{ key: (row) => row.purchase_order?.po_number || '', header: 'PO Number' },
|
||||||
|
{ key: (row) => row.vendor?.vendor_code || '', header: 'Vendor Code' },
|
||||||
|
{ key: (row) => row.vendor?.vendor_name || '', header: 'Vendor Name' },
|
||||||
|
{ key: (row) => row.warehouse?.name || '', header: 'Warehouse' },
|
||||||
|
{ key: 'vendor_invoice_no', header: 'Vendor Invoice No' },
|
||||||
|
{ key: 'vendor_invoice_date', header: 'Vendor Invoice Date', type: 'date' },
|
||||||
|
{ key: 'vendor_invoice_amount', header: 'Vendor Invoice Amount' },
|
||||||
|
{ key: 'vehicle_no', header: 'Vehicle No' },
|
||||||
|
{ key: 'lr_no', header: 'LR No' },
|
||||||
|
{ key: (row) => row.received_by_user?.full_name || '', header: 'Received By' },
|
||||||
|
{ key: (row) => row.created_by_user?.full_name || '', header: 'Created By' },
|
||||||
|
{ key: 'created_at', header: 'Created At', type: 'datetime' },
|
||||||
|
],
|
||||||
|
rows.map(sanitizeGrn)
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
const getGrnById = async (id) => sanitizeGrn(await getGrnOrThrow(id, { includeItems: true }));
|
const getGrnById = async (id) => sanitizeGrn(await getGrnOrThrow(id, { includeItems: true }));
|
||||||
|
|
||||||
const updateGrn = async (id, payload, userId, requestId) => {
|
const updateGrn = async (id, payload, userId, requestId) => {
|
||||||
@ -582,6 +614,7 @@ const getGrnPdf = async (id) => {
|
|||||||
module.exports = {
|
module.exports = {
|
||||||
createGrn,
|
createGrn,
|
||||||
listGrns,
|
listGrns,
|
||||||
|
exportGrns,
|
||||||
getGrnById,
|
getGrnById,
|
||||||
updateGrn,
|
updateGrn,
|
||||||
cancelGrn,
|
cancelGrn,
|
||||||
|
|||||||
@ -62,6 +62,11 @@ const listGrnQuerySchema = Joi.object({
|
|||||||
date_to: Joi.date().iso().optional(),
|
date_to: Joi.date().iso().optional(),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const exportGrnQuerySchema = listGrnQuerySchema.keys({
|
||||||
|
page: Joi.strip(),
|
||||||
|
limit: Joi.strip(),
|
||||||
|
});
|
||||||
|
|
||||||
const cancelGrnSchema = Joi.object({
|
const cancelGrnSchema = Joi.object({
|
||||||
cancellation_reason: Joi.string().trim().min(1).required(),
|
cancellation_reason: Joi.string().trim().min(1).required(),
|
||||||
});
|
});
|
||||||
@ -70,5 +75,6 @@ module.exports = {
|
|||||||
createGrnSchema,
|
createGrnSchema,
|
||||||
updateGrnSchema,
|
updateGrnSchema,
|
||||||
listGrnQuerySchema,
|
listGrnQuerySchema,
|
||||||
|
exportGrnQuerySchema,
|
||||||
cancelGrnSchema,
|
cancelGrnSchema,
|
||||||
};
|
};
|
||||||
|
|||||||
@ -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 exportCsv = asyncHandler(async (req, res) => {
|
||||||
|
const csv = await service.exportPurchaseOrders(req.query);
|
||||||
|
res.setHeader('Content-Type', 'text/csv; charset=utf-8');
|
||||||
|
res.setHeader('Content-Disposition', 'attachment; filename="purchase-orders-export.csv"');
|
||||||
|
res.send(csv);
|
||||||
|
});
|
||||||
|
|
||||||
const getOne = asyncHandler(async (req, res) => {
|
const getOne = asyncHandler(async (req, res) => {
|
||||||
const data = await service.getPurchaseOrderById(req.params.id);
|
const data = await service.getPurchaseOrderById(req.params.id);
|
||||||
res.json(new ApiResponse(200, data, 'Purchase order fetched'));
|
res.json(new ApiResponse(200, data, 'Purchase order fetched'));
|
||||||
@ -110,6 +117,7 @@ const removeAttachment = asyncHandler(async (req, res) => {
|
|||||||
module.exports = {
|
module.exports = {
|
||||||
create,
|
create,
|
||||||
list,
|
list,
|
||||||
|
exportCsv,
|
||||||
getOne,
|
getOne,
|
||||||
update,
|
update,
|
||||||
remove,
|
remove,
|
||||||
|
|||||||
@ -9,6 +9,7 @@ const {
|
|||||||
updatePurchaseOrderSchema,
|
updatePurchaseOrderSchema,
|
||||||
amendPurchaseOrderSchema,
|
amendPurchaseOrderSchema,
|
||||||
listPurchaseOrdersQuerySchema,
|
listPurchaseOrdersQuerySchema,
|
||||||
|
exportPurchaseOrdersQuerySchema,
|
||||||
workflowRemarksSchema,
|
workflowRemarksSchema,
|
||||||
rejectPurchaseOrderSchema,
|
rejectPurchaseOrderSchema,
|
||||||
} = require('./purchase-orders.validation');
|
} = require('./purchase-orders.validation');
|
||||||
@ -17,6 +18,12 @@ const router = express.Router();
|
|||||||
|
|
||||||
router.use(authenticate);
|
router.use(authenticate);
|
||||||
|
|
||||||
|
router.get(
|
||||||
|
'/export',
|
||||||
|
authorize('PURCHASE_ORDER', 'export'),
|
||||||
|
validate(exportPurchaseOrdersQuerySchema, 'query'),
|
||||||
|
controller.exportCsv
|
||||||
|
);
|
||||||
router.get(
|
router.get(
|
||||||
'/',
|
'/',
|
||||||
authorize('PURCHASE_ORDER', 'view'),
|
authorize('PURCHASE_ORDER', 'view'),
|
||||||
|
|||||||
@ -7,6 +7,7 @@ const { generatePdf } = require('../../utils/pdf/pdfGenerator');
|
|||||||
const { generatePoHtml } = require('../../utils/pdf/templates/po.template');
|
const { generatePoHtml } = require('../../utils/pdf/templates/po.template');
|
||||||
const { formatDate } = require('../../utils/pdf/helpers/formatDate');
|
const { formatDate } = require('../../utils/pdf/helpers/formatDate');
|
||||||
const { getCompanyForDocuments } = require('../settings/settings.service');
|
const { getCompanyForDocuments } = require('../settings/settings.service');
|
||||||
|
const { rowsToCsv } = require('../../utils/csv');
|
||||||
const {
|
const {
|
||||||
EDITABLE_STATUSES,
|
EDITABLE_STATUSES,
|
||||||
SUBMITTABLE_STATUSES,
|
SUBMITTABLE_STATUSES,
|
||||||
@ -429,9 +430,7 @@ const createPurchaseOrder = async (payload, userId, requestId) => {
|
|||||||
return sanitizePo(created);
|
return sanitizePo(created);
|
||||||
};
|
};
|
||||||
|
|
||||||
const listPurchaseOrders = async (query) => {
|
const buildPurchaseOrdersWhere = (query) => ({
|
||||||
const { page, limit, skip } = getPagination(query);
|
|
||||||
const where = {
|
|
||||||
deleted_at: null,
|
deleted_at: null,
|
||||||
...(query.status ? { status: query.status } : {}),
|
...(query.status ? { status: query.status } : {}),
|
||||||
...(query.po_type ? { po_type: query.po_type } : {}),
|
...(query.po_type ? { po_type: query.po_type } : {}),
|
||||||
@ -446,7 +445,11 @@ const listPurchaseOrders = async (query) => {
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
: {}),
|
: {}),
|
||||||
};
|
});
|
||||||
|
|
||||||
|
const listPurchaseOrders = async (query) => {
|
||||||
|
const { page, limit, skip } = getPagination(query);
|
||||||
|
const where = buildPurchaseOrdersWhere(query);
|
||||||
|
|
||||||
const [rows, total] = await Promise.all([
|
const [rows, total] = await Promise.all([
|
||||||
prisma.purchase_orders.findMany({
|
prisma.purchase_orders.findMany({
|
||||||
@ -462,6 +465,36 @@ const listPurchaseOrders = async (query) => {
|
|||||||
return { data: rows.map(sanitizePo), meta: { page, limit, total } };
|
return { data: rows.map(sanitizePo), meta: { page, limit, total } };
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const exportPurchaseOrders = async (query) => {
|
||||||
|
const rows = await prisma.purchase_orders.findMany({
|
||||||
|
where: buildPurchaseOrdersWhere(query),
|
||||||
|
include: poListInclude,
|
||||||
|
orderBy: { created_at: 'desc' },
|
||||||
|
});
|
||||||
|
|
||||||
|
return rowsToCsv(
|
||||||
|
[
|
||||||
|
{ key: 'po_number', header: 'PO Number' },
|
||||||
|
{ key: 'po_date', header: 'PO Date', type: 'date' },
|
||||||
|
{ key: 'po_type', header: 'Type' },
|
||||||
|
{ key: 'status', header: 'Status' },
|
||||||
|
{ key: 'revision_no', header: 'Revision' },
|
||||||
|
{ key: (row) => row.vendor?.vendor_code || '', header: 'Vendor Code' },
|
||||||
|
{ key: (row) => row.vendor?.vendor_name || '', header: 'Vendor Name' },
|
||||||
|
{ key: (row) => row.plant?.name || '', header: 'Plant' },
|
||||||
|
{ key: (row) => row.warehouse?.name || '', header: 'Warehouse' },
|
||||||
|
{ key: (row) => row.brand?.name || '', header: 'Brand' },
|
||||||
|
{ key: 'expected_delivery_date', header: 'Expected Delivery', type: 'date' },
|
||||||
|
{ key: 'sub_total', header: 'Sub Total' },
|
||||||
|
{ key: 'tax_total', header: 'Tax Total' },
|
||||||
|
{ key: 'grand_total', header: 'Grand Total' },
|
||||||
|
{ key: (row) => row.created_by_user?.full_name || '', header: 'Created By' },
|
||||||
|
{ key: 'created_at', header: 'Created At', type: 'datetime' },
|
||||||
|
],
|
||||||
|
rows.map(sanitizePo)
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
const getPurchaseOrderById = async (id) =>
|
const getPurchaseOrderById = async (id) =>
|
||||||
sanitizePo(await getPoOrThrow(id, { includeItems: true }));
|
sanitizePo(await getPoOrThrow(id, { includeItems: true }));
|
||||||
|
|
||||||
@ -828,6 +861,7 @@ const getPurchaseOrderPdf = async (id) => {
|
|||||||
module.exports = {
|
module.exports = {
|
||||||
createPurchaseOrder,
|
createPurchaseOrder,
|
||||||
listPurchaseOrders,
|
listPurchaseOrders,
|
||||||
|
exportPurchaseOrders,
|
||||||
getPurchaseOrderById,
|
getPurchaseOrderById,
|
||||||
updatePurchaseOrder,
|
updatePurchaseOrder,
|
||||||
deletePurchaseOrder,
|
deletePurchaseOrder,
|
||||||
|
|||||||
@ -107,6 +107,11 @@ const listPurchaseOrdersQuerySchema = Joi.object({
|
|||||||
date_to: Joi.date().iso().optional(),
|
date_to: Joi.date().iso().optional(),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const exportPurchaseOrdersQuerySchema = listPurchaseOrdersQuerySchema.keys({
|
||||||
|
page: Joi.strip(),
|
||||||
|
limit: Joi.strip(),
|
||||||
|
});
|
||||||
|
|
||||||
const workflowRemarksSchema = Joi.object({
|
const workflowRemarksSchema = Joi.object({
|
||||||
remarks: Joi.string().allow(null, '').optional(),
|
remarks: Joi.string().allow(null, '').optional(),
|
||||||
});
|
});
|
||||||
@ -120,6 +125,7 @@ module.exports = {
|
|||||||
updatePurchaseOrderSchema,
|
updatePurchaseOrderSchema,
|
||||||
amendPurchaseOrderSchema,
|
amendPurchaseOrderSchema,
|
||||||
listPurchaseOrdersQuerySchema,
|
listPurchaseOrdersQuerySchema,
|
||||||
|
exportPurchaseOrdersQuerySchema,
|
||||||
workflowRemarksSchema,
|
workflowRemarksSchema,
|
||||||
rejectPurchaseOrderSchema,
|
rejectPurchaseOrderSchema,
|
||||||
};
|
};
|
||||||
|
|||||||
@ -15,6 +15,30 @@ const buildPublicUrl = (filePath) => {
|
|||||||
return normalized.startsWith('/') ? normalized : `/${normalized}`;
|
return normalized.startsWith('/') ? normalized : `/${normalized}`;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const MIME_BY_EXT = {
|
||||||
|
'.png': 'image/png',
|
||||||
|
'.jpg': 'image/jpeg',
|
||||||
|
'.jpeg': 'image/jpeg',
|
||||||
|
'.webp': 'image/webp',
|
||||||
|
'.gif': 'image/gif',
|
||||||
|
'.svg': 'image/svg+xml',
|
||||||
|
};
|
||||||
|
|
||||||
|
const buildLogoDataUri = (logoPath) => {
|
||||||
|
if (!logoPath) return null;
|
||||||
|
|
||||||
|
const absolute = path.isAbsolute(logoPath)
|
||||||
|
? logoPath
|
||||||
|
: path.resolve(process.cwd(), logoPath);
|
||||||
|
|
||||||
|
if (!fs.existsSync(absolute)) return null;
|
||||||
|
|
||||||
|
const ext = path.extname(absolute).toLowerCase();
|
||||||
|
const mime = MIME_BY_EXT[ext] || 'image/png';
|
||||||
|
const base64 = fs.readFileSync(absolute).toString('base64');
|
||||||
|
return `data:${mime};base64,${base64}`;
|
||||||
|
};
|
||||||
|
|
||||||
const unlinkIfExists = (relativePath) => {
|
const unlinkIfExists = (relativePath) => {
|
||||||
if (!relativePath) return;
|
if (!relativePath) return;
|
||||||
const absolute = path.resolve(process.cwd(), relativePath);
|
const absolute = path.resolve(process.cwd(), relativePath);
|
||||||
@ -218,7 +242,9 @@ const getCompanyForDocuments = async () => {
|
|||||||
phone: row.mobile || '',
|
phone: row.mobile || '',
|
||||||
email: row.email || '',
|
email: row.email || '',
|
||||||
website: row.website || '',
|
website: row.website || '',
|
||||||
|
logo_path: row.logo_path || null,
|
||||||
logo_url: buildPublicUrl(row.logo_path),
|
logo_url: buildPublicUrl(row.logo_path),
|
||||||
|
logo_src: buildLogoDataUri(row.logo_path),
|
||||||
favicon_url: buildPublicUrl(row.favicon_path),
|
favicon_url: buildPublicUrl(row.favicon_path),
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|||||||
8
src/modules/vendors/vendors.controller.js
vendored
8
src/modules/vendors/vendors.controller.js
vendored
@ -12,6 +12,13 @@ const list = asyncHandler(async (req, res) => {
|
|||||||
res.json(new ApiResponse(200, result.data, 'Vendors fetched', result.meta));
|
res.json(new ApiResponse(200, result.data, 'Vendors fetched', result.meta));
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const exportCsv = asyncHandler(async (req, res) => {
|
||||||
|
const csv = await service.exportVendors(req.query);
|
||||||
|
res.setHeader('Content-Type', 'text/csv; charset=utf-8');
|
||||||
|
res.setHeader('Content-Disposition', 'attachment; filename="vendors-export.csv"');
|
||||||
|
res.send(csv);
|
||||||
|
});
|
||||||
|
|
||||||
const listGstTreatments = asyncHandler(async (_req, res) => {
|
const listGstTreatments = asyncHandler(async (_req, res) => {
|
||||||
const data = service.listGstTreatments();
|
const data = service.listGstTreatments();
|
||||||
res.json(new ApiResponse(200, data, 'GST treatment options fetched'));
|
res.json(new ApiResponse(200, data, 'GST treatment options fetched'));
|
||||||
@ -184,6 +191,7 @@ const removeItemMapping = asyncHandler(async (req, res) => {
|
|||||||
module.exports = {
|
module.exports = {
|
||||||
create,
|
create,
|
||||||
list,
|
list,
|
||||||
|
exportCsv,
|
||||||
listGstTreatments,
|
listGstTreatments,
|
||||||
listSourceOfSupplyOptions,
|
listSourceOfSupplyOptions,
|
||||||
getOne,
|
getOne,
|
||||||
|
|||||||
7
src/modules/vendors/vendors.routes.js
vendored
7
src/modules/vendors/vendors.routes.js
vendored
@ -8,6 +8,7 @@ const {
|
|||||||
updateVendorSchema,
|
updateVendorSchema,
|
||||||
vendorStatusSchema,
|
vendorStatusSchema,
|
||||||
listVendorsQuerySchema,
|
listVendorsQuerySchema,
|
||||||
|
exportVendorsQuerySchema,
|
||||||
createAddressSchema,
|
createAddressSchema,
|
||||||
updateAddressSchema,
|
updateAddressSchema,
|
||||||
createContactSchema,
|
createContactSchema,
|
||||||
@ -22,6 +23,12 @@ const router = express.Router();
|
|||||||
|
|
||||||
router.use(authenticate);
|
router.use(authenticate);
|
||||||
|
|
||||||
|
router.get(
|
||||||
|
'/export',
|
||||||
|
authorize('VENDOR', 'export'),
|
||||||
|
validate(exportVendorsQuerySchema, 'query'),
|
||||||
|
controller.exportCsv
|
||||||
|
);
|
||||||
router.get(
|
router.get(
|
||||||
'/',
|
'/',
|
||||||
authorize('VENDOR', 'view'),
|
authorize('VENDOR', 'view'),
|
||||||
|
|||||||
29
src/modules/vendors/vendors.service.js
vendored
29
src/modules/vendors/vendors.service.js
vendored
@ -4,6 +4,7 @@ const auditLog = require('../../utils/auditLog');
|
|||||||
const { getPagination } = require('../../utils/pagination');
|
const { getPagination } = require('../../utils/pagination');
|
||||||
const { nextDocumentNumber } = require('../../utils/generateCode');
|
const { nextDocumentNumber } = require('../../utils/generateCode');
|
||||||
const { encrypt, decrypt, blindIndex } = require('../../utils/encryption');
|
const { encrypt, decrypt, blindIndex } = require('../../utils/encryption');
|
||||||
|
const { rowsToCsv } = require('../../utils/csv');
|
||||||
const {
|
const {
|
||||||
GST_TREATMENTS,
|
GST_TREATMENTS,
|
||||||
SOURCE_OF_SUPPLY_OPTIONS,
|
SOURCE_OF_SUPPLY_OPTIONS,
|
||||||
@ -166,6 +167,33 @@ const listVendors = async (query) => {
|
|||||||
return { data: rows.map((row) => sanitizeVendor(row)), meta: { page, limit, total } };
|
return { data: rows.map((row) => sanitizeVendor(row)), meta: { page, limit, total } };
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const exportVendors = async (query) => {
|
||||||
|
const where = buildVendorsWhere(query);
|
||||||
|
const rows = await prisma.vendors.findMany({
|
||||||
|
where,
|
||||||
|
include: vendorInclude,
|
||||||
|
orderBy: { created_at: 'desc' },
|
||||||
|
});
|
||||||
|
|
||||||
|
return rowsToCsv(
|
||||||
|
[
|
||||||
|
{ key: 'vendor_code', header: 'Vendor Code' },
|
||||||
|
{ key: 'vendor_name', header: 'Vendor Name' },
|
||||||
|
{ key: 'vendor_type', header: 'Vendor Type' },
|
||||||
|
{ key: 'gstin', header: 'GSTIN' },
|
||||||
|
{ key: 'pan', header: 'PAN' },
|
||||||
|
{ key: 'gst_treatment', header: 'GST Treatment' },
|
||||||
|
{ key: 'source_of_supply', header: 'Source of Supply' },
|
||||||
|
{ key: (row) => row.payment_terms?.name || '', header: 'Payment Term' },
|
||||||
|
{ key: 'credit_period_days', header: 'Credit Days' },
|
||||||
|
{ key: 'status', header: 'Status' },
|
||||||
|
{ key: 'is_active', header: 'Active' },
|
||||||
|
{ key: 'created_at', header: 'Created At', type: 'datetime' },
|
||||||
|
],
|
||||||
|
rows.map((row) => sanitizeVendor(row))
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
const listGstTreatments = () => GST_TREATMENTS;
|
const listGstTreatments = () => GST_TREATMENTS;
|
||||||
|
|
||||||
const listSourceOfSupplyOptions = () => SOURCE_OF_SUPPLY_OPTIONS;
|
const listSourceOfSupplyOptions = () => SOURCE_OF_SUPPLY_OPTIONS;
|
||||||
@ -657,6 +685,7 @@ const deleteItemMapping = async (vendorId, mappingId, userId, requestId) => {
|
|||||||
module.exports = {
|
module.exports = {
|
||||||
createVendor,
|
createVendor,
|
||||||
listVendors,
|
listVendors,
|
||||||
|
exportVendors,
|
||||||
listGstTreatments,
|
listGstTreatments,
|
||||||
listSourceOfSupplyOptions,
|
listSourceOfSupplyOptions,
|
||||||
getVendorById,
|
getVendorById,
|
||||||
|
|||||||
6
src/modules/vendors/vendors.validation.js
vendored
6
src/modules/vendors/vendors.validation.js
vendored
@ -70,6 +70,11 @@ const listVendorsQuerySchema = Joi.object({
|
|||||||
is_active: Joi.boolean().optional(),
|
is_active: Joi.boolean().optional(),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const exportVendorsQuerySchema = listVendorsQuerySchema.keys({
|
||||||
|
page: Joi.strip(),
|
||||||
|
limit: Joi.strip(),
|
||||||
|
});
|
||||||
|
|
||||||
const createAddressSchema = Joi.object({
|
const createAddressSchema = Joi.object({
|
||||||
address_type: Joi.string()
|
address_type: Joi.string()
|
||||||
.valid(...addressTypes)
|
.valid(...addressTypes)
|
||||||
@ -145,6 +150,7 @@ module.exports = {
|
|||||||
updateVendorSchema,
|
updateVendorSchema,
|
||||||
vendorStatusSchema,
|
vendorStatusSchema,
|
||||||
listVendorsQuerySchema,
|
listVendorsQuerySchema,
|
||||||
|
exportVendorsQuerySchema,
|
||||||
createAddressSchema,
|
createAddressSchema,
|
||||||
updateAddressSchema,
|
updateAddressSchema,
|
||||||
createContactSchema,
|
createContactSchema,
|
||||||
|
|||||||
@ -207,6 +207,19 @@ const generateGrnHtml = (inputData = getDummyGrnData()) => {
|
|||||||
align-items: flex-start;
|
align-items: flex-start;
|
||||||
gap: 24px;
|
gap: 24px;
|
||||||
}
|
}
|
||||||
|
.brand {
|
||||||
|
display: flex;
|
||||||
|
gap: 12px;
|
||||||
|
align-items: flex-start;
|
||||||
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
.company-logo {
|
||||||
|
width: 56px;
|
||||||
|
height: 56px;
|
||||||
|
object-fit: contain;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
.company-name {
|
.company-name {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
font-size: 20px;
|
font-size: 20px;
|
||||||
@ -383,6 +396,12 @@ const generateGrnHtml = (inputData = getDummyGrnData()) => {
|
|||||||
<body>
|
<body>
|
||||||
<div class="document">
|
<div class="document">
|
||||||
<div class="header">
|
<div class="header">
|
||||||
|
<div class="brand">
|
||||||
|
${
|
||||||
|
company.logo_src
|
||||||
|
? `<img class="company-logo" src="${company.logo_src}" alt="Company logo" />`
|
||||||
|
: ''
|
||||||
|
}
|
||||||
<div>
|
<div>
|
||||||
<h1 class="company-name">${escapeHtml(company.name || 'Company')}</h1>
|
<h1 class="company-name">${escapeHtml(company.name || 'Company')}</h1>
|
||||||
<div class="company-meta">
|
<div class="company-meta">
|
||||||
@ -390,6 +409,7 @@ const generateGrnHtml = (inputData = getDummyGrnData()) => {
|
|||||||
${escapeHtml(companyContactLine(company))}
|
${escapeHtml(companyContactLine(company))}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
<div class="title-block">
|
<div class="title-block">
|
||||||
<h2 class="doc-title">Goods receipt note</h2>
|
<h2 class="doc-title">Goods receipt note</h2>
|
||||||
<div class="doc-number">${escapeHtml(grn.grn_number || '-')}</div>
|
<div class="doc-number">${escapeHtml(grn.grn_number || '-')}</div>
|
||||||
|
|||||||
@ -205,6 +205,19 @@ const generatePoHtml = (inputData = getDummyPoData()) => {
|
|||||||
align-items: flex-start;
|
align-items: flex-start;
|
||||||
gap: 24px;
|
gap: 24px;
|
||||||
}
|
}
|
||||||
|
.brand {
|
||||||
|
display: flex;
|
||||||
|
gap: 12px;
|
||||||
|
align-items: flex-start;
|
||||||
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
.company-logo {
|
||||||
|
width: 56px;
|
||||||
|
height: 56px;
|
||||||
|
object-fit: contain;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
.company-name {
|
.company-name {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
font-size: 20px;
|
font-size: 20px;
|
||||||
@ -391,6 +404,12 @@ const generatePoHtml = (inputData = getDummyPoData()) => {
|
|||||||
<body>
|
<body>
|
||||||
<div class="document">
|
<div class="document">
|
||||||
<div class="header">
|
<div class="header">
|
||||||
|
<div class="brand">
|
||||||
|
${
|
||||||
|
company.logo_src
|
||||||
|
? `<img class="company-logo" src="${company.logo_src}" alt="Company logo" />`
|
||||||
|
: ''
|
||||||
|
}
|
||||||
<div>
|
<div>
|
||||||
<h1 class="company-name">${escapeHtml(company.name || 'Company')}</h1>
|
<h1 class="company-name">${escapeHtml(company.name || 'Company')}</h1>
|
||||||
<div class="company-meta">
|
<div class="company-meta">
|
||||||
@ -398,6 +417,7 @@ const generatePoHtml = (inputData = getDummyPoData()) => {
|
|||||||
${escapeHtml(companyContactLine(company))}
|
${escapeHtml(companyContactLine(company))}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
<div class="title-block">
|
<div class="title-block">
|
||||||
<h2 class="doc-title">Purchase order</h2>
|
<h2 class="doc-title">Purchase order</h2>
|
||||||
<div class="doc-number">${escapeHtml(po.po_number || '-')}</div>
|
<div class="doc-number">${escapeHtml(po.po_number || '-')}</div>
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user