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));
|
||||
});
|
||||
|
||||
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 data = await service.getAssetById(req.params.id);
|
||||
res.json(new ApiResponse(200, data, 'Asset fetched'));
|
||||
@ -276,6 +283,7 @@ const removeAttachment = asyncHandler(async (req, res) => {
|
||||
module.exports = {
|
||||
create,
|
||||
list,
|
||||
exportCsv,
|
||||
getOne,
|
||||
update,
|
||||
remove,
|
||||
|
||||
@ -8,6 +8,7 @@ const {
|
||||
createAssetSchema,
|
||||
updateAssetSchema,
|
||||
listAssetsQuerySchema,
|
||||
exportAssetsQuerySchema,
|
||||
transferAssetSchema,
|
||||
amcContractSchema,
|
||||
updateAmcContractSchema,
|
||||
@ -28,6 +29,12 @@ const router = express.Router();
|
||||
|
||||
router.use(authenticate);
|
||||
|
||||
router.get(
|
||||
'/export',
|
||||
authorize('ASSET', 'export'),
|
||||
validate(exportAssetsQuerySchema, 'query'),
|
||||
controller.exportCsv
|
||||
);
|
||||
router.get(
|
||||
'/',
|
||||
authorize('ASSET', 'view'),
|
||||
|
||||
@ -3,6 +3,7 @@ const ApiError = require('../../utils/ApiError');
|
||||
const auditLog = require('../../utils/auditLog');
|
||||
const { getPagination } = require('../../utils/pagination');
|
||||
const { nextDocumentNumber } = require('../../utils/generateCode');
|
||||
const { rowsToCsv } = require('../../utils/csv');
|
||||
const { DISPOSAL_STATUSES, getAssetDropdownOptions } = require('./assets.constants');
|
||||
const { assertPlant, assertWarehouse } = require('../../utils/locations');
|
||||
const repository = require('./assets.repository');
|
||||
@ -307,30 +308,32 @@ const createAsset = async (payload, userId, requestId) => {
|
||||
return sanitizeAsset(created);
|
||||
};
|
||||
|
||||
const buildAssetsWhere = (query) => ({
|
||||
deleted_at: null,
|
||||
...(query.status ? { status: query.status } : {}),
|
||||
...(query.condition ? { condition: query.condition } : {}),
|
||||
...(query.item_category_id ? { item_category_id: BigInt(query.item_category_id) } : {}),
|
||||
...(query.item_subcategory_id
|
||||
? { item_subcategory_id: BigInt(query.item_subcategory_id) }
|
||||
: {}),
|
||||
...(query.plant_id ? { plant_id: BigInt(query.plant_id) } : {}),
|
||||
...(query.department_id ? { department_id: BigInt(query.department_id) } : {}),
|
||||
...(query.is_active !== undefined ? { is_active: query.is_active } : {}),
|
||||
...(query.search
|
||||
? {
|
||||
OR: [
|
||||
{ asset_code: { contains: query.search, mode: 'insensitive' } },
|
||||
{ asset_name: { contains: query.search, mode: 'insensitive' } },
|
||||
{ serial_number: { contains: query.search, mode: 'insensitive' } },
|
||||
{ qr_code_value: { contains: query.search, mode: 'insensitive' } },
|
||||
],
|
||||
}
|
||||
: {}),
|
||||
});
|
||||
|
||||
const listAssets = async (query) => {
|
||||
const { page, limit, skip } = getPagination(query);
|
||||
const where = {
|
||||
deleted_at: null,
|
||||
...(query.status ? { status: query.status } : {}),
|
||||
...(query.condition ? { condition: query.condition } : {}),
|
||||
...(query.item_category_id ? { item_category_id: BigInt(query.item_category_id) } : {}),
|
||||
...(query.item_subcategory_id
|
||||
? { item_subcategory_id: BigInt(query.item_subcategory_id) }
|
||||
: {}),
|
||||
...(query.plant_id ? { plant_id: BigInt(query.plant_id) } : {}),
|
||||
...(query.department_id ? { department_id: BigInt(query.department_id) } : {}),
|
||||
...(query.is_active !== undefined ? { is_active: query.is_active } : {}),
|
||||
...(query.search
|
||||
? {
|
||||
OR: [
|
||||
{ asset_code: { contains: query.search, mode: 'insensitive' } },
|
||||
{ asset_name: { contains: query.search, mode: 'insensitive' } },
|
||||
{ serial_number: { contains: query.search, mode: 'insensitive' } },
|
||||
{ qr_code_value: { contains: query.search, mode: 'insensitive' } },
|
||||
],
|
||||
}
|
||||
: {}),
|
||||
};
|
||||
const where = buildAssetsWhere(query);
|
||||
|
||||
const [rows, total] = await Promise.all([
|
||||
prisma.assets.findMany({
|
||||
@ -346,6 +349,39 @@ const listAssets = async (query) => {
|
||||
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 updateAsset = async (id, payload, userId, requestId) => {
|
||||
@ -566,6 +602,7 @@ const previewDepreciation = (payload) => {
|
||||
module.exports = {
|
||||
createAsset,
|
||||
listAssets,
|
||||
exportAssets,
|
||||
getAssetById,
|
||||
updateAsset,
|
||||
deleteAsset,
|
||||
|
||||
@ -108,6 +108,11 @@ const listAssetsQuerySchema = Joi.object({
|
||||
is_active: Joi.boolean().optional(),
|
||||
});
|
||||
|
||||
const exportAssetsQuerySchema = listAssetsQuerySchema.keys({
|
||||
page: Joi.strip(),
|
||||
limit: Joi.strip(),
|
||||
});
|
||||
|
||||
const transferAssetSchema = Joi.object({
|
||||
transfer_date: Joi.date().iso().required(),
|
||||
to_plant_id: Joi.number().integer().positive().allow(null).optional(),
|
||||
@ -305,6 +310,7 @@ module.exports = {
|
||||
createAssetSchema,
|
||||
updateAssetSchema,
|
||||
listAssetsQuerySchema,
|
||||
exportAssetsQuerySchema,
|
||||
transferAssetSchema,
|
||||
amcContractSchema,
|
||||
updateAmcContractSchema,
|
||||
|
||||
@ -14,6 +14,13 @@ const list = asyncHandler(async (req, res) => {
|
||||
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 data = await service.getGrnById(req.params.id);
|
||||
res.json(new ApiResponse(200, data, 'GRN fetched'));
|
||||
@ -85,6 +92,7 @@ const removeAttachment = asyncHandler(async (req, res) => {
|
||||
module.exports = {
|
||||
create,
|
||||
list,
|
||||
exportCsv,
|
||||
getOne,
|
||||
update,
|
||||
cancel,
|
||||
|
||||
@ -8,6 +8,7 @@ const {
|
||||
createGrnSchema,
|
||||
updateGrnSchema,
|
||||
listGrnQuerySchema,
|
||||
exportGrnQuerySchema,
|
||||
cancelGrnSchema,
|
||||
} = require('./grn.validation');
|
||||
|
||||
@ -15,6 +16,12 @@ const router = express.Router();
|
||||
|
||||
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.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 { formatDate } = require('../../utils/pdf/helpers/formatDate');
|
||||
const { getCompanyForDocuments } = require('../settings/settings.service');
|
||||
const { rowsToCsv } = require('../../utils/csv');
|
||||
const repository = require('./grn.repository');
|
||||
const { assertWarehouse } = require('../../utils/locations');
|
||||
const { sanitizeAttachment } = require('./grn.attachments.service');
|
||||
@ -445,24 +446,26 @@ const createGrn = async (payload, userId, requestId) => {
|
||||
return sanitizeGrn(detail);
|
||||
};
|
||||
|
||||
const buildGrnsWhere = (query) => ({
|
||||
deleted_at: null,
|
||||
...(query.status ? { status: query.status } : {}),
|
||||
...(query.po_id ? { po_id: BigInt(query.po_id) } : {}),
|
||||
...(query.vendor_id ? { vendor_id: BigInt(query.vendor_id) } : {}),
|
||||
...(query.warehouse_id ? { warehouse_id: BigInt(query.warehouse_id) } : {}),
|
||||
...(query.search ? { grn_number: { contains: query.search, mode: 'insensitive' } } : {}),
|
||||
...(query.date_from || query.date_to
|
||||
? {
|
||||
grn_date: {
|
||||
...(query.date_from ? { gte: toDateOnly(query.date_from) } : {}),
|
||||
...(query.date_to ? { lte: toDateOnly(query.date_to) } : {}),
|
||||
},
|
||||
}
|
||||
: {}),
|
||||
});
|
||||
|
||||
const listGrns = async (query) => {
|
||||
const { page, limit, skip } = getPagination(query);
|
||||
const where = {
|
||||
deleted_at: null,
|
||||
...(query.status ? { status: query.status } : {}),
|
||||
...(query.po_id ? { po_id: BigInt(query.po_id) } : {}),
|
||||
...(query.vendor_id ? { vendor_id: BigInt(query.vendor_id) } : {}),
|
||||
...(query.warehouse_id ? { warehouse_id: BigInt(query.warehouse_id) } : {}),
|
||||
...(query.search ? { grn_number: { contains: query.search, mode: 'insensitive' } } : {}),
|
||||
...(query.date_from || query.date_to
|
||||
? {
|
||||
grn_date: {
|
||||
...(query.date_from ? { gte: toDateOnly(query.date_from) } : {}),
|
||||
...(query.date_to ? { lte: toDateOnly(query.date_to) } : {}),
|
||||
},
|
||||
}
|
||||
: {}),
|
||||
};
|
||||
const where = buildGrnsWhere(query);
|
||||
|
||||
const [rows, total] = await Promise.all([
|
||||
prisma.grn.findMany({
|
||||
@ -478,6 +481,35 @@ const listGrns = async (query) => {
|
||||
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 updateGrn = async (id, payload, userId, requestId) => {
|
||||
@ -582,6 +614,7 @@ const getGrnPdf = async (id) => {
|
||||
module.exports = {
|
||||
createGrn,
|
||||
listGrns,
|
||||
exportGrns,
|
||||
getGrnById,
|
||||
updateGrn,
|
||||
cancelGrn,
|
||||
|
||||
@ -62,6 +62,11 @@ const listGrnQuerySchema = Joi.object({
|
||||
date_to: Joi.date().iso().optional(),
|
||||
});
|
||||
|
||||
const exportGrnQuerySchema = listGrnQuerySchema.keys({
|
||||
page: Joi.strip(),
|
||||
limit: Joi.strip(),
|
||||
});
|
||||
|
||||
const cancelGrnSchema = Joi.object({
|
||||
cancellation_reason: Joi.string().trim().min(1).required(),
|
||||
});
|
||||
@ -70,5 +75,6 @@ module.exports = {
|
||||
createGrnSchema,
|
||||
updateGrnSchema,
|
||||
listGrnQuerySchema,
|
||||
exportGrnQuerySchema,
|
||||
cancelGrnSchema,
|
||||
};
|
||||
|
||||
@ -14,6 +14,13 @@ const list = asyncHandler(async (req, res) => {
|
||||
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 data = await service.getPurchaseOrderById(req.params.id);
|
||||
res.json(new ApiResponse(200, data, 'Purchase order fetched'));
|
||||
@ -110,6 +117,7 @@ const removeAttachment = asyncHandler(async (req, res) => {
|
||||
module.exports = {
|
||||
create,
|
||||
list,
|
||||
exportCsv,
|
||||
getOne,
|
||||
update,
|
||||
remove,
|
||||
|
||||
@ -9,6 +9,7 @@ const {
|
||||
updatePurchaseOrderSchema,
|
||||
amendPurchaseOrderSchema,
|
||||
listPurchaseOrdersQuerySchema,
|
||||
exportPurchaseOrdersQuerySchema,
|
||||
workflowRemarksSchema,
|
||||
rejectPurchaseOrderSchema,
|
||||
} = require('./purchase-orders.validation');
|
||||
@ -17,6 +18,12 @@ const router = express.Router();
|
||||
|
||||
router.use(authenticate);
|
||||
|
||||
router.get(
|
||||
'/export',
|
||||
authorize('PURCHASE_ORDER', 'export'),
|
||||
validate(exportPurchaseOrdersQuerySchema, 'query'),
|
||||
controller.exportCsv
|
||||
);
|
||||
router.get(
|
||||
'/',
|
||||
authorize('PURCHASE_ORDER', 'view'),
|
||||
|
||||
@ -7,6 +7,7 @@ const { generatePdf } = require('../../utils/pdf/pdfGenerator');
|
||||
const { generatePoHtml } = require('../../utils/pdf/templates/po.template');
|
||||
const { formatDate } = require('../../utils/pdf/helpers/formatDate');
|
||||
const { getCompanyForDocuments } = require('../settings/settings.service');
|
||||
const { rowsToCsv } = require('../../utils/csv');
|
||||
const {
|
||||
EDITABLE_STATUSES,
|
||||
SUBMITTABLE_STATUSES,
|
||||
@ -429,24 +430,26 @@ const createPurchaseOrder = async (payload, userId, requestId) => {
|
||||
return sanitizePo(created);
|
||||
};
|
||||
|
||||
const buildPurchaseOrdersWhere = (query) => ({
|
||||
deleted_at: null,
|
||||
...(query.status ? { status: query.status } : {}),
|
||||
...(query.po_type ? { po_type: query.po_type } : {}),
|
||||
...(query.vendor_id ? { vendor_id: BigInt(query.vendor_id) } : {}),
|
||||
...(query.plant_id ? { plant_id: BigInt(query.plant_id) } : {}),
|
||||
...(query.search ? { po_number: { contains: query.search, mode: 'insensitive' } } : {}),
|
||||
...(query.date_from || query.date_to
|
||||
? {
|
||||
po_date: {
|
||||
...(query.date_from ? { gte: toDateOnly(query.date_from) } : {}),
|
||||
...(query.date_to ? { lte: toDateOnly(query.date_to) } : {}),
|
||||
},
|
||||
}
|
||||
: {}),
|
||||
});
|
||||
|
||||
const listPurchaseOrders = async (query) => {
|
||||
const { page, limit, skip } = getPagination(query);
|
||||
const where = {
|
||||
deleted_at: null,
|
||||
...(query.status ? { status: query.status } : {}),
|
||||
...(query.po_type ? { po_type: query.po_type } : {}),
|
||||
...(query.vendor_id ? { vendor_id: BigInt(query.vendor_id) } : {}),
|
||||
...(query.plant_id ? { plant_id: BigInt(query.plant_id) } : {}),
|
||||
...(query.search ? { po_number: { contains: query.search, mode: 'insensitive' } } : {}),
|
||||
...(query.date_from || query.date_to
|
||||
? {
|
||||
po_date: {
|
||||
...(query.date_from ? { gte: toDateOnly(query.date_from) } : {}),
|
||||
...(query.date_to ? { lte: toDateOnly(query.date_to) } : {}),
|
||||
},
|
||||
}
|
||||
: {}),
|
||||
};
|
||||
const where = buildPurchaseOrdersWhere(query);
|
||||
|
||||
const [rows, total] = await Promise.all([
|
||||
prisma.purchase_orders.findMany({
|
||||
@ -462,6 +465,36 @@ const listPurchaseOrders = async (query) => {
|
||||
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) =>
|
||||
sanitizePo(await getPoOrThrow(id, { includeItems: true }));
|
||||
|
||||
@ -828,6 +861,7 @@ const getPurchaseOrderPdf = async (id) => {
|
||||
module.exports = {
|
||||
createPurchaseOrder,
|
||||
listPurchaseOrders,
|
||||
exportPurchaseOrders,
|
||||
getPurchaseOrderById,
|
||||
updatePurchaseOrder,
|
||||
deletePurchaseOrder,
|
||||
|
||||
@ -107,6 +107,11 @@ const listPurchaseOrdersQuerySchema = Joi.object({
|
||||
date_to: Joi.date().iso().optional(),
|
||||
});
|
||||
|
||||
const exportPurchaseOrdersQuerySchema = listPurchaseOrdersQuerySchema.keys({
|
||||
page: Joi.strip(),
|
||||
limit: Joi.strip(),
|
||||
});
|
||||
|
||||
const workflowRemarksSchema = Joi.object({
|
||||
remarks: Joi.string().allow(null, '').optional(),
|
||||
});
|
||||
@ -120,6 +125,7 @@ module.exports = {
|
||||
updatePurchaseOrderSchema,
|
||||
amendPurchaseOrderSchema,
|
||||
listPurchaseOrdersQuerySchema,
|
||||
exportPurchaseOrdersQuerySchema,
|
||||
workflowRemarksSchema,
|
||||
rejectPurchaseOrderSchema,
|
||||
};
|
||||
|
||||
@ -15,6 +15,30 @@ const buildPublicUrl = (filePath) => {
|
||||
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) => {
|
||||
if (!relativePath) return;
|
||||
const absolute = path.resolve(process.cwd(), relativePath);
|
||||
@ -218,7 +242,9 @@ const getCompanyForDocuments = async () => {
|
||||
phone: row.mobile || '',
|
||||
email: row.email || '',
|
||||
website: row.website || '',
|
||||
logo_path: row.logo_path || null,
|
||||
logo_url: buildPublicUrl(row.logo_path),
|
||||
logo_src: buildLogoDataUri(row.logo_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));
|
||||
});
|
||||
|
||||
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 data = service.listGstTreatments();
|
||||
res.json(new ApiResponse(200, data, 'GST treatment options fetched'));
|
||||
@ -184,6 +191,7 @@ const removeItemMapping = asyncHandler(async (req, res) => {
|
||||
module.exports = {
|
||||
create,
|
||||
list,
|
||||
exportCsv,
|
||||
listGstTreatments,
|
||||
listSourceOfSupplyOptions,
|
||||
getOne,
|
||||
|
||||
7
src/modules/vendors/vendors.routes.js
vendored
7
src/modules/vendors/vendors.routes.js
vendored
@ -8,6 +8,7 @@ const {
|
||||
updateVendorSchema,
|
||||
vendorStatusSchema,
|
||||
listVendorsQuerySchema,
|
||||
exportVendorsQuerySchema,
|
||||
createAddressSchema,
|
||||
updateAddressSchema,
|
||||
createContactSchema,
|
||||
@ -22,6 +23,12 @@ const router = express.Router();
|
||||
|
||||
router.use(authenticate);
|
||||
|
||||
router.get(
|
||||
'/export',
|
||||
authorize('VENDOR', 'export'),
|
||||
validate(exportVendorsQuerySchema, 'query'),
|
||||
controller.exportCsv
|
||||
);
|
||||
router.get(
|
||||
'/',
|
||||
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 { nextDocumentNumber } = require('../../utils/generateCode');
|
||||
const { encrypt, decrypt, blindIndex } = require('../../utils/encryption');
|
||||
const { rowsToCsv } = require('../../utils/csv');
|
||||
const {
|
||||
GST_TREATMENTS,
|
||||
SOURCE_OF_SUPPLY_OPTIONS,
|
||||
@ -166,6 +167,33 @@ const listVendors = async (query) => {
|
||||
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 listSourceOfSupplyOptions = () => SOURCE_OF_SUPPLY_OPTIONS;
|
||||
@ -657,6 +685,7 @@ const deleteItemMapping = async (vendorId, mappingId, userId, requestId) => {
|
||||
module.exports = {
|
||||
createVendor,
|
||||
listVendors,
|
||||
exportVendors,
|
||||
listGstTreatments,
|
||||
listSourceOfSupplyOptions,
|
||||
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(),
|
||||
});
|
||||
|
||||
const exportVendorsQuerySchema = listVendorsQuerySchema.keys({
|
||||
page: Joi.strip(),
|
||||
limit: Joi.strip(),
|
||||
});
|
||||
|
||||
const createAddressSchema = Joi.object({
|
||||
address_type: Joi.string()
|
||||
.valid(...addressTypes)
|
||||
@ -145,6 +150,7 @@ module.exports = {
|
||||
updateVendorSchema,
|
||||
vendorStatusSchema,
|
||||
listVendorsQuerySchema,
|
||||
exportVendorsQuerySchema,
|
||||
createAddressSchema,
|
||||
updateAddressSchema,
|
||||
createContactSchema,
|
||||
|
||||
@ -207,6 +207,19 @@ const generateGrnHtml = (inputData = getDummyGrnData()) => {
|
||||
align-items: flex-start;
|
||||
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 {
|
||||
margin: 0;
|
||||
font-size: 20px;
|
||||
@ -383,11 +396,18 @@ const generateGrnHtml = (inputData = getDummyGrnData()) => {
|
||||
<body>
|
||||
<div class="document">
|
||||
<div class="header">
|
||||
<div>
|
||||
<h1 class="company-name">${escapeHtml(company.name || 'Company')}</h1>
|
||||
<div class="company-meta">
|
||||
${escapeHtml(companyAddressLine(company))}<br />
|
||||
${escapeHtml(companyContactLine(company))}
|
||||
<div class="brand">
|
||||
${
|
||||
company.logo_src
|
||||
? `<img class="company-logo" src="${company.logo_src}" alt="Company logo" />`
|
||||
: ''
|
||||
}
|
||||
<div>
|
||||
<h1 class="company-name">${escapeHtml(company.name || 'Company')}</h1>
|
||||
<div class="company-meta">
|
||||
${escapeHtml(companyAddressLine(company))}<br />
|
||||
${escapeHtml(companyContactLine(company))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="title-block">
|
||||
|
||||
@ -205,6 +205,19 @@ const generatePoHtml = (inputData = getDummyPoData()) => {
|
||||
align-items: flex-start;
|
||||
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 {
|
||||
margin: 0;
|
||||
font-size: 20px;
|
||||
@ -391,11 +404,18 @@ const generatePoHtml = (inputData = getDummyPoData()) => {
|
||||
<body>
|
||||
<div class="document">
|
||||
<div class="header">
|
||||
<div>
|
||||
<h1 class="company-name">${escapeHtml(company.name || 'Company')}</h1>
|
||||
<div class="company-meta">
|
||||
${escapeHtml(companyAddressLine(company))}<br />
|
||||
${escapeHtml(companyContactLine(company))}
|
||||
<div class="brand">
|
||||
${
|
||||
company.logo_src
|
||||
? `<img class="company-logo" src="${company.logo_src}" alt="Company logo" />`
|
||||
: ''
|
||||
}
|
||||
<div>
|
||||
<h1 class="company-name">${escapeHtml(company.name || 'Company')}</h1>
|
||||
<div class="company-meta">
|
||||
${escapeHtml(companyAddressLine(company))}<br />
|
||||
${escapeHtml(companyContactLine(company))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="title-block">
|
||||
|
||||
Loading…
Reference in New Issue
Block a user