import { MyDate } from '../../utils/my-date'; import { normalizeHeaderData } from '../header-footer-section/pdf-header-footer.section'; import { firstValue } from '../header-footer-section/pdf-shared.utils'; import { Add } from '../math-operations'; import { formatAmount, readNumericValue } from '../table-section/pdf-table.section'; export function buildInvoiceTotalsAndNotesSection(invoiceData: any, availableWidth: number, PrintConfigData: any = {}) { const normalizedInvoice = invoiceData || {}; const sType = firstValue(normalizedInvoice?.SType); const taxMode = resolveInvoiceTaxMode(normalizedInvoice); const companyName = firstValue( normalizedInvoice?.Entity?.CName, normalizedInvoice?.Entity?.Name, normalizedInvoice?.CName, normalizedInvoice?.CompanyName ); const hideTopSection = isTruthy(normalizedInvoice?.Settings?.NoPr); const isPoSPrint = isTruthy(PrintConfigData?.IsPoSPrint); const shouldShowTaxSummary = resolveShowTaxSummary(normalizedInvoice, PrintConfigData, taxMode); const taxSummary = resolveTaxSummary(normalizedInvoice, shouldShowTaxSummary); const showBankDetails = resolveShowBankDetails(normalizedInvoice, PrintConfigData); const showDueSection = shouldShowDueSection(PrintConfigData); const bankDetails = showBankDetails ? resolveBankDetails(normalizedInvoice) : { lines: [], hasData: false }; const totals = resolveTotals(normalizedInvoice, taxSummary.totalTax, PrintConfigData, taxMode); const totalInWords = resolveTotalInWords(normalizedInvoice); const notes = firstValue( normalizedInvoice?.Notes, normalizedInvoice?.Note, normalizedInvoice?.Remarks, normalizedInvoice?.Remark ); const terms = firstValue( normalizedInvoice?.TermsAndConditions, normalizedInvoice?.TermsCondition, normalizedInvoice?.Terms, normalizedInvoice?.TC ); const dueInfo = resolveDueInfo(normalizedInvoice, totals); const qrPayload = resolveQrPayload(normalizedInvoice, PrintConfigData); const hasTopSection = !hideTopSection && (taxSummary.hasData || bankDetails.hasData || totals.hasData); const hasBottomSection = !!(notes || terms || totalInWords || (showDueSection && dueInfo.hasData) || qrPayload); if (!hasTopSection && !hasBottomSection) { return []; } // const dividerLine = { // canvas: [ // { type: 'line', x1: 0, y1: 0, x2: Math.max(1, availableWidth), y2: 0, lineWidth: 1, lineColor: '#1f2937' } // ], // margin: [0, 5, 0, 5] // }; const topColumns = { columns: buildTopSectionColumns(taxSummary, sType, showBankDetails, bankDetails, totals, totalInWords), columnGap: 20, margin: [0, 2, 0, 8] }; const bottomColumns = buildPostTotalsInfoSection({ totalInWords, notes, terms, companyName, paidAmount: dueInfo.paid, dueAmount: dueInfo.due, qrPayload, upiPhone: resolveUpiPhone(normalizedInvoice), showInWords: taxSummary.hasData || showBankDetails, showDueSection, isPoSPrint }); if (hasTopSection && hasBottomSection) { return [ topColumns, bottomColumns]; } if (hasTopSection) { return [ topColumns]; } return [ bottomColumns]; } function buildTopSectionColumns( taxSummary: { hasData: boolean; rows: any[] }, sType: string, showBankDetails: boolean, bankDetails: { lines: Array<{ label: string; value: string }>; hasData: boolean }, totals: { lines: Array<{ label: string; value: string }>; hasData: boolean }, totalInWords: string ) { const inWords = totalInWords || '-'; if (taxSummary.hasData) { return [ { width: '42%', stack: buildTaxSummaryTableStack(null, taxSummary.rows, sType), marginTop: 2 }, { width: '28%', stack: showBankDetails ? buildInfoCardStack('Bank Details', bankDetails.lines) : [{ text: '' }] }, { width: '30%', stack: buildInfoCardStack(null, totals.lines, { rightAlign: true }) } ]; } if (showBankDetails) { return [ { width: '40%', stack: buildInfoCardStack('Bank Details', bankDetails.lines) }, { width: '60%', stack: buildInfoCardStack(null, totals.lines, { rightAlign: true }) } ]; } return [ { width: '70%', columns: [ { width: 'auto', text: 'In Words :', bold: true, fontSize: 9, color: '#111827' }, { width: '*', text: inWords, bold: true, fontSize: 9, color: '#111827' } ], columnGap: 6, margin: [0, 2, 0, 4] }, { width: '30%', stack: buildInfoCardStack(null, totals.lines, { rightAlign: true }) } ]; } function buildPostTotalsInfoSection(data: { totalInWords: string; notes: string; terms: string; companyName: string; paidAmount: number; dueAmount: number; qrPayload: string; upiPhone: string; showInWords: boolean; showDueSection: boolean; isPoSPrint: boolean; }) { const hasQr = !!firstValue(data.qrPayload); const inWords = data.totalInWords || '-'; const termsText = firstValue(data.terms); const leftStack: any[] = [ ]; if (data.showInWords) { leftStack.push( { columns: [ { width: 'auto', text: 'In Words :', bold: true, fontSize: 9, color: '#111827' }, { width: '*', text: inWords, bold: true, fontSize: 9, color: '#111827' } ], columnGap: 6, margin: [0, 2, 0, 4] } ) } if (data.showDueSection && !data.isPoSPrint) { const paidText = `Paid : Rs.${formatAmountFixed2(data.paidAmount)}`; const dueText = `Due : Rs.${formatAmountFixed2(data.dueAmount)}`; leftStack.push({ table: { widths: ['*', '*'], body: [[ { text: paidText, fontSize: 8, color: '#111827', margin: [4, 2, 4, 2], alignment: 'left' }, { text: dueText, fontSize: 8, color: '#111827', margin: [4, 2, 4, 2], alignment: 'right' } ]] }, layout: { hLineWidth: () => 0.5, vLineWidth: () => 0.5, hLineColor: () => '#d3d3d3', vLineColor: () => '#d3d3d3' }, margin: [0, 0, 0, 6] }); } if (data.isPoSPrint) { leftStack.push({ columns: [ { width: '60%', stack: [ { text: '* Goods once sold will not be taken back or exchanged', fontSize: 6, color: '#111827' }, { text: '* PLEASE GET YOUR MEDICINES CHECKED BY YOUR DOCTOR BEFORE USE', fontSize: 6, color: '#111827' } ], margin: [5, 0, 0, 6], lineHeight: 1.1 }, { width: '40%', text: `For ${data.companyName || ''}`.trim(), fontSize: 9, bold: true, color: '#111827', alignment: 'right' } ], columnGap: 6, margin: [0, 2, 0, 0] }); } if (termsText) { leftStack.push({ text: `\u2022 ${termsText}`, fontSize: 8, color: '#111827', lineHeight: 1.1 }); } const leftColumn = { width: hasQr ? '74%' : '*', stack: leftStack }; const columns: any[] = [leftColumn]; if (hasQr) { columns.push({ width: '26%', stack: [ { qr: data.qrPayload, fit: 95, alignment: 'right' }, { text: 'Scan To Pay', fontSize: 8, bold: true, alignment: 'right', margin: [0, 2, 0, 0] }, ...(firstValue(data.upiPhone) ? [{ text: `UPIPhone: ${data.upiPhone}`, fontSize: 8, bold: true, alignment: 'right', margin: [0, 2, 0, 0] }] : []) ] }); } return { columns, columnGap: hasQr ? 12 : 0, margin: [0, 0, 0, 10] }; } export function buildSignatureSection(PrintConfig: any, invoiceData: any, availableWidth: number) { const headerData = normalizeHeaderData(PrintConfig?.headerData ?? {}); const isPoSPrint = isTruthy(PrintConfig?.IsPoSPrint ?? PrintConfig?.isPoSPrint); const companyName = firstValue( headerData?.organization?.cName, invoiceData?.Entity?.CName, invoiceData?.Entity?.Name, invoiceData?.CName ); const withPass = !!(PrintConfig?.withPass || invoiceData?.withPass || invoiceData?.WithPass); if (isPoSPrint) { return []; } const signatureRows: any[] = [ { columns: [ { width: '*', text: '' }, { width: '*', text: `For ${companyName || ''}`.trim(), fontSize: 9, bold: true, color: '#111827', alignment: 'right' } ], margin: [0, 4, 0, 0] } ]; if (!isPoSPrint) { signatureRows.push({ text: '', margin: [0, 10, 0, 0] }); signatureRows.push({ columns: [ { width: '*', text: 'Customer Signature', fontSize: 9, color: '#111827', alignment: 'left' }, { width: '*', text: 'Authorised Signatory', fontSize: 9, color: '#111827', alignment: 'right' } ], margin: [0, 0, 0, 0] }); } if (!withPass) { return signatureRows; } return [...signatureRows, ...buildGatePassSection(companyName, invoiceData, availableWidth)]; } function buildGatePassSection(companyName: string, invoiceData: any, availableWidth: number) { const customerName = firstValue( invoiceData?.Customer?.Name, invoiceData?.Cust?.Name, invoiceData?.BillTo?.Name, invoiceData?.CustomerName ); const serviceAdvisor = firstValue( invoiceData?.Name, invoiceData?.ServiceAdvisor, invoiceData?.AdvisorName, invoiceData?.SAName ); const chassisNo = firstValue( invoiceData?.Prod?.CIN, invoiceData?.Prod?.ChassisNo, invoiceData?.Product?.VIN, invoiceData?.Vehicle?.VIN ); const regnNo = firstValue( invoiceData?.Prod?.RegnNo, invoiceData?.Product?.RegNo, invoiceData?.Veh?.RegNo, invoiceData?.Vehicle?.RegNo, invoiceData?.VehicleNo, invoiceData?.VehNo ); const vehicleModel = firstValue( invoiceData?.Prod?.Model, invoiceData?.Product?.Model, invoiceData?.Vehicle?.Model ); const gatePassDate = MyDate.ConvertUTCDateToReadable(MyDate.GetDateTimeNowInUTC(invoiceData?.Entity?.User?.TZ)) const dividerLine = { canvas: [ { type: 'line', x1: 0, y1: 0, x2: Math.max(1, availableWidth), y2: 0, lineWidth: 1, lineColor: '#1f2937' } ], margin: [0, 5, 0, 5] }; return [ { canvas: [ { type: 'line', x1: 0, y1: 0, x2: 555, y2: 0, lineWidth: 0.8, lineColor: '#374151' } ], margin: [0, 10, 0, 5] }, { columns: [ { width: '*', text: '' }, { width: 'auto', text: 'Gate Pass', bold: true, fontSize: 10, alignment: 'center' }, { width: '*', text: `Date: ${gatePassDate}`, fontSize: 9, alignment: 'right' } ], margin: [0, 0, 0, 4] }, { text: companyName || '-', alignment: 'center', bold: true, fontSize: 11, margin: [0, 0, 0, 6] }, { columns: [ { width: '50%', table: { widths: ['auto', 8, '*'], body: [ [ { text: 'Customer Name', fontSize: 8, bold: true }, { text: ':', fontSize: 8, bold: true, alignment: 'center' }, { text: customerName || '-', fontSize: 8 } ], [ { text: 'Service Advisor', fontSize: 8, bold: true }, { text: ':', fontSize: 8, bold: true, alignment: 'center' }, { text: serviceAdvisor || '-', fontSize: 8 } ] ] }, layout: 'noBorders' }, { width: '50%', table: { widths: ['auto', 8, '*'], body: [ [ { text: 'Chassis No', fontSize: 8, bold: true }, { text: ':', fontSize: 8, bold: true, alignment: 'center' }, { text: chassisNo || '-', fontSize: 8 } ], [ { text: 'Regn. No', fontSize: 8, bold: true }, { text: ':', fontSize: 8, bold: true, alignment: 'center' }, { text: regnNo || '-', fontSize: 8 } ], [ { text: 'Vehicle Model', fontSize: 8, bold: true }, { text: ':', fontSize: 8, bold: true, alignment: 'center' }, { text: vehicleModel || '-', fontSize: 8 } ] ] }, layout: 'noBorders' } ], columnGap: 12, margin: [0, 0, 0, 0] }, { text: 'Vehicle has been received from workshop and work done as per my satisfaction.', fontSize: 7, color: '#111827', margin: [0, 8, 0, 10] }, { columns: [ { width: '*', text: 'Customer Signature', fontSize: 8, color: '#111827', alignment: 'left' }, { width: '*', text: 'Authorised Signatory', fontSize: 8, color: '#111827', alignment: 'right' } ], margin: [0, 0, 0, 0] } ]; } // function formatGatePassDate(value: string) { // const raw = firstValue(value); // if (!raw) { // return formatAsDDMMYYYY(new Date()); // } // const parsed = new Date(raw); // if (Number.isNaN(parsed.getTime())) { // return raw; // } // return formatAsDDMMYYYY(parsed); // } // function formatAsDDMMYYYY(dateValue: Date) { // const day = leftPadWithZero(dateValue.getDate()); // const month = leftPadWithZero(dateValue.getMonth() + 1); // const year = dateValue.getFullYear(); // return `${day}/${month}/${year}`; // } // function leftPadWithZero(value: number) { // const text = String(value); // return text.length >= 2 ? text : `0${text}`; // } function buildSimpleTextCardStack(title: string, value: string) { return [ { text: title, bold: true, fontSize: 9, color: '#374151', margin: [0, 0, 0, 4] }, { text: value || '-', fontSize: 8, color: '#111827', lineHeight: 1.1 } ]; } function buildInfoCardStack(title: string | null, lines: Array<{ label: string; value: string }>, options: { rightAlign?: boolean } = {}) { const body = Array.isArray(lines) ? lines : []; const isRightAlign = !!options.rightAlign; const heading = firstValue(title); return [ ...(heading ? [{ text: heading, bold: true, fontSize: (heading === 'Bank Details') ? 7 : 9, color: '#374151', margin: [0, 0, 0, 4], alignment: isRightAlign ? 'right' : 'left' }] : []), { table: { widths: isRightAlign ? ['*', 6, 'auto'] : ['auto', 6, '*'], body: (body.length > 0 ? body : [{ label: '', value: '-' }]).map((line: any) => { const displayValue = hasDisplayValue(line?.value) ? String(line.value) : '-'; return [ { text: line.label || '', bold: true, fontSize: (heading === 'Bank Details') ? 6 : 8, color: '#111827', alignment: isRightAlign ? 'right' : 'left' }, { text: ':', bold: true, fontSize: (heading === 'Bank Details') ? 6 : 8, color: '#111827', alignment: 'center' }, { text: displayValue, fontSize: (heading === 'Bank Details') ? 6 : 8, color: '#111827', alignment: isRightAlign ? 'right' : 'left' } ]; }) }, layout: 'noBorders' } ]; } function buildTaxSummaryTableStack( title: string | null, rows: Array<{ taxRate: string; taxableAmount: string; cgstAmount: string; sgstAmount: string; igstAmount: string }>, sType: string = '' ) { const isInter = sType === 'Inter'; const normalizedRows = Array.isArray(rows) ? rows : []; const tableRows = normalizedRows.length > 0 ? normalizedRows.map((row: any) => ( isInter ? [ { text: row.taxRate || '-', fontSize: 7, color: '#111827' }, { text: row.taxableAmount || '-', fontSize: 7, color: '#111827', alignment: 'right' }, { text: row.igstAmount || '-', fontSize: 7, color: '#111827', alignment: 'right' } ] : [ { text: row.taxRate || '-', fontSize: 7, color: '#111827' }, { text: row.taxableAmount || '-', fontSize: 7, color: '#111827', alignment: 'right' }, { text: row.cgstAmount || '-', fontSize: 7, color: '#111827', alignment: 'right' }, { text: row.sgstAmount || '-', fontSize: 7, color: '#111827', alignment: 'right' } ] )) : [ isInter ? [ { text: '-', fontSize: 7, color: '#111827' }, { text: '-', fontSize: 7, color: '#111827', alignment: 'right' }, { text: '-', fontSize: 7, color: '#111827', alignment: 'right' } ] : [ { text: '-', fontSize: 7, color: '#111827' }, { text: '-', fontSize: 7, color: '#111827', alignment: 'right' }, { text: '-', fontSize: 7, color: '#111827', alignment: 'right' }, { text: '-', fontSize: 7, color: '#111827', alignment: 'right' } ] ]; return [ // { text: title, bold: true, fontSize: 9, color: '#374151', margin: [0, 0, 0, 4] }, { table: { headerRows: 0, widths: isInter ? ['auto', '*', '*'] : ['auto', '*', '*', '*'], body: [ isInter ? [ { text: 'Tax Rate', bold: true, fontSize: 7, fillColor: '#eeeeee' }, { text: 'Taxable Amount', bold: true, fontSize: 7, fillColor: '#eeeeee', alignment: 'right' }, { text: 'IGST(Rs)', bold: true, fontSize: 7, fillColor: '#eeeeee', alignment: 'right' } ] : [ { text: 'Tax Rate', bold: true, fontSize: 7, fillColor: '#eeeeee' }, { text: 'Taxable Amount', bold: true, fontSize: 7, fillColor: '#eeeeee', alignment: 'right' }, { text: 'CGST(Rs)', bold: true, fontSize: 7, fillColor: '#eeeeee', alignment: 'right' }, { text: 'SGST(Rs)', bold: true, fontSize: 7, fillColor: '#eeeeee', alignment: 'right' } ], ...tableRows ] }, layout: { hLineWidth: (lineIndex: number) => lineIndex === 1 ? 0.5 : 0, vLineWidth: () => 0, hLineColor: () => '#d3d3d3', paddingLeft: () => 4, paddingRight: () => 4, paddingTop: () => 3, paddingBottom: () => 3 } } ]; } function resolveTaxSummary(invoiceData: any, shouldShowTaxSummary: boolean) { if (!shouldShowTaxSummary) { return { rows: [], totalTax: 0, hasData: false }; } const sourceRows = Array.isArray(invoiceData?.TaxSummary) ? invoiceData.TaxSummary : []; const rows = sourceRows.map((entry: any) => { const taxRate = firstValue( entry?.Tax, entry?.TaxRate, entry?.CombinedTaxPercentage, entry?.Perc, entry?.Percentage ); const taxable = readNumericValue(entry, ['TaxableAmount', 'TotalTaxableAmount', 'TaxableValue', 'Amount']); const cgst = readNumericValue(entry, ['CGSTAmount', 'CGSTAmt', 'CGST']); const sgst = readNumericValue(entry, ['SGSTAmount', 'SGSTAmt', 'SGST', 'UGSTAmount', 'UGSTAmt', 'UGST']); const igst = readNumericValue(entry, ['IGSTAmount', 'IGSTAmt', 'IGST']); return { taxRate: formatTaxRate(taxRate), taxableAmount: formatAmount(taxable), cgstAmount: formatAmount(cgst), sgstAmount: formatAmount(sgst), igstAmount: formatAmount(igst), totalTaxAmount: cgst + sgst + igst }; }); const totalTax = rows.reduce((sum: number, row: any) => sum + (row.totalTaxAmount || 0), 0); return { rows, totalTax, hasData: rows.length > 0 }; } function resolveShowTaxSummary(invoiceData: any, printConfigData: any = {}, taxMode: string = '') { if (shouldHideTaxDetails(taxMode)) { return false; } const value = firstValue( printConfigData?.ShowTaxSummary, invoiceData?.ShowTaxSummary ); return isTruthy(value); } function resolveBankDetails(invoiceData: any) { const bank = invoiceData?.Entity?.Bank || invoiceData?.Entity?.BankDetails || invoiceData?.Bank || invoiceData?.BankDetails || {}; const lines = [ { label: 'Bank', value: firstValue(bank?.Name, bank?.BankName, bank?.BName) }, { label: 'A/C No', value: firstValue(bank?.ACNo, bank?.AccountNo, bank?.AccNo) }, { label: 'A/C Holder Name', value: firstValue(bank?.ACName, bank?.AccountHolderName, bank?.AccHolderName, bank?.AccHolder) }, { label: 'IFSC', value: firstValue(bank?.IFSC, bank?.Ifsc) }, { label: 'Branch', value: firstValue(bank?.Branch, bank?.Bran) }, // { label: 'UPI', value: firstValue(bank?.UPI, bank?.Upi) } ].filter((line: any) => !!line.value); return { lines, hasData: lines.length > 0 }; } function resolveShowBankDetails(invoiceData: any, printConfigData: any = {}) { const value = firstValue( printConfigData?.ShowBankDetails, invoiceData?.ShowBankDetails, invoiceData?.Entity?.ShowBankDetails ); return isTruthy(value); } function shouldShowDueSection(printConfigData: any = {}) { const type = firstValue(printConfigData?.Type); if (!type) { return false; } const normalizedType = String(type).trim().toLowerCase(); return normalizedType === 'invoice' || normalizedType === 'bill'; } function isTruthy(value: any) { if (typeof value === 'boolean') { return value; } if (typeof value === 'number') { return value !== 0; } if (typeof value === 'string') { const normalized = value.trim().toLowerCase(); return normalized === 'true' || normalized === '1' || normalized === 'yes'; } return false; } function hasDisplayValue(value: any) { if (value === undefined || value === null) { return false; } if (typeof value === 'number') { return Number.isFinite(value); } if (typeof value === 'string') { return value.trim().length > 0; } return true; } function resolveTotals(invoiceData: any, totalTax: number, printConfigData: any = {}, taxMode: string = '') { const subtotal = readNumericValue(invoiceData, ['SIndTotal', 'STotal', 'SubTotal', 'SubTot', 'TaxableAmount', 'TaxableValue']); const isPoSPrint = isTruthy(printConfigData?.IsPoSPrint ?? printConfigData?.isPoSPrint); const hideTaxDetails = shouldHideTaxDetails(taxMode); const showDetailedDisc = isTruthy(firstValue(printConfigData?.ShowDetailedDisc)); const itemsDiscount = parseNumber(firstValue(invoiceData?.PDisc)); const servicesDiscount = parseNumber(firstValue(invoiceData?.LDisc)); const overallDiscount = parseNumber(firstValue(invoiceData?.Disc, invoiceData?.Discount, invoiceData?.TotDisc)); const overalItemsDiscount = parseNumber(firstValue(invoiceData?.CustPartsDiscTotal || invoiceData?.PartsDiscTotal)); const overalServicesDiscount = parseNumber(firstValue(invoiceData?.CustLaborDiscTotal || invoiceData?.ServiceDiscTotal)); const hasDetailedDiscSource = firstValue(invoiceData?.PDisc) !== '' || firstValue(invoiceData?.LDisc) !== '' || firstValue(invoiceData?.Disc, invoiceData?.Discount, invoiceData?.TotDisc) !== ''; const discount = hasDetailedDiscSource ? itemsDiscount + servicesDiscount + overallDiscount : readNumericValue(invoiceData, ['Disc', 'Discount', 'TotDisc']); const roundOff = readNumericValue(invoiceData, ['Round', 'RoundOff']); const taxFromInvoice = readNumericValue(invoiceData, ['TotalTax', 'TaxTotal', 'TaxAmt', 'GSTTotal', 'CustItemITax']); const totalTaxValue = taxFromInvoice !== 0 ? taxFromInvoice : totalTax; const returnTotal = firstValue(invoiceData?.RetTotal); const finalTotalRaw = firstValue(invoiceData?.FinalTotal, invoiceData?.Total, invoiceData?.GrandTotal, invoiceData?.NetTotal); const finalTotal = parseNumber(finalTotalRaw); const paidAmount = readNumericValue(invoiceData, ['Paid', 'PaidAmount', 'PaidAmt', 'ReceivedAmount', 'RecAmt']); let totalBeforeRound = readNumericValue(invoiceData, ['TotalBeforeRound', 'BeforeRoundTotal', 'PreRoundTotal']); if (totalBeforeRound === 0) { if (finalTotalRaw) { totalBeforeRound = finalTotal - roundOff; } else if (subtotal !== 0 || discount !== 0 || totalTaxValue !== 0) { totalBeforeRound = subtotal - discount + totalTaxValue; } else { totalBeforeRound = finalTotal; } } const resolvedFinalTotal = finalTotalRaw ? finalTotal : totalBeforeRound + roundOff; const dueFromInvoice = readNumericValue(invoiceData, ['Due', 'DueAmount', 'DueAmt', 'Balance', 'BalAmt']); const dueAmount = dueFromInvoice !== 0 ? dueFromInvoice : Math.max(0, resolvedFinalTotal - paidAmount); const discountLines = (showDetailedDisc ? [ { label: 'Items Discount', amount: itemsDiscount }, { label: 'Services Discount', amount: servicesDiscount }, { label: 'Overall Discount', amount: overallDiscount } ] : [ { label: 'Discount', amount: Add(overalItemsDiscount, overalServicesDiscount) } ]) .filter((line: any) => parseNumber(line?.amount) !== 0); const totalLines = [ { label: 'SubTotal', amount: subtotal }, ...discountLines, ...(!hideTaxDetails ? [{ label: 'Total Tax', amount: totalTaxValue }] : []), { label: 'Return Total', amount: returnTotal }, { label: 'Total', amount: totalBeforeRound }, { label: 'Rounding', amount: roundOff }, ...(roundOff !== 0 ? [{ label: 'Final Total', amount: resolvedFinalTotal }] : []), ...(isPoSPrint ? [{ label: 'Due', amount: dueFromInvoice }] : []) ]; const lines = totalLines .filter((line: any) => line?.label === 'Due' || parseNumber(line?.amount) !== 0) .map((line: any) => ({ label: line.label, value: formatAmountFixed2(parseNumber(line?.amount)) })); const hasData = lines.some((line: any) => hasDisplayValue(line?.value)); return { lines, hasData }; } function resolveDueInfo(invoiceData: any, totals: any) { const paid = readNumericValue(invoiceData, ['Paid', 'PaidAmount', 'PaidAmt', 'ReceivedAmount', 'RecAmt']); const dueFromInvoice = readNumericValue(invoiceData, ['Due', 'DueAmount', 'DueAmt', 'Balance', 'BalAmt']); const finalTotal = parseNumber(firstValue(invoiceData?.Total, invoiceData?.FinalTotal, invoiceData?.GrandTotal, invoiceData?.NetTotal)); const due = dueFromInvoice !== 0 ? dueFromInvoice : Math.max(0, finalTotal - paid); return { paid, due, hasData: paid !== 0 || due !== 0 || !!totals?.hasData }; } function resolveQrPayload(invoiceData: any, printConfigData: any = {}) { if (!isTruthy(printConfigData?.ShowPayQrCode)) { return ''; } return firstValue( invoiceData?.Entity?.UPI, invoiceData?.QRText, invoiceData?.QrText, invoiceData?.QRCode, invoiceData?.QRData, invoiceData?.UPIId, invoiceData?.Bank?.UPI, invoiceData?.BankDetails?.UPI ); } function resolveUpiPhone(invoiceData: any) { return firstValue( invoiceData?.UPIPhone, invoiceData?.Entity?.UPIPhone, invoiceData?.Bank?.UPIPhone, invoiceData?.BankDetails?.UPIPhone, invoiceData?.Entity?.Bank?.UPIPhone, invoiceData?.Entity?.BankDetails?.UPIPhone ); } function formatAmountFixed2(value: number) { if (!Number.isFinite(value)) { return '0.00'; } return value.toFixed(2); } function parseNumber(value: any) { if (value === undefined || value === null) { return 0; } if (typeof value === 'number') { return Number.isFinite(value) ? value : 0; } const raw = String(value).trim(); if (!raw) { return 0; } const cleaned = raw.replace(/\s/g, '').replace(/[^0-9,.-]/g, ''); if (!cleaned) { return 0; } const commaCount = (cleaned.match(/,/g) || []).length; const dotCount = (cleaned.match(/\./g) || []).length; let normalized = cleaned; if (commaCount > 0 && dotCount > 0) { const lastCommaIndex = cleaned.lastIndexOf(','); const lastDotIndex = cleaned.lastIndexOf('.'); const decimalSeparator = lastCommaIndex > lastDotIndex ? ',' : '.'; if (decimalSeparator === ',') { normalized = cleaned.replace(/\./g, '').replace(',', '.'); } else { normalized = cleaned.replace(/,/g, ''); } } else if (commaCount > 0) { const isCommaDecimal = /^-?\d+,\d{1,2}$/.test(cleaned); normalized = isCommaDecimal ? cleaned.replace(',', '.') : cleaned.replace(/,/g, ''); } const numeric = Number(normalized); return Number.isFinite(numeric) ? numeric : 0; } function resolveInvoiceTaxMode(invoiceData: any) { return String(invoiceData?.Settings?.Tax ?? '').trim().toUpperCase(); } function shouldHideTaxDetails(taxMode: string) { return taxMode === 'BS' || taxMode === 'NO'; } function formatTaxRate(value: string) { const normalized = firstValue(value); if (!normalized) { return '-'; } if (normalized.includes('%')) { return normalized; } const numericValue = Number(normalized); return Number.isFinite(numericValue) ? `${normalized}%` : normalized; } function resolveTotalInWords(invoiceData: any) { const roundedTotal = parseNumber(firstValue(invoiceData?.CustRoundedTotal || invoiceData?.RoundedTotal)); if (roundedTotal > 0) { return convertAmountToWordsINR(roundedTotal); } return firstValue( invoiceData?.TotalInWords, invoiceData?.AmtInWords, invoiceData?.AmountInWords ); } function convertAmountToWordsINR(amount: number) { if (!Number.isFinite(amount)) { return ''; } const absoluteAmount = Math.abs(amount); const integerPart = Math.floor(absoluteAmount); const decimalPart = Math.round((absoluteAmount - integerPart) * 100); const rupeesInWords = numberToWordsIndian(integerPart); const paiseInWords = decimalPart > 0 ? ` And ${numberToWordsIndian(decimalPart)} Paise` : ''; const signText = amount < 0 ? 'Minus ' : ''; return `${signText}${rupeesInWords} Rupee(s)${paiseInWords} Only`; } function numberToWordsIndian(num: number): string { if (num === 0) { return 'Zero'; } const ones = ['', 'One', 'Two', 'Three', 'Four', 'Five', 'Six', 'Seven', 'Eight', 'Nine', 'Ten', 'Eleven', 'Twelve', 'Thirteen', 'Fourteen', 'Fifteen', 'Sixteen', 'Seventeen', 'Eighteen', 'Nineteen']; const tens = ['', '', 'Twenty', 'Thirty', 'Forty', 'Fifty', 'Sixty', 'Seventy', 'Eighty', 'Ninety']; const belowThousand = (n: number): string => { let words = ''; if (n >= 100) { words += `${ones[Math.floor(n / 100)]} Hundred`; n %= 100; if (n > 0) { words += ' And '; } } if (n >= 20) { words += tens[Math.floor(n / 10)]; if (n % 10 > 0) { words += ` ${ones[n % 10]}`; } } else if (n > 0) { words += ones[n]; } return words.trim(); }; const units = [ { value: 10000000, label: 'Crore' }, { value: 100000, label: 'Lakh' }, { value: 1000, label: 'Thousand' } ]; let n = Math.floor(num); const parts: string[] = []; for (const unit of units) { if (n >= unit.value) { const unitCount = Math.floor(n / unit.value); parts.push(`${belowThousand(unitCount)} ${unit.label}`.trim()); n %= unit.value; } } if (n > 0) { parts.push(belowThousand(n)); } return parts.join(' ').replace(/\s+/g, ' ').trim(); }