Untitled
unknown
plain_text
14 days ago
16 kB
3
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