diff --git a/src/modules/grn/grn.service.js b/src/modules/grn/grn.service.js index b247d8b..0556a5e 100644 --- a/src/modules/grn/grn.service.js +++ b/src/modules/grn/grn.service.js @@ -73,6 +73,7 @@ const grnPdfInclude = { code: true, name: true, type: true, + gstin: true, address: true, city: true, state: true, @@ -220,10 +221,21 @@ const buildGrnPdfPayload = (grn, company) => { }, warehouse: { name: grn.location?.name || '-', + gstin: grn.location?.gstin || '', address: grn.location?.address || '', city: grn.location?.city || '', state: grn.location?.state || '', pincode: grn.location?.pincode || '', + type: grn.location?.type || null, + }, + location: { + name: grn.location?.name || '-', + gstin: grn.location?.gstin || '', + address: grn.location?.address || '', + city: grn.location?.city || '', + state: grn.location?.state || '', + pincode: grn.location?.pincode || '', + type: grn.location?.type || null, }, items, generated_at: `${formatDate(new Date(), { style: 'datetime' })} IST`, diff --git a/src/modules/purchase-orders/purchase-orders.calculations.js b/src/modules/purchase-orders/purchase-orders.calculations.js index 0c6fbb5..5bfcd1a 100644 --- a/src/modules/purchase-orders/purchase-orders.calculations.js +++ b/src/modules/purchase-orders/purchase-orders.calculations.js @@ -31,25 +31,26 @@ const computeHeaderTotals = ( other = 0, { tdsApplicable = false, tdsSectionPct = 0, adjustment = 0 } = {} ) => { + // Sub total = sum of line taxable amounts (after line-level discounts). const subTotal = round4(lines.reduce((sum, line) => sum + toNum(line.taxable_amount), 0)); const taxTotal = round4(lines.reduce((sum, line) => sum + toNum(line.tax_amount), 0)); const headerDiscountAmount = round4(headerDiscount); const freightCharges = round4(freight); const otherCharges = round4(other); + + // Taxable amount = Sub Total + Freight + Other − Discount (header-level). + const taxableAmount = round4(subTotal + freightCharges + otherCharges - headerDiscountAmount); + + // TDS is calculated on sub total (line taxable), not on header taxable. const tdsAmount = tdsApplicable ? round4((subTotal * toNum(tdsSectionPct)) / 100) : 0; const adjustmentAmount = round4(adjustment); - const grandTotal = round4( - subTotal + - taxTotal + - freightCharges + - otherCharges - - headerDiscountAmount - - tdsAmount + - adjustmentAmount - ); + + // Grand total = Taxable Amount + Tax Total − TDS + Adjustment. + const grandTotal = round4(taxableAmount + taxTotal - tdsAmount + adjustmentAmount); return { sub_total: subTotal, + taxable_amount: taxableAmount, tax_total: taxTotal, discount_amount: headerDiscountAmount, freight_charges: freightCharges, diff --git a/src/modules/purchase-orders/purchase-orders.service.js b/src/modules/purchase-orders/purchase-orders.service.js index 2e3a45c..506e598 100644 --- a/src/modules/purchase-orders/purchase-orders.service.js +++ b/src/modules/purchase-orders/purchase-orders.service.js @@ -21,6 +21,7 @@ const { computeHeaderTotals, splitGst, toNum, + round4, } = require('./purchase-orders.calculations'); const repository = require('./purchase-orders.repository'); const { assertAnyLocation } = require('../../utils/locations'); @@ -143,6 +144,13 @@ const sanitizePo = (po) => { return { ...rest, + // Header taxable = sub_total + freight + other − discount (not a DB column). + taxable_amount: round4( + toNum(rest.sub_total) + + toNum(rest.freight_charges) + + toNum(rest.other_charges) - + toNum(rest.discount_amount) + ), vendor: vendors || null, billing: billing_location || null, shipping: shipping_location || null, @@ -312,8 +320,10 @@ const buildHeaderData = async (payload, builtItems, userId, { applyDefaultTerms adjustment: payload.adjustment, } ); + // taxable_amount is derived for API/PDF; not a persisted PO column. + const { taxable_amount: _taxableAmount, ...persistedTotals } = totals; - const gstSplit = splitGst(totals.tax_total, isInterStateSupply(billing, vendor)); + const gstSplit = splitGst(persistedTotals.tax_total, isInterStateSupply(billing, vendor)); let termsAndConditions = payload.terms_and_conditions !== undefined && payload.terms_and_conditions !== null @@ -339,7 +349,7 @@ const buildHeaderData = async (payload, builtItems, userId, { applyDefaultTerms remarks: payload.remarks || null, tds_applicable: tdsApplicable, tds_section_pct: tdsApplicable ? tdsSectionPct : null, - ...totals, + ...persistedTotals, ...gstSplit, updated_by: userId ? BigInt(userId) : null, }; @@ -436,18 +446,21 @@ const buildPoPdfPayload = (po, company) => { rate, discount_pct: toNum(line.discount_pct), gst_rate_pct: toNum(line.gst_rate?.rate_pct), - amount: orderedQty * rate, + amount: toNum(line.taxable_amount != null ? line.taxable_amount : orderedQty * rate), }; }), totals: { sub_total: toNum(po.sub_total), - tax_total: toNum(po.tax_total), - cgst: toNum(po.cgst), - sgst: toNum(po.sgst), - igst: toNum(po.igst), freight_charges: toNum(po.freight_charges), other_charges: toNum(po.other_charges), discount_amount: toNum(po.discount_amount), + taxable_amount: toNum(po.taxable_amount), + cgst: toNum(po.cgst), + sgst: toNum(po.sgst), + igst: toNum(po.igst), + tax_total: toNum(po.tax_total), + tds_amount: toNum(po.tds_amount), + adjustment: toNum(po.adjustment), grand_total: toNum(po.grand_total), }, generated_at: `${formatDate(new Date(), { style: 'datetime' })} IST`, diff --git a/src/utils/pdf/templates/grn.template.js b/src/utils/pdf/templates/grn.template.js index 8b03ad2..be3e751 100644 --- a/src/utils/pdf/templates/grn.template.js +++ b/src/utils/pdf/templates/grn.template.js @@ -1,21 +1,28 @@ const { formatDate } = require('../helpers/formatDate'); +const BRAND = '#1e3a5f'; +const BRAND_SOFT = '#eef3f8'; +const STATUS_BG = '#fde8d0'; +const STATUS_FG = '#9a5b16'; +const MUTED = '#6b7280'; +const LINE = '#e5e7eb'; + const getDummyGrnData = () => ({ company: { - name: 'Bharat Industries Pvt. Ltd.', - address: 'Plot 14, Guindy Industrial Estate', + name: 'BCPL Chennai', + address: 'Chennai', city: 'Chennai', - state: 'Tamil Nadu', - pincode: '600032', - gstin: '33AABCB1234D1Z5', - phone: '+91 44 4000 1200', - email: 'procurement@bharatindustries.in', + state: 'TN', + pincode: '665544', + gstin: '29ABCDE1234F1Z5', + phone: '6369084112', + email: 'venba2026@gmail.com', }, grn: { grn_number: 'GRN/2026-27/00012', grn_date: '2026-07-14', status: 'Posted', - po_number: 'PO/2026-27/00007', + po_number: 'PO/2026-27/00038', vendor_invoice_no: 'TSL/INV/2026/441', vendor_invoice_date: '2026-07-13', vehicle_no: 'TN-09-AB-4521', @@ -26,31 +33,33 @@ const getDummyGrnData = () => ({ remarks: 'All items inspected and accepted except partial rejection on line 3.', }, vendor: { - vendor_name: 'Tata Steel Limited', - gstin: '20AABCT3456A1Z9', - address: 'Bistupur Main Road', - city: 'Jamshedpur', - state: 'Jharkhand', - pincode: '831001', - }, - warehouse: { - name: 'Main warehouse', - address: 'Guindy Industrial Estate', + vendor_name: 'G E Capital Business Process Management Service Limited, Chennai Branch', + gstin: '29ABCDE1234F1Z5', + address: 'B6/5 Police Qrts, Alandur, Raja Street', city: 'Chennai', state: 'Tamil Nadu', - pincode: '600032', + pincode: '600117', + }, + location: { + name: 'Ahmedabad Warehouse', + gstin: '24AAACT1234A1Z8', + address: 'GIDC Vatva, Phase II', + city: 'Ahmedabad', + state: 'Gujarat', + pincode: '382445', + type: 'warehouse', }, items: [ { line_no: 1, - item_name: 'TMT steel bars 12mm', - item_code: 'ITM-TMT-12', - uom: 'MT', - ordered_qty: 25, - current_qty: 25, - accepted_qty: 25, + item_name: 'Bootle', + item_code: 'ITM-BTL-01', + uom: 'BTL', + ordered_qty: 500, + current_qty: 500, + accepted_qty: 500, rejected_qty: 0, - batch_no: 'BAT-TMT-2407', + batch_no: 'BAT-BTL-01', mfg_date: '2026-06-01', expiry_date: null, rejection_reason: null, @@ -59,38 +68,22 @@ const getDummyGrnData = () => ({ }, { line_no: 2, - item_name: 'Cement OPC 53 grade', - item_code: 'ITM-CEM-53', - uom: 'Bag', - ordered_qty: 500, - current_qty: 500, - accepted_qty: 500, - rejected_qty: 0, - batch_no: 'CEM-JUN-26', + item_name: 'Raw Materials - 1', + item_code: 'ITM-RM-01', + uom: 'KG', + ordered_qty: 1000, + current_qty: 980, + accepted_qty: 960, + rejected_qty: 20, + batch_no: 'RM-JUL-26', mfg_date: '2026-05-15', expiry_date: '2027-05-15', - rejection_reason: null, + rejection_reason: 'Damaged packaging', storage_location: 'Shed B', - remarks: null, - }, - { - line_no: 3, - item_name: 'River sand', - item_code: 'ITM-SND-01', - uom: 'CFT', - ordered_qty: 1200, - current_qty: 1200, - accepted_qty: 1180, - rejected_qty: 20, - batch_no: null, - mfg_date: null, - expiry_date: null, - rejection_reason: 'Damaged bags in transit', - storage_location: 'Yard 2', - remarks: 'Confirm bag count on delivery', + remarks: 'Confirm count on delivery', }, ], - generated_at: '14/07/2026 09:40', + generated_at: '14/07/2026 09:40 IST', }); const escapeHtml = (value = '') => @@ -124,12 +117,247 @@ const displayOrDash = (value) => { return String(value); }; +const renderAddressBlock = (party = {}) => { + const lines = []; + if (party.name || party.vendor_name) { + lines.push( + `