Untitled
unknown
typescript
8 months ago
15 kB
4
Indexable
import fs from "fs";
import path from "path";
import PDFDocument from "pdfkit";
interface Invoice {
invoice?: string;
billTo?: string;
billToInfo?: any;
shipTo?: string;
shipToInfo?: any;
paymentDate?: string;
paymentDateInfo?: string;
paymentTerms?: string;
calculationModes: any;
paymentTermsInfo?: string;
currency?: string;
dueDate?: string;
dueDateInfo?: string;
poNumber?: string;
poNumberInfo?: string;
amountDue?: string;
calc?: {
due?: number;
subTotal?: number;
grandTotal: any;
};
headerBgColor?: string;
headerTextColor?: string;
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;
waterMarkOpacity: number;
discount?: string;
discountInfo?: number;
total?: string;
totalInfo?: number;
amountPaid?: string;
amountPaidInfo?: number;
note?: string;
noteInfo?: string;
terms?: string;
termsInfo?: string;
invoiceType: "default" | "custom";
logo?: string;
header?: string;
footer?: string;
watermark?: string;
invoiceNum?: string;
status: string;
}
async function createCustomInvoice(
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" });
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: PDFKit.PDFDocument, invoice: Invoice): void {
if (invoice?.header) {
doc.image(
path.join(process.cwd(), "files", "images", invoice.header),
0,
0,
{ width: 600, height: 80 }
);
}
if (invoice?.footer)
doc.image(
path.join(process.cwd(), "files", "images", invoice.footer),
0,
805,
{ width: 600, fit: [100, 100] }
);
if (invoice?.watermark)
doc.opacity(Number(invoice?.waterMarkOpacity) / 100 || 1);
if (invoice?.watermark) {
doc.image(
path.join(process.cwd(), "files", "images", invoice?.watermark || ""),
163,
290,
{ width: 270, fit: [100, 100] }
);
}
doc.opacity(100 / 100);
// if (invoice?.invoice) {
// doc
// .fillColor("#000000")
// .fontSize(24)
// .text(`${invoice?.invoice}`, 0, 75, { align: "right" })
// .moveDown();
// }
// if (invoice?.invoiceNum) {
// doc
// .fillColor("#000000")
// .fontSize(12)
// .text(`${invoice?.invoiceNum}`, 0, 100, { align: "right" })
// .fontSize(14)
// .moveDown();
// }
}
function generateDynamicSections(
doc: PDFKit.PDFDocument,
data: Invoice,
startX: number,
startY: number
): number {
const sectionSpacing = 152;
let currentX = startX;
let maxY = startY;
const extraSpacing = 6;
const flattenObject = (obj: any, parentKey = ""): 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 {
// @ts-ignore
acc[newKey] = obj[key];
}
return acc;
}, {});
};
const billToInfo = flattenObject(data.billToInfo);
let currentY = startY;
doc
.fillColor("#666666")
.font("Helvetica")
.fontSize(11)
.text(`${data.billTo}`, currentX - 20, currentY);
currentY += 15;
Object.keys(billToInfo).forEach((key) => {
const value = billToInfo[key];
const wrappedText = `${
key.charAt(0).toUpperCase() + key.slice(1)
}: ${value}`;
doc
.fillColor("#333333")
.font("Helvetica")
.fontSize(10)
.text(wrappedText, currentX - 20, currentY, {
width: 160,
lineBreak: true,
});
const textHeight = doc.heightOfString(wrappedText, { width: 160 });
currentY += textHeight + extraSpacing;
});
maxY = Math.max(maxY, currentY);
currentX += sectionSpacing;
const shipToInfo = flattenObject(data.shipToInfo);
currentY = startY;
doc
.fillColor("#666666")
.font("Helvetica")
.fontSize(11)
.text(`${data.shipTo}`, currentX, currentY);
currentY += 15;
Object.keys(shipToInfo).forEach((key) => {
const value = shipToInfo[key];
const wrappedText = `${
key.charAt(0).toUpperCase() + key.slice(1)
}: ${value}`;
doc
.fillColor("#333333")
.font("Helvetica")
.fontSize(10)
.text(wrappedText, currentX, currentY, {
width: 160,
lineBreak: true,
});
const textHeight = doc.heightOfString(wrappedText, { width: 160 });
currentY += textHeight + extraSpacing;
});
maxY = Math.max(maxY, currentY);
return maxY;
}
function generateInvoiceDetails(
doc: PDFKit.PDFDocument,
invoice: Invoice
): void {
const detailsTop = 130;
let currentY = detailsTop;
const isEmpty = (value: any): boolean => {
return value === null || value === undefined || value === "";
};
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;
}
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;
}
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;
}
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;
}
let stockY = currentY - 6.1;
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}`);
if (!isEmpty(invoice?.amountDue) && !isEmpty(invoice.calc?.due)) {
doc
.font("Helvetica-Bold")
.text(`${invoice?.subtotal}`, 0, currentY, {
width: 470,
align: "right",
})
.text(`${invoice?.currency} ${invoice?.calc?.due}`, 0, currentY, {
width: 550,
align: "right",
});
}
}
function generateInvoiceTable(
doc: PDFKit.PDFDocument,
invoice: Invoice,
_tableStartY: number
): void {
let i;
const invoiceTableTop = 320;
let currentY = 308.5;
const rectWidth = 555;
const rectHeight = 20;
const roundingRadius = 2;
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; i++) {
const item = invoice.rows![i];
const position = invoiceTableTop + (i + 1) * 24;
if (position > doc.page.height - 50) {
doc.addPage();
currentY = 50;
}
generateTableRow(
doc,
position,
item.title,
item.quantity.toString(),
`${invoice.currency} ${item.rate.toString()}`,
`${invoice.currency} ${item.quantity * item.rate}`.toString(),
"center"
);
}
const subtotalPosition = invoiceTableTop + (i + 1) * 27;
doc.opacity(30 / 100);
generateHr(doc, subtotalPosition - 15);
doc.opacity(100 / 100);
generateTableInfo(
doc,
subtotalPosition,
"",
"",
`${invoice.subtotal}`,
formatCurrency(invoice.calc?.subTotal || 0, invoice?.currency || "$")
);
let currentPosition = subtotalPosition + 20;
if (invoice.shippingInfo) {
generateTableInfo(
doc,
currentPosition,
"",
"",
`${invoice.shipping} (${invoice.shippingInfo} ${invoice.calculationModes.shipping})`,
formatCurrency(invoice.shippingInfo || 0, invoice?.currency || "$")
);
currentPosition += 20;
}
if (invoice.taxInfo) {
generateTableInfo(
doc,
currentPosition,
"",
"",
`${invoice.tax} (${invoice.taxInfo} ${invoice.calculationModes.tax})`,
formatCurrency(invoice.taxInfo || 0, invoice?.currency || "$")
);
currentPosition += 20;
}
if (invoice.discountInfo) {
generateTableInfo(
doc,
currentPosition,
"",
"",
`${invoice.discount} (${invoice.discountInfo} ${invoice.calculationModes.discount})`,
formatCurrency(invoice.discountInfo || 0, invoice?.currency || "$")
);
currentPosition += 20;
}
if (invoice.calc?.grandTotal) {
generateTableInfo(
doc,
currentPosition,
"",
"",
`${invoice.total}`,
formatCurrency(invoice.calc?.grandTotal || 0, invoice?.currency || "$")
);
currentPosition += 20;
}
if (invoice.amountPaidInfo) {
generateTableInfo(
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");
generateTableInfo(
doc,
duePosition,
"",
"",
`${invoice.amountDue}`,
formatCurrency(
invoice.status === "PAID" ? 0 : invoice.calc?.due || 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);
}
function generateTableRow(
doc: PDFKit.PDFDocument,
y: number,
item: string,
unitCost: string,
quantity: string,
lineTotal: string,
_align: string = "right"
): void {
doc
.fontSize(10)
.text(item, 30, y)
.text(unitCost, 320, y, { width: 90, align: "center" })
.text(quantity, 420, y, { width: 90, align: "center" })
.text(lineTotal, 470, y, { align: "right" });
}
function generateTableInfo(
doc: PDFKit.PDFDocument,
y: number,
item: string,
unitCost: string,
quantity: string,
lineTotal: string
): void {
doc
.fontSize(10)
.text(item, 30, y)
.text(unitCost, 240, y, { width: 90, align: "right" })
.text(quantity, 360, y, { width: 90, align: "right" })
.text(lineTotal, 470, y, { align: "right" });
}
function generateNotesAndTerms(
doc: PDFKit.PDFDocument,
invoice: Invoice,
tableHeight: number
): void {
let notePosition = tableHeight + 20;
// Calculate the height required for the note and noteInfo
const noteHeight = doc.heightOfString(`${invoice?.noteInfo}`, {
width: 500,
});
// Check if there is enough space on the current page for the note and noteInfo
if (notePosition + noteHeight + 50 > doc.page.height) {
doc.addPage();
notePosition = 50; // Reset the position to the top of the new page
}
if (invoice.noteInfo) {
doc.fontSize(10).text(`${invoice.note}`, 25, notePosition);
doc.font("Helvetica").text(`${invoice.noteInfo}`, 25, notePosition + 15);
}
let termsPosition = notePosition + noteHeight + 20;
// Check if there is enough space on the current page for the terms and termsInfo
if (termsPosition + 50 > doc.page.height) {
doc.addPage();
termsPosition = 50; // Reset the position to the top of the new page
}
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 formatDate(date: Date): string {
// const day = date.getDate();
// const month = date.getMonth() + 1;
// const year = date.getFullYear();
// return `${year}/${month}/${day}`;
// }
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 { createCustomInvoice };
Editor is loading...
Leave a Comment