From 3590fbb39f0ded602b2a02603098dd34be0bacbf Mon Sep 17 00:00:00 2001 From: Gowtham M Date: Fri, 17 Jul 2026 11:12:51 +0530 Subject: [PATCH] PO Re-Design --- src/modules/grn/grn.service.js | 12 + .../purchase-orders.calculations.js | 19 +- .../purchase-orders.service.js | 27 +- src/utils/pdf/templates/grn.template.js | 600 +++++++------ src/utils/pdf/templates/po.template.js | 802 ++++++++++-------- 5 files changed, 788 insertions(+), 672 deletions(-) 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( + `
${escapeHtml(party.name || party.vendor_name || '-')}
` + ); + } + if (party.address) { + lines.push(`
${escapeHtml(party.address)}
`); + } + const cityLine = joinParts(party.city, party.state, party.pincode); + if (cityLine) { + lines.push(`
${escapeHtml(cityLine)}
`); + } + if (party.gstin) { + lines.push(`
GSTIN: ${escapeHtml(party.gstin)}
`); + } + if (!lines.length) { + lines.push(`
-
`); + } + return lines.join(''); +}; + +const sharedDocumentStyles = () => ` + * { box-sizing: border-box; } + body { + margin: 0; + font-family: Helvetica, Arial, sans-serif; + color: #111827; + background: #ffffff; + font-size: 11px; + line-height: 1.45; + } + .document { width: 100%; padding: 0; } + .text-right { text-align: right; } + .label { + color: ${MUTED}; + font-size: 9px; + letter-spacing: 0.08em; + text-transform: uppercase; + font-weight: 700; + } + + .header { + display: flex; + justify-content: space-between; + align-items: flex-start; + gap: 24px; + } + .brand { + display: flex; + gap: 12px; + align-items: flex-start; + flex: 1; + min-width: 0; + } + .company-logo { + width: 52px; + height: 52px; + object-fit: contain; + flex-shrink: 0; + } + .company-name { + margin: 0; + font-size: 20px; + font-weight: 800; + color: ${BRAND}; + letter-spacing: -0.02em; + } + .company-meta { + margin-top: 4px; + color: #4b5563; + font-size: 10.5px; + line-height: 1.5; + } + .title-block { text-align: right; min-width: 230px; } + .doc-title { + margin: 0; + font-size: 24px; + font-weight: 800; + color: ${BRAND}; + letter-spacing: -0.02em; + } + .doc-number { + margin-top: 4px; + color: #374151; + font-size: 12px; + font-weight: 600; + } + + .meta-bar { + margin-top: 16px; + display: grid; + grid-template-columns: 1fr 1fr 1fr; + gap: 0; + border-top: 1px solid ${LINE}; + border-bottom: 1px solid ${LINE}; + background: #fafbfc; + } + .meta-cell { + padding: 10px 14px; + border-right: 1px solid ${LINE}; + } + .meta-cell:last-child { border-right: 0; } + .meta-cell .value { + margin-top: 4px; + font-size: 12px; + font-weight: 700; + color: #111827; + } + .status-pill { + display: inline-block; + margin-top: 4px; + padding: 3px 10px; + border-radius: 999px; + background: ${STATUS_BG}; + color: ${STATUS_FG}; + font-size: 11px; + font-weight: 700; + } + + .party-grid { + margin-top: 16px; + display: grid; + grid-template-columns: 1fr 1fr; + gap: 20px 28px; + } + .party-name { + margin-top: 4px; + font-size: 12.5px; + font-weight: 800; + color: #111827; + } + .party-line { + margin-top: 2px; + color: #4b5563; + font-size: 10.5px; + line-height: 1.45; + } + + .info-grid { + margin-top: 14px; + display: grid; + grid-template-columns: 1fr 1fr 1fr 1fr; + gap: 12px 16px; + padding: 12px 14px; + background: #fafbfc; + border: 1px solid ${LINE}; + } + .info-grid .value { + margin-top: 3px; + font-size: 11.5px; + font-weight: 700; + color: #111827; + } + + table.items { + width: 100%; + border-collapse: collapse; + margin-top: 18px; + } + table.items thead th { + background: ${BRAND}; + color: #ffffff; + padding: 9px 4px; + font-size: 8px; + letter-spacing: 0.05em; + text-transform: uppercase; + font-weight: 700; + text-align: left; + } + table.items thead th.text-right { text-align: right; } + table.items tbody td { + padding: 9px 4px; + border-bottom: 1px solid ${LINE}; + vertical-align: top; + font-size: 10px; + } + table.items tbody tr:nth-child(even) { background: ${BRAND_SOFT}; } + .col-no { width: 3%; color: #6b7280; } + .col-item { width: 20%; } + .col-qty { width: 7%; } + .col-uom { width: 5%; } + .col-batch { width: 10%; } + .col-date { width: 9%; } + .col-reason { width: 14%; } + .item-name { font-weight: 700; } + .item-code { + margin-top: 1px; + color: #888888; + font-size: 9px; + } + .item-note { + margin-top: 2px; + color: #888888; + font-size: 9px; + font-style: italic; + } + + .notes { margin-top: 16px; } + .notes-block { margin-bottom: 10px; } + .notes-block .body { + margin-top: 4px; + color: #4b5563; + font-size: 10.5px; + line-height: 1.5; + white-space: pre-wrap; + } + + .sign-row { + display: grid; + grid-template-columns: 1fr 1fr 1fr; + gap: 24px; + margin-top: 36px; + } + .sign-line { + border-top: 1px solid #9ca3af; + margin-top: 48px; + padding-top: 8px; + } + .sign-title { font-size: 11px; font-weight: 700; color: #111827; } + .sign-sub { + margin-top: 2px; + color: #6b7280; + font-size: 10px; + } + + .footer { + margin-top: 28px; + text-align: center; + color: #9ca3af; + font-size: 9.5px; + } +`; + const generateGrnHtml = (inputData = getDummyGrnData()) => { const data = inputData || getDummyGrnData(); const company = data.company || {}; const grn = data.grn || {}; const vendor = data.vendor || {}; - const warehouse = data.warehouse || {}; + const location = data.location || data.warehouse || {}; const itemsRows = (data.items || []) .map((item, index) => { @@ -165,13 +393,6 @@ const generateGrnHtml = (inputData = getDummyGrnData()) => { }) .join(''); - const vendorAddress = joinParts(vendor.address, vendor.city, vendor.state, vendor.pincode); - const warehouseAddress = joinParts( - warehouse.address, - warehouse.city, - warehouse.state, - warehouse.pincode - ); const generatedAt = data.generated_at || formatDate(new Date(), { style: 'datetime' }); const lrLine = [grn.lr_no, grn.lr_date ? formatDate(grn.lr_date, { style: 'numeric' }) : null] .filter(Boolean) @@ -183,190 +404,7 @@ const generateGrnHtml = (inputData = getDummyGrnData()) => { Goods Receipt Note ${escapeHtml(grn.grn_number || '')} - +
@@ -386,79 +424,73 @@ const generateGrnHtml = (inputData = getDummyGrnData()) => {
-

Goods receipt note

+

Goods Receipt Note

${escapeHtml(grn.grn_number || '-')}
-
- -
-
+
+
Status
-
${escapeHtml(grn.status || '-')}
+
${escapeHtml(grn.status || '-')}
-
-
Date received
+
+
Date Received
${formatDate(grn.grn_date, { style: 'numeric' })}
-
-
PO reference
+
+
PO Reference
${escapeHtml(grn.po_number || '-')}
-
+
Vendor
-
${escapeHtml(vendor.vendor_name || '-')}
-
- ${escapeHtml(vendorAddress || '-')}
- ${vendor.gstin ? `GSTIN: ${escapeHtml(vendor.gstin)}` : ''} -
+ ${renderAddressBlock(vendor)}
-
Received at
-
${escapeHtml(warehouse.name || '-')}
-
${escapeHtml(warehouseAddress || '-')}
+
Received At
+ ${renderAddressBlock(location)} + ${ + location.type + ? `
Type: ${escapeHtml(location.type)}
` + : '' + }
-
- -
+
-
Vendor invoice
+
Vendor Invoice
${escapeHtml(grn.vendor_invoice_no || '-')}
-
Invoice date
+
Invoice Date
${formatDate(grn.vendor_invoice_date, { style: 'numeric' })}
-
Vehicle no
+
Vehicle No
${escapeHtml(grn.vehicle_no || '-')}
-
LR no / date
+
LR No / Date
${escapeHtml(lrLine || '-')}
-
- - + - + @@ -481,16 +513,22 @@ const generateGrnHtml = (inputData = getDummyGrnData()) => {
-
Received by
-
${escapeHtml(grn.received_by || '-')}
+
+
Received By
+
${escapeHtml(grn.received_by || '-')}
+
-
Quality checked by
-
${escapeHtml(grn.quality_checked_by || '-')}
+
+
Quality Checked By
+
${escapeHtml(grn.quality_checked_by || '-')}
+
-
Authorised signatory
-
${escapeHtml(company.name || '')}
+
+
Authorised Signatory
+
${escapeHtml(company.name || '')}
+
diff --git a/src/utils/pdf/templates/po.template.js b/src/utils/pdf/templates/po.template.js index c97e513..0c2000d 100644 --- a/src/utils/pdf/templates/po.template.js +++ b/src/utils/pdf/templates/po.template.js @@ -2,102 +2,98 @@ const { formatCurrency } = require('../helpers/formatCurrency'); const { formatDate } = require('../helpers/formatDate'); const { numberToWords } = require('../helpers/numberToWords'); +const BRAND = '#1e3a5f'; +const BRAND_SOFT = '#eef3f8'; +const STATUS_BG = '#fde8d0'; +const STATUS_FG = '#9a5b16'; +const MUTED = '#6b7280'; +const LINE = '#e5e7eb'; + const getDummyPoData = () => ({ 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', }, po: { - po_number: 'PO/2026-27/00007', - po_date: '2026-07-07', - expected_delivery_date: '2026-07-21', - status: 'Approved', - vendor_type: 'Raw material', - revision_no: 1, - payment_term: '30 days credit', - delivery_term: 'FOB (freight on board)', + po_number: 'PO/2026-27/00038', + po_date: '2026-07-15', + expected_delivery_date: '2026-06-01', + status: 'Partially Received', + vendor_type: 'Raw Material', + revision_no: 0, + payment_term: 'Net 30 Days', + delivery_term: 'Free on Board', terms_and_conditions: 'Payment as per agreed terms. Goods must match PO specification. Shortages or damages must be reported within 48 hours of delivery.', - remarks: 'Priority delivery required for production line restart on 22 Jul 2026.', + remarks: null, }, vendor: { - vendor_name: 'Tata Steel Limited', - gstin: '20AABCT3456A1Z9', - address: 'Bistupur Main Road', - city: 'Jamshedpur', - state: 'Jharkhand', - pincode: '831001', + 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: '600117', }, bill_to: { - name: 'Chennai manufacturing plant', + name: 'Chennai Junior Plants', gstin: '33AAACT1234A1Z5', - address: 'Guindy Industrial Estate', + address: 'Guindy Industrial Estate, Plot 14', city: 'Chennai', state: 'Tamil Nadu', pincode: '600032', }, ship_to: { - name: 'Main warehouse', - gstin: '33AAACT1234A1Z5', - address: 'Ambattur Industrial Estate', - city: 'Chennai', - state: 'Tamil Nadu', - pincode: '600058', + name: 'Ahmedabad Warehouse', + gstin: '24AAACT1234A1Z8', + address: 'GIDC Vatva, Phase II', + city: 'Ahmedabad', + state: 'Gujarat', + pincode: '382445', }, items: [ { line_no: 1, - item_name: 'TMT steel bars 12mm', - hsn_code: '72142090', - uom: 'MT', - ordered_qty: 25, - rate: 4250, - discount_pct: 2, - gst_rate_pct: 18, - amount: 106250, + item_name: 'Bootle', + hsn_code: '34029099', + uom: 'BTL', + ordered_qty: 500, + rate: 5, + discount_pct: 0, + gst_rate_pct: 12, + amount: 2500, }, { line_no: 2, - item_name: 'Cement OPC 53 grade', - hsn_code: '25232930', - uom: 'Bag', - ordered_qty: 500, - rate: 380, - discount_pct: 0, - gst_rate_pct: 28, - amount: 190000, - }, - { - line_no: 3, - item_name: 'River sand', - hsn_code: '25051000', - remarks: 'Confirm bag count on delivery', - uom: 'CFT', - ordered_qty: 1200, - rate: 45, - discount_pct: 5, - gst_rate_pct: 5, - amount: 54000, + item_name: 'Raw Materials - 1', + hsn_code: '28289040', + uom: 'KG', + ordered_qty: 1000, + rate: 10, + discount_pct: 10, + gst_rate_pct: 18, + amount: 10000, }, ], totals: { - sub_total: 119775, - tax_total: 19768.75, - cgst: 9884.38, - sgst: 9884.37, - igst: 0, - freight_charges: 750, - other_charges: 150, - discount_amount: 500, - grand_total: 139943.75, + sub_total: 11500, + freight_charges: 100, + other_charges: 0, + discount_amount: 20, + taxable_amount: 11580, + cgst: 0, + sgst: 0, + igst: 1920, + tax_total: 1920, + grand_total: 13500, }, - generated_at: '2026-07-07 14:32 IST', + generated_at: '17/07/2026 05:17 IST', }); const escapeHtml = (value = '') => @@ -126,6 +122,282 @@ const qtyDisplay = (value) => { return Number.isInteger(num) ? String(num) : num.toFixed(2); }; +/** Multi-line party address block (name + street + city/state/pin + GSTIN). */ +const renderAddressBlock = (party = {}, { showGstin = true } = {}) => { + const lines = []; + if (party.name || party.vendor_name) { + lines.push( + `
${escapeHtml(party.name || party.vendor_name || '-')}
` + ); + } + if (party.address) { + lines.push(`
${escapeHtml(party.address)}
`); + } + const cityLine = joinParts(party.city, party.state, party.pincode); + if (cityLine) { + lines.push(`
${escapeHtml(cityLine)}
`); + } + if (showGstin && party.gstin) { + lines.push(`
GSTIN: ${escapeHtml(party.gstin)}
`); + } + if (!lines.length) { + lines.push(`
-
`); + } + return lines.join(''); +}; + +const sharedDocumentStyles = () => ` + * { box-sizing: border-box; } + body { + margin: 0; + font-family: Helvetica, Arial, sans-serif; + color: #111827; + background: #ffffff; + font-size: 11px; + line-height: 1.45; + } + .document { width: 100%; padding: 0; } + .text-right { text-align: right; } + .label { + color: ${MUTED}; + font-size: 9px; + letter-spacing: 0.08em; + text-transform: uppercase; + font-weight: 700; + } + + .header { + display: flex; + justify-content: space-between; + align-items: flex-start; + gap: 24px; + } + .brand { + display: flex; + gap: 12px; + align-items: flex-start; + flex: 1; + min-width: 0; + } + .company-logo { + width: 52px; + height: 52px; + object-fit: contain; + flex-shrink: 0; + } + .company-name { + margin: 0; + font-size: 20px; + font-weight: 800; + color: ${BRAND}; + letter-spacing: -0.02em; + } + .company-meta { + margin-top: 4px; + color: #4b5563; + font-size: 10.5px; + line-height: 1.5; + } + .title-block { text-align: right; min-width: 210px; } + .doc-title { + margin: 0; + font-size: 26px; + font-weight: 800; + color: ${BRAND}; + letter-spacing: -0.02em; + } + .doc-number { + margin-top: 4px; + color: #374151; + font-size: 12px; + font-weight: 600; + } + + .meta-bar { + margin-top: 16px; + display: grid; + grid-template-columns: 1fr 1fr 1fr; + gap: 0; + border-top: 1px solid ${LINE}; + border-bottom: 1px solid ${LINE}; + background: #fafbfc; + } + .meta-cell { + padding: 10px 14px; + border-right: 1px solid ${LINE}; + } + .meta-cell:last-child { border-right: 0; } + .meta-cell .value { + margin-top: 4px; + font-size: 12px; + font-weight: 700; + color: #111827; + } + .status-pill { + display: inline-block; + margin-top: 4px; + padding: 3px 10px; + border-radius: 999px; + background: ${STATUS_BG}; + color: ${STATUS_FG}; + font-size: 11px; + font-weight: 700; + } + + .party-grid { + margin-top: 16px; + display: grid; + grid-template-columns: 1fr 1fr; + gap: 20px 28px; + } + .party-name { + margin-top: 4px; + font-size: 12.5px; + font-weight: 800; + color: #111827; + } + .party-line { + margin-top: 2px; + color: #4b5563; + font-size: 10.5px; + line-height: 1.45; + } + .party-term { + margin-top: 6px; + color: #374151; + font-size: 10.5px; + } + .party-term strong { font-weight: 700; } + + table.items { + width: 100%; + border-collapse: collapse; + margin-top: 18px; + } + table.items thead th { + background: ${BRAND}; + color: #ffffff; + padding: 9px 6px; + font-size: 9px; + letter-spacing: 0.06em; + text-transform: uppercase; + font-weight: 700; + text-align: left; + } + table.items thead th.text-right { text-align: right; } + table.items tbody td { + padding: 10px 6px; + border-bottom: 1px solid ${LINE}; + vertical-align: top; + font-size: 11px; + } + table.items tbody tr:nth-child(even) { background: ${BRAND_SOFT}; } + .col-no { width: 4%; color: #6b7280; } + .col-item { width: 26%; } + .col-hsn { width: 10%; } + .col-qty { width: 7%; } + .col-uom { width: 7%; } + .col-rate { width: 12%; } + .col-disc { width: 7%; } + .col-gst { width: 7%; } + .col-amt { width: 14%; } + .item-name { font-weight: 700; } + .item-note { + margin-top: 2px; + color: #888888; + font-size: 9.5px; + font-style: italic; + } + + .summary-wrap { + margin-top: 14px; + display: flex; + justify-content: flex-end; + } + .summary { width: 300px; } + .summary-row { + display: flex; + justify-content: space-between; + gap: 24px; + padding: 4px 0; + color: #4b5563; + font-size: 11px; + } + .summary-row .amount { color: #111827; font-weight: 600; } + .summary-row.discount .amount { color: #c62828; } + .summary-row.taxable { + margin-top: 2px; + padding-top: 6px; + border-top: 1px solid ${LINE}; + color: #111827; + font-weight: 700; + } + .grand-row { + display: flex; + justify-content: space-between; + gap: 24px; + margin-top: 8px; + padding: 10px 0 4px; + border-top: 1.5px solid ${BRAND}; + font-size: 14px; + font-weight: 800; + color: ${BRAND}; + } + + .amount-words { + margin-top: 16px; + background: #f3f4f6; + border: 1px solid ${LINE}; + padding: 10px 12px; + } + .amount-words .label { + color: ${MUTED}; + font-size: 9px; + letter-spacing: 0.08em; + } + .amount-words .words { + margin-top: 3px; + font-weight: 700; + font-size: 12px; + color: #111827; + } + + .notes { margin-top: 16px; } + .notes-block { margin-bottom: 10px; } + .notes-block .body { + margin-top: 4px; + color: #4b5563; + font-size: 10.5px; + line-height: 1.5; + white-space: pre-wrap; + } + + .sign-row { + display: grid; + grid-template-columns: 1fr 1fr; + gap: 40px; + margin-top: 36px; + } + .sign-line { + border-top: 1px solid #9ca3af; + margin-top: 48px; + padding-top: 8px; + } + .sign-title { font-size: 11px; font-weight: 700; color: #111827; } + .sign-sub { + margin-top: 2px; + color: #6b7280; + font-size: 10px; + } + + .footer { + margin-top: 28px; + text-align: center; + color: #9ca3af; + font-size: 9.5px; + } +`; + const generatePoHtml = (inputData = getDummyPoData()) => { const data = inputData || getDummyPoData(); const company = data.company || {}; @@ -164,12 +436,24 @@ const generatePoHtml = (inputData = getDummyPoData()) => { .join(''); const discountValue = Number(totals.discount_amount || 0); - const discountFormatted = discountValue > 0 ? `-${formatCurrency(discountValue)}` : formatCurrency(0); + const discountFormatted = + discountValue > 0 ? `-${formatCurrency(discountValue)}` : formatCurrency(0); + + const taxableAmount = + totals.taxable_amount != null + ? Number(totals.taxable_amount) + : Number(totals.sub_total || 0) + + Number(totals.freight_charges || 0) + + Number(totals.other_charges || 0) - + discountValue; const cgst = Number(totals.cgst || 0); const sgst = Number(totals.sgst || 0); const igst = Number(totals.igst || 0); - const taxRowsHtml = + const tdsAmount = Number(totals.tds_amount || 0); + const adjustmentAmount = Number(totals.adjustment || 0); + + const taxBreakdownHtml = igst > 0 ? `
@@ -186,9 +470,27 @@ const generatePoHtml = (inputData = getDummyPoData()) => { ${formatCurrency(sgst)}
`; - const vendorAddress = joinParts(vendor.address, vendor.city, vendor.state, vendor.pincode); - const billAddress = joinParts(billTo.address, billTo.city, billTo.state, billTo.pincode); - const shipAddress = joinParts(shipTo.address, shipTo.city, shipTo.state, shipTo.pincode); + const optionalChargeRows = [ + tdsAmount > 0 + ? ` +
+ TDS + -${formatCurrency(tdsAmount)} +
` + : '', + adjustmentAmount !== 0 + ? ` +
+ Adjustment + ${ + adjustmentAmount < 0 + ? `-${formatCurrency(Math.abs(adjustmentAmount))}` + : formatCurrency(adjustmentAmount) + } +
` + : '', + ].join(''); + const generatedAt = data.generated_at || formatDate(new Date(), { style: 'datetime' }); return ` @@ -197,239 +499,7 @@ const generatePoHtml = (inputData = getDummyPoData()) => { Purchase Order ${escapeHtml(po.po_number || '')} - +
@@ -449,90 +519,62 @@ const generatePoHtml = (inputData = getDummyPoData()) => {
-

Purchase order

+

Purchase Order

${escapeHtml(po.po_number || '-')}
-
- -
-
+
+
Status
-
${escapeHtml(po.status || '-')}
+
${escapeHtml(po.status || '-')}
-
-
Date issued
+
+
Date Issued
${formatDate(po.po_date, { style: 'numeric' })}
-
-
Expected delivery
+
+
Expected Delivery
${formatDate(po.expected_delivery_date, { style: 'numeric' })}
-
+
Vendor
-
${escapeHtml(vendor.vendor_name || '-')}
-
- ${escapeHtml(vendorAddress || '-')}
- ${vendor.gstin ? `GSTIN: ${escapeHtml(vendor.gstin)}` : ''} -
+ ${renderAddressBlock(vendor)} ${ po.payment_term - ? `
Payment term: ${escapeHtml(po.payment_term)}
` + ? `
Payment term: ${escapeHtml(po.payment_term)}
` : '' }
-
Bill to
-
${escapeHtml(billTo.name || '-')}
-
- ${escapeHtml(billAddress || '-')}
- ${billTo.gstin ? `GSTIN: ${escapeHtml(billTo.gstin)}` : ''} -
+
Bill To
+ ${renderAddressBlock(billTo)}
-
- -
- -
-
Ship to
-
${escapeHtml(shipTo.name || '-')}
-
- ${escapeHtml(shipAddress || '-')}
- ${shipTo.gstin ? `GSTIN: ${escapeHtml(shipTo.gstin)}` : ''} -
+
Ship To
+ ${renderAddressBlock(shipTo)} ${ po.delivery_term - ? `
Delivery term: ${escapeHtml(po.delivery_term)}
` + ? `
Delivery term: ${escapeHtml(po.delivery_term)}
` : '' }
-
- -
- -
-
Vendor type
-
${escapeHtml(po.vendor_type || '-')}
-
-
-
Revision
-
${escapeHtml(String(po.revision_no ?? 0))}
+
Vendor Type
+
${escapeHtml(po.vendor_type || '-')}
+
Revision
+
${escapeHtml(String(po.revision_no ?? 0))}
-
-
#Item descriptionItem Description Ordered Received Accepted Rejected UOM BatchMfg dateMfg Date Expiry Reason
- + @@ -550,42 +592,48 @@ const generatePoHtml = (inputData = getDummyPoData()) => {
- Taxable amount + Sub Total ${formatCurrency(totals.sub_total)}
- Tax (GST) - ${formatCurrency(totals.tax_total)} -
- ${taxRowsHtml} -
- Freight charges + Freight Charges ${formatCurrency(totals.freight_charges)}
- Other charges + Other Charges ${formatCurrency(totals.other_charges)}
-
- Discount +
+ Discount Amount ${discountFormatted}
+
+ Taxable Amount + ${formatCurrency(taxableAmount)} +
+ ${taxBreakdownHtml} +
+ Tax Total + ${formatCurrency(totals.tax_total)} +
+ ${optionalChargeRows}
- Grand total + Grand Total ${formatCurrency(totals.grand_total)}
-
-
Amount in words
-
${escapeHtml(numberToWords(totals.grand_total))}
-
+
+
Amount In Words
+
${escapeHtml(numberToWords(totals.grand_total))}
+
+
${ po.terms_and_conditions ? `
-
Terms and conditions
+
Terms And Conditions
${escapeHtml(po.terms_and_conditions)}
` : '' @@ -602,12 +650,16 @@ const generatePoHtml = (inputData = getDummyPoData()) => {
-
Authorised signatory
-
${escapeHtml(company.name || '')}
+
+
Authorised Signatory
+
${escapeHtml(company.name || '')}
+
-
Accepted by vendor
-
${escapeHtml(vendor.vendor_name || '')}
+
+
Accepted By Vendor
+
${escapeHtml(vendor.vendor_name || '')}
+
#Item descriptionItem Description HSN Qty UOM