Untitled
unknown
plain_text
8 months ago
16 kB
4
Indexable
import fs from "fs";
import path from "path";
import PDFDocument from "pdfkit";
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;
waterMark?: string;
amountDue?: string;
calc?: {
due?: number;
subTotal?: number;
grandTotal?: any;
};
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;
waterMarkOpacity: number;
note?: string;
noteInfo?: string;
terms?: string;
termsInfo?: string;
currency?: string;
discountInMoney?: number;
taxInMoney?: number;
shippingInMoney?: number;
calculationModes: { shipping: string; discount: string; tax: string };
invoiceType: "default" | "custom";
logo?: string;
header?: string;
status: string;
footer?: string;
invoiceNum?: string;
watermark?: string;
}
async function createDefaultInvoice(
invoice: Invoice,
outputPath: string
): Promise<void> {
if (!invoice || typeof invoice !== "object") {
throw new Error("Invalid input invoice provided.");
}
let mInvoice = { ...invoice };
["user", "createdAt", "updatedAt", "id", "type"].forEach((key) => {
delete mInvoice?.billToInfo?.[key];
delete mInvoice?.shipToInfo?.[key];
});
const doc = new PDFDocument({ margin: 35, size: "A4" });
const stream = fs.createWriteStream(outputPath);
doc.pipe(stream);
doc.font("Helvetica");
if (mInvoice?.logo) {
await generateHeader(
doc,
mInvoice,
path.join(process.cwd(), "files", "images", mInvoice.logo)
);
}
const dynamicSectionsEndY = await generateDynamicSections(
doc,
mInvoice,
50,
140
);
const tableStartY = Math.max(dynamicSectionsEndY, 300);
generateInvoiceDetails(doc, mInvoice);
generateInvoiceTable(doc, mInvoice, tableStartY);
doc.end();
return new Promise((resolve, reject) => {
stream.on("finish", resolve);
stream.on("error", reject);
});
}
async function generateHeader(
doc: PDFKit.PDFDocument,
invoice: Invoice,
logoPath: string
): Promise<void> {
doc
.image(logoPath, 20, 25, {
fit: [150, 60],
})
.fillColor("#444444");
if (invoice?.invoice) {
doc
.fillColor("#000000")
.fontSize(24)
.text(`${invoice?.invoice}`, 0, 75, { align: "right" })
.fontSize(14)
.moveDown();
}
if (invoice?.invoiceNum) {
doc
.fillColor("#000000")
.fontSize(12)
.text(`${invoice?.invoiceNum}`, 0, 105, { align: "right" })
.fontSize(14)
.moveDown();
}
if (invoice?.watermark) {
doc.opacity(Number(invoice?.waterMarkOpacity || 100) / 100);
doc.image(
path.join(process.cwd(), "files", "images", invoice?.watermark || ""),
163,
290,
{ width: 270, fit: [100, 100] }
);
}
doc.opacity(100 / 100);
}
async function generateDynamicSections(
doc: PDFKit.PDFDocument,
data: Invoice,
startX: number,
startY: number
): Promise<number> {
const sectionSpacing = 152;
let currentX = startX;
let maxY = startY;
const extraSpacing = 6; // Add extra space between fields
// Helper function to flatten nested objects
const flattenObject = (
obj: Record<string, any>,
parentKey = ""
): Record<string, any> => {
console.log(obj, "obj");
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 {
// @ts-ignore
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(10)
.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}`;
const wrappedText = `${value}`;
doc
.fillColor("#333333")
.font("Helvetica")
.fontSize(9)
.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(10)
.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}`;
const wrappedText = `${value}`;
doc
.fillColor("#333333")
.font("Helvetica")
.fontSize(9)
.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;
}
async function generateInvoiceDetails(
doc: PDFKit.PDFDocument,
invoice: Invoice
): Promise<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(
formatDateDDMMYYYY(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(formatDateDDMMYYYY(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
}
let stockY = currentY - 6;
// Calculate line length
const rectWidth = 185;
const rectHeight = 20;
const roundingRadius = 2;
doc.roundedRect(390, stockY, rectWidth, rectHeight, roundingRadius);
doc.fill(invoice.headerBgColor);
doc.font("Helvetica-Bold");
doc.fillColor(`${invoice?.headerTextColor}`);
// 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?.currency} ${invoice?.calc?.due}`, 0, currentY, {
width: 550,
align: "right",
});
}
}
async function generateInvoiceTable(
doc: PDFKit.PDFDocument,
invoice: Invoice,
_tableStartY: number
): Promise<void> {
let i;
const invoiceTableTop = 320;
let currentY = 308.5;
// Calculate line length
const rectWidth = 555;
const rectHeight = 20;
const roundingRadius = 2;
// Apply 10% rounding (or any dynamic rounding)
doc
.roundedRect(20, currentY, rectWidth, rectHeight, roundingRadius)
.fill(`${invoice.headerBgColor}`);
doc.font("Helvetica-Bold");
doc.fillColor(`${invoice.headerTextColor}`);
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 || 0).toString(),
`${invoice.currency} ${item.rate.toString()}`,
`${invoice.currency} ${item.quantity * item.rate}`.toString()
);
}
const subtotalPosition = invoiceTableTop + (i + 1) * 27;
doc.opacity(30 / 100);
generateHr(doc, subtotalPosition - 15);
doc.opacity(100 / 100);
generateTableRow(
doc,
subtotalPosition,
"",
"",
`${invoice.subtotal}`,
formatCurrency(invoice.calc?.subTotal || 0, invoice?.currency || "$")
);
let currentPosition = subtotalPosition + 20;
if (invoice.shippingInfo) {
generateTableRow(
doc,
currentPosition,
"",
"",
`${invoice.shipping} (${invoice.shippingInfo} ${invoice.calculationModes.shipping})`,
formatCurrency(invoice.shippingInMoney || 0, invoice?.currency || "$")
);
currentPosition += 20;
}
if (invoice.taxInfo) {
generateTableRow(
doc,
currentPosition,
"",
"",
`${invoice.tax} (${invoice.taxInfo} ${invoice.calculationModes.tax})`,
formatCurrency(invoice.taxInMoney || 0, invoice?.currency || "$")
);
currentPosition += 20;
}
if (invoice.discountInfo) {
generateTableRow(
doc,
currentPosition,
"",
"",
`${invoice.discount} (${invoice.discountInfo} ${invoice.calculationModes.discount})`,
formatCurrency(invoice.discountInMoney || 0, invoice?.currency || "$")
);
currentPosition += 20;
}
if (invoice.calc?.grandTotal) {
generateTableRow(
doc,
currentPosition,
"",
"",
`${invoice.total}`,
formatCurrency(invoice.calc?.grandTotal || 0, invoice?.currency || "$")
);
currentPosition += 20;
}
if (invoice.amountPaidInfo) {
generateTableRow(
doc,
currentPosition,
"",
"",
`${invoice.amountPaid}`,
formatCurrency(
invoice.status === "PAID"
? invoice.calc?.grandTotal
: invoice.amountPaidInfo || 0,
invoice?.currency || "$"
)
);
currentPosition += 20;
}
const duePosition = currentPosition;
doc.font("Helvetica-Bold");
generateTableRow(
doc,
duePosition,
"",
"",
`${invoice.amountDue}`,
formatCurrency(
invoice.status === "PAID" ? 0 : invoice.calc?.due || 0 || 0,
invoice?.currency || "$"
)
);
const tableHeight = duePosition + 20;
if (invoice.status === "PAID") {
doc.opacity(50 / 100 || 1);
doc.image("static/paid.png", 460, tableHeight + 40, {
fit: [150, 60],
});
}
doc.opacity(100 / 100 || 1);
generateNotesAndTerms(doc, invoice, tableHeight);
}
async function generateTableRow(
doc: PDFKit.PDFDocument,
y: number,
item: string,
unitCost: string,
quantity: string,
lineTotal: string
): Promise<void> {
doc
.fontSize(10)
.text(item, 30, y)
.text(unitCost, 270, y, { width: 90, align: "right" })
.text(quantity, 370, y, { width: 90, align: "right" })
.text(lineTotal, 430, y, { align: "right" });
}
async function generateNotesAndTerms(
doc: PDFKit.PDFDocument,
invoice: Invoice,
tableHeight: number
): Promise<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 + 30;
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, currency?: string): string {
return `${currency === "" ? "" : currency} ${Number(amount)?.toFixed(2)} `;
}
function generateHr(doc: PDFKit.PDFDocument, y: number): void {
doc
.strokeColor("#06243E")
.lineWidth(0.1)
.moveTo(27, y)
.lineTo(562, y)
.stroke();
}
function formatDateDDMMYYYY(date: Date): string {
const day = String(date.getDate()).padStart(2, "0");
const month = String(date.getMonth() + 1).padStart(2, "0");
const year = date.getFullYear();
return `${month}/${day}/${year}`;
}
export { createDefaultInvoice };
Editor is loading...
Leave a Comment