Untitled
unknown
plain_text
9 months ago
12 kB
3
Indexable
import PDFDocument from 'pdfkit';
import fs from 'fs';
interface Invoice {
companyName?: string;
invoice?: string;
billTo?: string;
billToInfo?: Record<string, any>;
shipTo?: string;
shipToInfo?: Record<string, any>;
paymentDate?: string;
paymentDateInfo?: string;
paymentTerms?: string;
paymentTermsInfo?: string;
dueDate?: string;
dueDateInfo?: string;
poNumber?: string;
poNumberInfo?: string;
headerTextColor?: string;
headerBgColor?: string;
amountDue?: string;
calc?: {
due?: number;
subTotal?: number;
};
item?: string;
quantity?: string;
rate?: string;
amount?: string;
rows?: Array<{
title: string;
quantity: number;
rate: number;
}>;
subtotal?: string;
shipping?: string;
shippingInfo?: number;
tax?: string;
taxInfo?: number;
discount?: string;
discountInfo?: number;
total?: string;
totalInfo?: number;
amountPaid?: string;
amountPaidInfo?: number;
note?: string;
noteInfo?: string;
terms?: string;
termsInfo?: string;
}
function createInvoice(invoice: Invoice, outputPath: string): void {
if (!invoice || typeof invoice !== 'object') {
throw new Error('Invalid input invoice provided.');
}
console.log(invoice, 'invoice');
const doc = new PDFDocument({ margin: 35, size: 'A4' });
doc.pipe(fs.createWriteStream(outputPath));
doc.font('Helvetica');
generateHeader(doc, invoice);
const dynamicSectionsEndY = generateDynamicSections(doc, invoice, 50, 140);
const tableStartY = Math.max(dynamicSectionsEndY, 300);
generateInvoiceDetails(doc, invoice);
generateInvoiceTable(doc, invoice, tableStartY);
doc.end();
}
function generateHeader(doc: PDFDocument, invoice: Invoice): void {
doc
.image('rootdevs-logo.png', 20, 25, { width: 160, fit: [100, 100] })
.fillColor('#444444')
.fontSize(10)
.text(`${invoice?.companyName}`, 30, 70, {
width: 350,
})
.fontSize(25)
.text(`${invoice?.invoice}`, 20, 36, { align: 'right' })
.fontSize(14)
.moveDown();
}
function generateDynamicSections(doc: PDFDocument, data: Invoice, startX: number, startY: number): number {
const sectionSpacing = 152;
let currentX = startX;
let maxY = startY;
const extraSpacing = 4; // Add extra space between fields
// Helper function to flatten nested objects
const flattenObject = (obj: Record<string, any>, parentKey = ''): Record<string, any> => {
return Object.keys(obj).reduce((acc, key) => {
const newKey = parentKey ? `${parentKey}.${key}` : key;
if (typeof obj[key] === 'object' && obj[key] !== null) {
Object.assign(acc, flattenObject(obj[key], newKey));
} else {
acc[newKey] = obj[key];
}
return acc;
}, {});
};
// Process billToInfo
const billToInfo = flattenObject(data.billToInfo || {});
let currentY = startY;
// Write section name for Bill To
doc
.fillColor('#666666')
.font('Helvetica')
.fontSize(11)
.text(`${data.billTo}`, currentX - 20, currentY);
currentY += 15;
// Process each field in billToInfo
Object.keys(billToInfo).forEach((key) => {
const value = billToInfo[key];
// Write field and value with text wrapping
const wrappedText = `${key.charAt(0).toUpperCase() + key.slice(1)}: ${value}`;
doc
.fillColor('#333333')
.font('Helvetica')
.fontSize(10)
.text(wrappedText, currentX - 20, currentY, {
width: 160, // Wrap text to the specified width
lineBreak: true, // Enable line wrapping for long text
});
// Calculate the height of the text block
const textHeight = doc.heightOfString(wrappedText, { width: 160 });
// Update currentY based on the actual height of the text block
currentY += textHeight + extraSpacing; // Add extra space after each field
});
// Update maxY based on the last field's Y position
maxY = Math.max(maxY, currentY);
// Move to the next section (Ship To)
currentX += sectionSpacing;
// Process shipToInfo
const shipToInfo = flattenObject(data.shipToInfo || {});
currentY = startY;
// Write section name for Ship To
doc
.fillColor('#666666')
.font('Helvetica')
.fontSize(11)
.text(`${data.shipTo}`, currentX, currentY);
currentY += 15;
// Process each field in shipToInfo
Object.keys(shipToInfo).forEach((key) => {
const value = shipToInfo[key];
// Write field and value with text wrapping
const wrappedText = `${key.charAt(0).toUpperCase() + key.slice(1)}: ${value}`;
doc
.fillColor('#333333')
.font('Helvetica')
.fontSize(10)
.text(wrappedText, currentX, currentY, {
width: 160, // Wrap text to the specified width
lineBreak: true, // Enable line wrapping for long text
});
// Calculate the height of the text block
const textHeight = doc.heightOfString(wrappedText, { width: 160 });
// Update currentY based on the actual height of the text block
currentY += textHeight + extraSpacing; // Add extra space after each field
});
// Update maxY based on the last field's Y position
maxY = Math.max(maxY, currentY);
return maxY;
}
function generateInvoiceDetails(doc: PDFDocument, invoice: Invoice): void {
const detailsTop = 130;
let currentY = detailsTop;
// Helper function to check if a value is empty
const isEmpty = (value: any): boolean => {
return value === null || value === undefined || value === '';
};
// Render fields only if both the label and value are not empty
if (!isEmpty(invoice?.paymentDate) && !isEmpty(invoice.paymentDateInfo)) {
doc
.fontSize(10)
.fillColor('#666666')
.text(`${invoice.paymentDate}`, 0, currentY, {
width: 470,
align: 'right',
})
.fillColor('#000000')
.text(formatDate(new Date(invoice.paymentDateInfo!)), 0, currentY, {
width: 550,
align: 'right',
});
currentY += 25; // Move to the next line
}
if (!isEmpty(invoice?.paymentTerms) && !isEmpty(invoice.paymentTermsInfo)) {
doc
.fontSize(10)
.fillColor('#666666')
.text(`${invoice.paymentTerms}`, 0, currentY, {
width: 470,
align: 'right',
})
.fillColor('#000000')
.text(`${invoice.paymentTermsInfo}`, 0, currentY, {
width: 550,
align: 'right',
});
currentY += 25; // Move to the next line
}
if (!isEmpty(invoice?.dueDate) && !isEmpty(invoice.dueDateInfo)) {
doc
.fontSize(10)
.fillColor('#666666')
.text(`${invoice.dueDate}`, 0, currentY, {
width: 470,
align: 'right',
})
.fillColor('#000000')
.text(formatDate(new Date(invoice.dueDateInfo!)), 0, currentY, {
width: 550,
align: 'right',
});
currentY += 25; // Move to the next line
}
if (!isEmpty(invoice?.poNumber) && !isEmpty(invoice.poNumberInfo)) {
doc
.fontSize(10)
.fillColor('#666666')
.text(`${invoice.poNumber}`, 0, currentY, {
width: 470,
align: 'right',
})
.fillColor('#000000')
.text(`${invoice.poNumberInfo}`, 0, currentY, {
width: 550,
align: 'right',
});
currentY += 25; // Move to the next line
}
// Draw the line
doc
.fillColor(`${invoice.headerTextColor?.slice(0, 7)}`)
.lineWidth(20)
.lineCap('round')
.moveTo(560, currentY + 4)
.lineTo(390, currentY + 4)
.strokeColor(`${invoice.headerBgColor?.slice(0, 7)}`)
.stroke();
// Render amount due only if it is not empty
if (!isEmpty(invoice?.amountDue) && !isEmpty(invoice.calc?.due)) {
doc
.font('Helvetica-Bold')
.text(`${invoice.amountDue}`, 0, currentY, {
width: 470,
align: 'right',
})
.text(`${invoice.calc.due}`, 0, currentY, {
width: 550,
align: 'right',
});
}
}
function generateInvoiceTable(doc: PDFDocument, invoice: Invoice, tableStartY: number): void {
let i;
const invoiceTableTop = 320;
doc.font('Helvetica-Bold');
doc
.lineWidth(25)
.lineCap('round')
.moveTo(25, 318)
.lineTo(565, 318)
.strokeColor(`${invoice.headerBgColor?.slice(0, 7)}`)
.stroke();
doc.fillColor(`${invoice.headerTextColor?.slice(0, 7)}`);
generateTableRow(
doc,
invoiceTableTop - 5,
`${invoice.item}`,
`${invoice.quantity}`,
`${invoice.rate}`,
`${invoice.amount}`
);
doc.fillColor('#000000');
doc.font('Helvetica');
for (i = 0; i < (invoice.rows?.length || 0); i++) {
const item = invoice.rows![i];
const position = invoiceTableTop + (i + 1) * 24;
generateTableRow(
doc,
position,
item.title,
item.quantity,
item.rate,
item.quantity * item.rate
);
}
const subtotalPosition = invoiceTableTop + (i + 1) * 27;
generateHr(doc, subtotalPosition - 15);
generateTableRow(
doc,
subtotalPosition,
'',
'',
`${invoice.subtotal}`,
formatCurrency(invoice.calc?.subTotal || 0)
);
let currentPosition = subtotalPosition + 20;
if (invoice.shippingInfo) {
generateTableRow(
doc,
currentPosition,
'',
'',
`${invoice.shipping}`,
formatCurrency(invoice.shippingInfo)
);
currentPosition += 20;
}
if (invoice.taxInfo) {
generateTableRow(
doc,
currentPosition,
'',
'',
`${invoice.tax}`,
formatCurrency(invoice.taxInfo)
);
currentPosition += 20;
}
if (invoice.discountInfo) {
generateTableRow(
doc,
currentPosition,
'',
'',
`${invoice.discount}`,
formatCurrency(invoice.discountInfo)
);
currentPosition += 20;
}
if (invoice.totalInfo) {
generateTableRow(
doc,
currentPosition,
'',
'',
`${invoice.total}`,
formatCurrency(invoice.totalInfo)
);
currentPosition += 20;
}
if (invoice.amountPaidInfo) {
generateTableRow(
doc,
currentPosition,
'',
'',
`${invoice.amountPaid}`,
formatCurrency(invoice.amountPaidInfo)
);
currentPosition += 20;
}
const duePosition = currentPosition;
doc.font('Helvetica-Bold');
generateTableRow(
doc,
duePosition,
'',
'',
`${invoice.amountDue}`,
formatCurrency(invoice.calc?.due || 0)
);
const tableHeight = duePosition + 20;
generateNotesAndTerms(doc, invoice, tableHeight);
}
function generateTableRow(doc: PDFDocument, y: number, item: string, unitCost: string, quantity: string, lineTotal: string): void {
doc
.fontSize(10)
.text(item, 30, y)
.text(unitCost, 250, y, { width: 90, align: 'right' })
.text(quantity, 350, y, { width: 90, align: 'right' })
.text(lineTotal, 420, y, { align: 'right' });
}
function generateNotesAndTerms(doc: PDFDocument, invoice: Invoice, tableHeight: number): void {
let notePosition = tableHeight + 20;
const lorem1Height = doc.heightOfString(`${invoice?.noteInfo}`, {
width: 500,
});
if (invoice.noteInfo) {
doc.fontSize(10).text(`${invoice.note}`, 25, notePosition);
doc.font('Helvetica').text(`${invoice.noteInfo}`, 25, notePosition + 15);
}
const termsPosition = notePosition + lorem1Height + 20;
if (invoice.termsInfo) {
doc
.fontSize(10)
.font('Helvetica-Bold')
.text(`${invoice.terms}`, 25, termsPosition);
doc.font('Helvetica').text(`${invoice.termsInfo}`, 25, termsPosition + 15);
}
}
function formatCurrency(amount: number): string {
return `$${Number(amount)?.toFixed(2)}`;
}
function generateHr(doc: PDFDocument, y: number): void {
doc
.strokeColor('#06243E')
.lineWidth(0.1)
.moveTo(27, y)
.lineTo(562, y)
.stroke();
}
function formatDate(date: Date): string {
const day = date.getDate();
const month = date.getMonth() + 1;
const year = date.getFullYear();
return `${year}/${month}/${day}`;
}
export { createInvoice };Editor is loading...
Leave a Comment