import { Multiply } from '../math-operations'; import { pdfTableStyleConfig } from './pdf-table.config'; import { buildHeaderRows, GetTableHeader, getRenderableColumns } from './pdf-table.header'; import { buildDataRow, getCellAlignment, getFirstFieldValue, getNumericAlignment } from './pdf-table.row'; const CELL_PADDING_LEFT = 4; const CELL_PADDING_RIGHT = 4; const BORDER_WIDTH = 0.5; export function buildInvoiceTableData(PDFInvoiceData: any, PrintConfig: any, availableWidth: number) { const items = Array.isArray(PDFInvoiceData?.Items) ? PDFInvoiceData.Items : []; const services = Array.isArray(PDFInvoiceData?.Ops) ? PDFInvoiceData.Ops : []; const sType = PDFInvoiceData?.SType ?? ''; const taxMode = resolveInvoiceTaxMode(PDFInvoiceData); const showSectionSummaryRows = !resolveIsPoSPrint(PrintConfig); const tableConfig = normalizeTableConfig(PrintConfig?.TableConfig, PDFInvoiceData); const headerConfig = GetTableHeader(tableConfig, taxMode)[0]; const renderColumns = getRenderableColumns(headerConfig, sType); const headerRows = buildHeaderRows(headerConfig, sType); const fixedTo = resolveFixedTo(PrintConfig?.fixedTo); let body; let widths: number[]; if (renderColumns.length === 0) { const fallbackHeaderRow = headerRows[0]; const descriptionColumnIndex = getDescriptionColumnIndexFromHeaders(fallbackHeaderRow); const itemTotals = calculateSectionTotals(items); const serviceTotals = calculateSectionTotals(services); const itemSectionRows = items.length > 0 ? buildSectionRows({ rows: items, columnsOrHeaders: fallbackHeaderRow, descriptionColumnIndex, label: 'Items', buildRow: (item: any, index: number) => buildFallbackDataRow(item, index, fallbackHeaderRow, fixedTo), buildTotalRow: () => buildFallbackSectionTotalRow(fallbackHeaderRow, 'Items Total', itemTotals, fixedTo), showSectionSummaryRows }) : []; const serviceSectionRows = services.length > 0 ? buildSectionRows({ rows: services, columnsOrHeaders: fallbackHeaderRow, descriptionColumnIndex, label: 'Services', buildRow: (service: any, index: number) => buildFallbackDataRow(service, index, fallbackHeaderRow, fixedTo), buildTotalRow: () => buildFallbackSectionTotalRow(fallbackHeaderRow, 'Services Total', serviceTotals, fixedTo), showSectionSummaryRows }) : []; body = [ fallbackHeaderRow, ...itemSectionRows, ...serviceSectionRows ]; widths = fitWidthsToPage(fallbackHeaderRow.map((col: any) => col?.width), availableWidth, fallbackHeaderRow.length); } else { const descriptionColumnIndex = getDescriptionColumnIndexFromColumns(renderColumns); const itemTotals = calculateSectionTotals(items); const serviceTotals = calculateSectionTotals(services); const itemSectionRows = items.length > 0 ? buildSectionRows({ rows: items, columnsOrHeaders: renderColumns, descriptionColumnIndex, label: 'Items', buildRow: (item: any, index: number) => buildDataRow(item, index, renderColumns, fixedTo), buildTotalRow: () => buildSectionTotalRow(renderColumns, 'Items Total', itemTotals, fixedTo), showSectionSummaryRows }) : []; const serviceSectionRows = services.length > 0 ? buildSectionRows({ rows: services, columnsOrHeaders: renderColumns, descriptionColumnIndex, label: 'Services', buildRow: (service: any, index: number) => buildDataRow(service, index, renderColumns, fixedTo), buildTotalRow: () => buildSectionTotalRow(renderColumns, 'Services Total', serviceTotals, fixedTo), showSectionSummaryRows }) : []; body = [ ...headerRows, ...itemSectionRows, ...serviceSectionRows ]; widths = fitWidthsToPage(renderColumns.map((col: any) => col?.width), availableWidth, renderColumns.length); } return { headerRows, widths, body }; } export function getAvailablePageWidth( pageSize: 'A4' | 'LETTER' | 'LEGAL' | { width: number; height: number }, orientation: 'portrait' | 'landscape', margins: [number, number, number, number] ) { const pageWidths: Record<'A4' | 'LETTER' | 'LEGAL', { portrait: number; landscape: number }> = { A4: { portrait: 595.28, landscape: 841.89 }, LETTER: { portrait: 612, landscape: 792 }, LEGAL: { portrait: 612, landscape: 1008 } }; const fullWidth = typeof pageSize === 'object' ? (Number.isFinite(pageSize?.width) ? Number(pageSize.width) : pageWidths.A4[orientation]) : pageWidths[pageSize][orientation]; return Math.max(1, fullWidth - margins[0] - margins[2]); } export function calculateSectionTotals(rows: any[]) { return rows.reduce((acc: any, row: any) => { const sign = row?.Ret ? -1 : 1; const quantity = row?.Qty == null ? 1 : readNumericValue(row, ['Qty']); acc.qty += sign * quantity; const taxableAmt: number = Number(Multiply( quantity, readNumericValue(row, ['UnPr', 'Pr', 'UnCo']) )); acc.priceTotal += sign * (Number.isFinite(taxableAmt) ? taxableAmt : 0); acc.discountAmount += sign * readNumericValue(row, ['Disc']); acc.cgstAmount += sign * readNumericValue(row, ['CGSTAmt', 'CGST']); acc.sgstAmount += sign * readNumericValue(row, ['SGSTAmt', 'SGST']); acc.igstAmount += sign * readNumericValue(row, ['IGSTAmt', 'IGST']); acc.lineTotal += sign * readNumericValue(row, ['LineTotal']); return acc; }, { qty: 0, priceTotal: 0, discountAmount: 0, cgstAmount: 0, sgstAmount: 0, igstAmount: 0, lineTotal: 0 }); } export function readNumericValue(row: any, fields: string[]) { const value = getFirstFieldValue(row, fields); if (!value) { return 0; } const numeric = Number(String(value).replace(/,/g, '')); return Number.isFinite(numeric) ? numeric : 0; } export function formatAmount(value: number, fixedTo: number = 2) { if (!Number.isFinite(value)) { return ''; } return value.toFixed(fixedTo); } function fitWidthsToPage(rawWidths: any[], availableWidth: number, columnCount: number) { const tableOffsets = getTableHorizontalOffsets(columnCount); const availableContentWidth = Math.max(1, availableWidth - tableOffsets); const weights = rawWidths.map((width: any) => toWeight(width)); const totalWeight = weights.reduce((sum: number, weight: number) => sum + weight, 0) || 1; const widths = weights.map((weight: number) => Number(((weight / totalWeight) * availableContentWidth).toFixed(2))); const usedWidth = widths.reduce((sum: number, width: number) => sum + width, 0); const diff = Number((availableContentWidth - usedWidth).toFixed(2)); if (widths.length > 0 && diff !== 0) { widths[widths.length - 1] = Number((widths[widths.length - 1] + diff).toFixed(2)); } return widths; } function toWeight(width: any) { return typeof width === 'number' && Number.isFinite(width) && width > 0 ? width : 1; } function getTableHorizontalOffsets(columnCount: number) { if (columnCount <= 0) { return 0; } const totalPadding = columnCount * (CELL_PADDING_LEFT + CELL_PADDING_RIGHT); const totalBorders = (columnCount + 1) * BORDER_WIDTH; return totalPadding + totalBorders; } function buildSectionLabelRow(columnCount: number, descriptionColumnIndex: number, label: string) { const normalizedDescriptionIndex = descriptionColumnIndex >= 0 ? descriptionColumnIndex : 0; return Array.from({ length: columnCount }, (_: unknown, index: number) => { if (index === normalizedDescriptionIndex) { return { text: label, bold: true, fontSize: pdfTableStyleConfig.bodyFontSize, color: pdfTableStyleConfig.bodyFontColor }; } return { text: '', fontSize: pdfTableStyleConfig.bodyFontSize, color: pdfTableStyleConfig.bodyFontColor }; }); } function getDescriptionColumnIndexFromColumns(columns: any[]) { return columns.findIndex((column: any) => column.type === 'normal' && column.header?.text === 'Description'); } function getDescriptionColumnIndexFromHeaders(headers: any[]) { return headers.findIndex((header: any) => header.text === 'Description'); } function buildFallbackDataRow(item: any, index: number, fallbackHeaderRow: any[], fixedTo: number) { return fallbackHeaderRow.map((header: any) => { if (header.text === 'CGST') { if (header.taxKind === 'rate') { const value = getFirstFieldValue(item, ['CGSTPerc']); const renderedValue = formatNumericLikeValue(value, fixedTo, true); return { text: renderedValue, fontSize: pdfTableStyleConfig.bodyFontSize, color: pdfTableStyleConfig.bodyFontColor, alignment: getNumericAlignment(renderedValue) }; } else if (header.taxKind === 'amount') { const value = getDisplayValueForFallbackRow(item, ['CGSTAmt'], true); const renderedValue = formatNumericLikeValue(value, fixedTo); return { text: renderedValue, fontSize: pdfTableStyleConfig.bodyFontSize, color: pdfTableStyleConfig.bodyFontColor, alignment: getNumericAlignment(renderedValue) }; } } if (header.text === 'SGST') { if (header.taxKind === 'rate') { const value = getFirstFieldValue(item, ['SGSTPerc']); const renderedValue = formatNumericLikeValue(value, fixedTo, true); return { text: renderedValue, fontSize: pdfTableStyleConfig.bodyFontSize, color: pdfTableStyleConfig.bodyFontColor, alignment: getNumericAlignment(renderedValue) }; } else if (header.taxKind === 'amount') { const value = getDisplayValueForFallbackRow(item, ['SGSTAmt'], true); const renderedValue = formatNumericLikeValue(value, fixedTo); return { text: renderedValue, fontSize: pdfTableStyleConfig.bodyFontSize, color: pdfTableStyleConfig.bodyFontColor, alignment: getNumericAlignment(renderedValue) }; } } if (header.text === 'IGST %') { const value = getFirstFieldValue(item, ['IGSTPerc']); const renderedValue = formatNumericLikeValue(value, fixedTo, true); return { text: renderedValue, fontSize: pdfTableStyleConfig.bodyFontSize, color: pdfTableStyleConfig.bodyFontColor, alignment: getNumericAlignment(renderedValue) }; } if (header.text === 'IGST Amt') { const value = getDisplayValueForFallbackRow(item, ['IGSTAmt'], true); const renderedValue = formatNumericLikeValue(value, fixedTo); return { text: renderedValue, fontSize: pdfTableStyleConfig.bodyFontSize, color: pdfTableStyleConfig.bodyFontColor, alignment: getNumericAlignment(renderedValue) }; } if (header.text === 'S.No') { const value = String(index + 1); return { text: value, fontSize: pdfTableStyleConfig.bodyFontSize, color: pdfTableStyleConfig.bodyFontColor, alignment: getNumericAlignment(value) }; } const mainValue = formatNumericLikeValue( getDisplayValueForFallbackRow(item, header.dbFields, shouldNegateFallbackHeaderValue(item, header.text)), fixedTo, shouldSkipFixedTo(header) ); const stackedLines = (header.stackFields ?? []) .map((stackField: any) => { const stackValue = formatNumericLikeValue(getFirstFieldValue(item, stackField.dbFields), fixedTo, shouldSkipFixedTo(stackField)); if (!stackValue) { return null; } return { text: `${stackField.text}: ${stackValue}`, fontSize: pdfTableStyleConfig.stackFieldFontSize, color: pdfTableStyleConfig.bodyFontColor, italics: true }; }) .filter((line: any) => !!line); if (stackedLines.length === 0) { return { text: mainValue, fontSize: pdfTableStyleConfig.bodyFontSize, color: pdfTableStyleConfig.bodyFontColor, alignment: getCellAlignment(header.text, mainValue) }; } return { stack: [ { text: mainValue, fontSize: pdfTableStyleConfig.bodyFontSize, color: pdfTableStyleConfig.bodyFontColor }, ...stackedLines ] }; }); } function buildSectionTotalRow(columns: any[], label: string, totals: any, fixedTo: number) { return columns.map((column: any) => { if (column.type === 'tax') { if (column.dbField === 'CGST' && column.taxKind === 'amount') { return buildTotalValueCell(formatAmount(totals.cgstAmount, fixedTo)); } if (column.dbField === 'SGST' && column.taxKind === 'amount') { return buildTotalValueCell(formatAmount(totals.sgstAmount, fixedTo)); } if (column.dbField === 'IGST' && column.taxKind === 'amount') { return buildTotalValueCell(formatAmount(totals.igstAmount, fixedTo)); } if (column.dbField === 'GST' && column.taxKind === 'amount') { return buildTotalValueCell(formatAmount(totals.cgstAmount + totals.sgstAmount + totals.igstAmount, fixedTo)); } return buildTotalValueCell(''); } const headerText = column.header?.text ?? ''; if (headerText === 'Description') { return buildTotalLabelCell(label); } if (headerText === 'Qty') { return buildTotalValueCell(formatQty(totals.qty), headerText); } if (headerText === 'Price') { return buildTotalValueCell(formatAmount(totals.priceTotal, fixedTo)); } if (isDiscountAmountHeader(headerText)) { return buildTotalValueCell(formatAmount(totals.discountAmount, fixedTo)); } if (headerText === 'Line Total') { return buildTotalValueCell(formatAmount(totals.lineTotal, fixedTo)); } return buildTotalValueCell(''); }); } function buildFallbackSectionTotalRow(fallbackHeaderRow: any[], label: string, totals: any, fixedTo: number) { return fallbackHeaderRow.map((header: any) => { if (header.text === 'CGST' && header.taxKind === 'amount') { return buildTotalValueCell(formatAmount(totals.cgstAmount, fixedTo)); } if (header.text === 'SGST' && header.taxKind === 'amount') { return buildTotalValueCell(formatAmount(totals.sgstAmount, fixedTo)); } if (header.text === 'IGST Amt') { return buildTotalValueCell(formatAmount(totals.igstAmount, fixedTo)); } if (header.text === 'Description') { return buildTotalLabelCell(label); } if (header.text === 'Qty') { return buildTotalValueCell(formatQty(totals.qty), header.text); } if (header.text === 'Price') { return buildTotalValueCell(formatAmount(totals.priceTotal, fixedTo)); } if (isDiscountAmountHeader(header.text)) { return buildTotalValueCell(formatAmount(totals.discountAmount, fixedTo)); } if (header.text === 'Line Total') { return buildTotalValueCell(formatAmount(totals.lineTotal, fixedTo)); } return buildTotalValueCell(''); }); } function buildTotalLabelCell(text: string) { return { text, bold: true, fontSize: pdfTableStyleConfig.bodyFontSize, color: pdfTableStyleConfig.bodyFontColor }; } function buildTotalValueCell(text: string, headerText: string = '') { return { text, bold: true, fontSize: pdfTableStyleConfig.bodyFontSize, color: pdfTableStyleConfig.bodyFontColor, alignment: headerText ? getCellAlignment(headerText, text) : getNumericAlignment(text) }; } function formatQty(value: number) { if (!Number.isFinite(value)) { return ''; } return Number.isInteger(value) ? String(value) : stripTrailingZeros(value.toFixed(2)); } function stripTrailingZeros(value: string) { return value.replace(/\.?0+$/, ''); } function isDiscountAmountHeader(text: string) { return text === 'Disc Amount' || text === 'Adisc Amount'; } function resolveFixedTo(value: any) { const parsed = Number(value); if (!Number.isFinite(parsed) || parsed < 0) { return 2; } return Math.floor(parsed); } function resolveIsPoSPrint(printConfig: any) { return Boolean(printConfig?.IsPoSPrint ?? printConfig?.isPoSPrint); } function resolveInvoiceTaxMode(pdfInvoiceData: any) { return String(pdfInvoiceData?.Settings?.Tax ?? '').trim().toUpperCase(); } function normalizeTableConfig(tableConfig: any[], pdfInvoiceData: any) { if (!Array.isArray(tableConfig) || tableConfig.length === 0) { return Array.isArray(tableConfig) ? tableConfig : []; } if (pdfInvoiceData?.ShowDiscountColumn) { return tableConfig; } return tableConfig.map((column: any) => { if (!isDiscountColumnConfig(column)) { return column; } return { ...column, show: false, Show: false }; }); } function isDiscountColumnConfig(column: any) { const title = String(column?.title ?? column?.CustTitle ?? column?.Title ?? '').trim(); return title === 'Disc %' || title === 'Disc Amount' || title === 'Adisc Amount'; } function buildSectionRows({ rows, columnsOrHeaders, descriptionColumnIndex, label, buildRow, buildTotalRow, showSectionSummaryRows }: { rows: any[]; columnsOrHeaders: any[]; descriptionColumnIndex: number; label: string; buildRow: (row: any, index: number) => any; buildTotalRow: () => any; showSectionSummaryRows: boolean; }) { const sectionRows = rows.map((row: any, index: number) => buildRow(row, index)); if (!showSectionSummaryRows) { return sectionRows; } return [ buildSectionLabelRow(columnsOrHeaders.length, descriptionColumnIndex, label), ...sectionRows, buildTotalRow() ]; } function shouldNegateFallbackHeaderValue(item: any, headerText: string) { if (!item?.Ret) { return false; } const normalized = String(headerText ?? '').trim().toUpperCase(); return normalized === 'QTY' || normalized === 'OFFER QTY' || normalized === 'PRICE' || normalized === 'LINE TOTAL'; } function shouldSkipFixedTo(header: any) { const text = String(header?.text ?? '').trim().toUpperCase(); const normalizedText = text.replace(/\s+/g, ''); if (text === 'HSN/SAC' || text === 'BN' || text === 'BNO' || text.includes('%') || normalizedText === 'QTY' || normalizedText === 'OFFERQTY' || normalizedText === 'OFQTY' || normalizedText === 'MPN') { return true; } const dbFields = Array.isArray(header?.dbFields) ? header.dbFields : []; return dbFields.some((field: any) => { const normalized = String(field ?? '').trim().toUpperCase(); return normalized === 'BN' || normalized === 'BNO' || normalized === 'QTY' || normalized === 'OFQTY' || normalized === 'MPN' || normalized.endsWith('.BN') || normalized.endsWith('.QTY') || normalized.endsWith('.OFQTY') || normalized.endsWith('.MPN') || normalized.endsWith('PERC') || normalized.endsWith('.PERC'); }); } function getDisplayValueForFallbackRow(item: any, dbFields: string[] = [], shouldNegate: boolean = false) { const value = getFirstFieldValue(item, dbFields); if (!shouldNegate) { return value; } const normalized = Number(String(value ?? '').replace(/,/g, '').trim()); if (!Number.isFinite(normalized)) { return value; } return String(normalized === 0 ? 0 : -Math.abs(normalized)); } function formatNumericLikeValue(value: string, fixedTo: number, skipFixedTo: boolean = false) { if (skipFixedTo) { return value; } if (!value) { return value; } const normalized = Number(String(value).replace(/,/g, '').trim()); if (!Number.isFinite(normalized)) { return value; } return normalized.toFixed(fixedTo); }