UNPKG

lbx-invoice

Version:

Provides functionality around generating invoices.

628 lines 21.8 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.BasePdfService = void 0; const tslib_1 = require("tslib"); const child_process_1 = require("child_process"); const path_1 = tslib_1.__importDefault(require("path")); const functions_1 = require("../functions"); const utilities_1 = require("../utilities"); const big_number_utilities_1 = require("../utilities/big-number.utilities"); const invoice_calc_utilities_1 = require("../utilities/invoice-calc.utilities"); const pdf_utilities_1 = require("../utilities/pdf.utilities"); // TODO: getTaxGroupsFor // TODO: xml number format (only 2 numbers after the comma) /** * Handles the generation of invoice pdfs. * Uses pdfmake. */ class BasePdfService { constructor() { /** * The font size that should be used for the footer. * @default 10 */ this.FOOTER_FONT_SIZE = 10; /** * The currency used for formatting prices. * @default 'USD' */ this.CURRENCY = 'USD'; /** * The locale to use for formatting dates. * @default 'en-US' */ this.LOCALE = 'en-US'; /** * The timezone to use for formatting dates. * @default 'America/New_York' */ this.TIMEZONE = 'America/New_York'; /** * Globally defined images which can be referenced by name inside the document definition. * @default undefined */ this.IMAGES = undefined; /** * Available options: * * - A reference by name to an image defined in PdfMakeDocumentDefinition.images * - A data URL * - A remote URL via http:// or https://. * * Supported image formats: JPEG, PNG. * @default undefined */ this.LOGO = undefined; /** * The label for the phone number on the invoice. * @default 'Phone' */ this.PHONE_LABEL = 'Phone'; /** * The label for the email on the invoice. * @default 'E-Mail' */ this.EMAIL_LABEL = 'E-Mail'; /** * The headline of the invoice. * @default 'Invoice' */ this.HEADLINE = 'Invoice'; /** * The font size of the headline. * @default 20 */ this.HEADLINE_FONT_SIZE = 20; /** * The label for the invoice number on the invoice. * @default 'Invoice No.' */ this.INVOICE_NUMBER_LABEL = 'Invoice No.'; /** * The notice below the invoice number. * @default 'Please specify when making payments and invoicing!' */ this.INVOICE_NUMBER_NOTICE = 'Please specify when making payments and invoicing!'; /** * The label for the date on the invoice. * @default 'Date' */ this.DATE_LABEL = 'Date'; /** * The label for the date of performance on the invoice. * @default 'Date of performance' */ this.PERFORMANCE_DATE_LABEL = 'Date of performance'; /** * Whether or not a performance date should be displayed on the invoice. * @default false */ this.SHOW_PERFORMANCE_DATE = false; /** * Whether or not a due date should be displayed on the invoice. * @default true */ this.SHOW_DUE_DATE = true; /** * The label for the due date on the invoice. * @default 'Due date' */ this.DUE_DATE_LABEL = 'Due date'; /** * The font size that should be used inside the tables. * @default 12 */ this.TABLE_FONT_SIZE = 12; /** * The label for item name on the invoice. * @default 'Name' */ this.ITEM_NAME_LABEL = 'Name'; /** * The label for the total when no taxes exist on the invoice. * @default 'Total' */ this.TOTAL_LABEL = 'Total'; /** * The label for the total before tax value on the invoice. * @default 'Total (excl. tax)' */ this.TOTAL_BEFORE_TAX_LABEL = 'Total (excl. tax)'; /** * The label for the total after tax value on the invoice. * @default 'Total (incl. tax)' */ this.TOTAL_AFTER_TAX_LABEL = 'Total (incl. tax)'; /** * The label for tax value on the invoice. * @default 'VAT' */ this.TAX_LABEL = 'VAT'; /** * The label for tax number on the invoice. * @default 'Tax number:' */ this.TAX_NUMBER_LABEL = 'Tax number:'; /** * The label for the ceo on the invoice. * @default 'CEO:' */ this.CEO_LABEL = 'CEO:'; /** * The label for item amount on the invoice. * @default 'Amount' */ this.ITEM_AMOUNT_LABEL = 'Amount'; /** * The label for single price of an item on the invoice. * @default 'Price' */ this.ITEM_SINGLE_PRICE_LABEL = 'Price'; /** * The label for total price of an item on the invoice. * @default 'Total' */ this.ITEM_TOTAL_PRICE_LABEL = 'Total'; /** * The label for the bank details in the footer. */ this.BANK_DETAILS_LABEL = 'Bank details:'; /** * The label for the iban to display in the footer. * @default 'IBAN:' */ this.IBAN_LABEL = 'IBAN:'; /** * The label for the bic to display in the footer. * @default 'BIC/SWIFT:' */ this.BIC_LABEL = 'BIC/SWIFT:'; } /** * Creates a pdf for the provided invoice and handles it afterwards. * @param invoice - The invoice to create the pdf for. */ async createInvoicePdf(invoice) { const documentDefinition = this.generateInvoiceDocumentDefinition(invoice); const pdfData = await pdf_utilities_1.PdfUtilities.createPdf(documentDefinition); await this.handleFinishedInvoicePdf(pdfData, invoice); } /** * Converts the pdf at the given path to pdf-a. * @param pdfFilePath - The path to the pdf that should be converted. * @throws When there was an error in the script. */ pdfToPdfA(pdfFilePath) { const scriptPath = path_1.default.join(__dirname, './convert-to-pdf-a.sh'); const res = (0, child_process_1.execSync)(`${scriptPath} ${pdfFilePath} ${pdfFilePath}`, { encoding: 'utf8' }); if (res.includes('PDF-A ERROR')) { throw new Error(res); } } /** * Attaches an X-Rechnung to the invoice pdf at the given path. * @param pdfFilePath - The path of the invoice pdf that the X-Rechnung should be attached to. * @param invoice - The invoice data. */ async pdfToXRechnung(pdfFilePath, invoice) { this.pdfToPdfA(pdfFilePath); await utilities_1.XRechnungUtilities.attachXRechnung(pdfFilePath, invoice, this.CURRENCY, this.companyInfo); } /** * Generates a pdfmake document definition for the given invoice. * @param invoice - The invoice to generate the document definition for. * @returns The invoice document definition. */ generateInvoiceDocumentDefinition(invoice) { const taxGroups = this.getTaxGroups(invoice); const res = { pageSize: 'A4', pageMargins: [40, 40, 40, 70], images: this.IMAGES, header: this.getHeader(), content: [ this.getLetterhead(invoice), this.getHeadline(), this.getNumberAndDates(invoice), ...this.getContentFromParagraphs(invoice.textBeforeItems), this.getInvoiceItemsTable(invoice, taxGroups), this.getTotalTable(invoice, taxGroups), ...this.getContentFromParagraphs(invoice.textAfterItems) ], footer: this.getFooter() }; return res; } /** * Gets all relevant tax groups for the pdf file. * @param invoice - The invoice to get the tax groups for. * @returns The tax groups for the pdf. */ getTaxGroups(invoice) { const vats = [...new Set(invoice.items.map(item => item.vat))]; return vats.filter(v => v.rate !== 0); } /** * Gets the table that displays the total price of all items. * @param invoice - The invoice to get the table from. * @param taxGroups - The different tax groups that need to be taken into consideration. * @returns The total price of all items and the tax groups. */ getTotalTable(invoice, taxGroups) { return { table: { headerRows: 0, widths: ['*', 159], body: this.getTotalBody(invoice, taxGroups) }, fontSize: this.TABLE_FONT_SIZE, bold: true, margin: [0, 15, 0, 20] }; } /** * Gets the body of the total table. * @param invoice - The invoice to build the body from. * @param taxGroups - The different tax groups that need to be taken into consideration. * @returns The body of the total table. */ getTotalBody(invoice, taxGroups) { if (!taxGroups.length) { return [[this.TOTAL_LABEL, this.formatPrice(invoice_calc_utilities_1.InvoiceCalcUtilities.getTotalBeforeTax(invoice).toNumber())]]; } const res = [ [ this.TOTAL_BEFORE_TAX_LABEL, this.formatPrice(invoice_calc_utilities_1.InvoiceCalcUtilities.getTotalBeforeTax(invoice).toNumber()) ] ]; for (const group of taxGroups) { res.push([ `${this.formatPercent(group.rate)} ${this.TAX_LABEL}`, this.formatPrice(invoice_calc_utilities_1.InvoiceCalcUtilities.getTotalTaxForTaxGroup(invoice, group, true).toNumber()) ]); } res.push([this.TOTAL_AFTER_TAX_LABEL, this.formatPrice(invoice_calc_utilities_1.InvoiceCalcUtilities.getTotalAfterTax(invoice).toNumber())]); return res; } /** * Gets the table that displays all invoice items. * @param invoice - The invoice to build the table from. * @param taxGroups - The different tax groups that need to be taken into consideration. * @returns The table with all invoice items. */ getInvoiceItemsTable(invoice, taxGroups) { const widths = taxGroups.length ? ['*', 75, 75, 75, 75] : ['*', 100, 100, 100]; return { table: { headerRows: 1, widths: widths, body: this.getInvoiceItemsBody(invoice, taxGroups) }, fontSize: this.TABLE_FONT_SIZE, margin: [0, 25, 0, 0] }; } /** * Gets the body of the invoice items table. * @param invoice - The invoice to build the body from. * @param taxGroups - The different tax groups that need to be taken into consideration. * @returns The body of the invoice items table. */ getInvoiceItemsBody(invoice, taxGroups) { const res = [this.getInvoiceItemsHeaders(taxGroups)]; for (const item of invoice.items) { const totalPricePreTax = invoice_calc_utilities_1.InvoiceCalcUtilities.getItemTotalPriceBeforeTax(item); const amount = `${item.amount} ${item.amountUnit.displayName}`; if (taxGroups.length) { res.push([ item.name, amount, this.formatPrice(item.price), item.vat.rate !== 0 ? this.formatPercent(item.vat.rate) : '-', this.formatPrice(totalPricePreTax.toNumber()) ]); } else { res.push([ item.name, amount, this.formatPrice(item.price), this.formatPrice(totalPricePreTax.toNumber()) ]); } } return res; } /** * Gets the headers for the invoice items table. If the only tax group is 0, the tax column is removed. * @param taxGroups - The different tax groups that need to be taken into consideration. * @returns The headers of the invoice items table. */ getInvoiceItemsHeaders(taxGroups) { const headers = [ { text: this.ITEM_NAME_LABEL, style: { bold: true } }, { text: this.ITEM_AMOUNT_LABEL, style: { bold: true } }, { text: this.ITEM_SINGLE_PRICE_LABEL, style: { bold: true } } ]; if (taxGroups.length) { headers.push({ text: this.TAX_LABEL, style: { bold: true } }); } headers.push({ text: this.ITEM_TOTAL_PRICE_LABEL, style: { bold: true } }); return headers; } /** * Formats the given number as a price. * Uses the locale and currency values of this class. * @param value - The number to format as a price. * @returns A string representation of the number, including the currency symbol etc. */ formatPrice(value) { return value.toLocaleString(this.LOCALE, { style: 'currency', currency: this.CURRENCY, minimumFractionDigits: 2, maximumFractionDigits: 2 }); } /** * Formats the given number as a percentage. * Uses the locale of this class. * @param value - The number to format as a percentage. * @returns A string representation of the number, including the percentage symbol. */ formatPercent(value) { return big_number_utilities_1.BigNumberUtilities.divide(value, 100).toNumber() .toLocaleString(this.LOCALE, { style: 'percent' }); } /** * Gets the text that should be displayed before the actual invoice items start. * @param paragraphs - The paragraphs to get the text from. * @returns The text that should be displayed before the actual invoice items. */ getContentFromParagraphs(paragraphs) { return paragraphs.map(t => { return { text: t, margin: [0, 10, 0, 0] }; }); } /** * Gets the headline of the invoice. * @returns The headline. */ getHeadline() { return { text: this.HEADLINE, fontSize: this.HEADLINE_FONT_SIZE, style: { bold: true }, margin: [0, 10, 0, 10] }; } /** * Gets the invoice number and the dates. * @param invoice - The invoice to build the content from. * @returns The content containing the invoice number and the date, performance date and due date. */ getNumberAndDates(invoice) { const invoiceNumberColumn = [ { text: `${this.INVOICE_NUMBER_LABEL}: ${invoice.number}`, alignment: 'left' }, { text: this.INVOICE_NUMBER_NOTICE, fontSize: 8, italics: true } ]; const dateColumn = [ { text: `${this.DATE_LABEL}: ${this.formatDate(invoice.date)}`, alignment: 'right' } ]; if (this.SHOW_PERFORMANCE_DATE) { dateColumn.push({ text: `${this.PERFORMANCE_DATE_LABEL}: ${this.formatDate(invoice.performanceDate)}`, alignment: 'right' }); } if (this.SHOW_DUE_DATE) { dateColumn.push({ text: `${this.DUE_DATE_LABEL}: ${this.formatDate(invoice.dueDate)}`, alignment: 'right' }); } return { columns: [ invoiceNumberColumn, dateColumn ] }; } /** * Formats the given date using the locale and timezone. * @param date - The date to format. * @returns The formatted date string with the locale and timezone. */ formatDate(date) { return new Date(date).toLocaleString(this.LOCALE, { timeZone: this.TIMEZONE, day: '2-digit', month: '2-digit', year: 'numeric' }); } /** * Gets the Content of the header. * @returns A row with the logo or an empty array if no logo has been specified. */ getHeader() { if (!this.LOGO) { return []; } return [ { columns: [ { width: '*', text: '' }, { fit: [500, 25], image: this.LOGO } ] } ]; } /** * Gets the letterhead. * @param invoice - The invoice to get the letterhead from. * @returns The letterhead of the customer and the company. */ getLetterhead(invoice) { return { columns: [ this.getCustomerLetterheadColumn(invoice), { columns: [this.getCompanyLetterheadColumn()], width: 'auto' } ] }; } /** * Gets the letterhead of the company. * @returns The letterhead of the company, containing full name, address and contact information. */ getCompanyLetterheadColumn() { return [ { text: this.companyInfo.fullName, style: { bold: true } }, `${this.companyInfo.address.street} ${this.companyInfo.address.number}`, { text: `${this.companyInfo.address.postcode} ${this.companyInfo.address.city}`, margin: [0, 0, 0, 10] }, `${this.PHONE_LABEL}: ${this.companyInfo.phone}`, `${this.EMAIL_LABEL}: ${this.companyInfo.email}` ]; } /** * Gets the letterhead of the customer. * @param invoice - The invoice to get the customer letterhead from. * @returns The letterhead of the customer, containing the name and address. */ getCustomerLetterheadColumn(invoice) { // eslint-disable-next-line stylistic/max-len const companyAddressLine = `${this.companyInfo.fullName} - ${this.companyInfo.address.street} ${this.companyInfo.address.number} - ${this.companyInfo.address.postcode} ${this.companyInfo.address.city}`; const fontSize = this.getFontSizeForLetterhead(companyAddressLine); return [ { text: companyAddressLine, fontSize: fontSize, alignment: 'left', margin: [0, 0, 0, 5], decoration: 'underline' }, (0, functions_1.getCustomerName)(invoice), `${invoice.customerAddressData.street} ${invoice.customerAddressData.number}`, `${invoice.customerAddressData.postcode} ${invoice.customerAddressData.city}` ]; } /** * Gets the font size for the provided company address line. * Checks if the address line is too big and makes the font smaller accordingly. * @param companyAddressLine - The company address line of the invoice. * @returns The font size to use for the letter head. */ getFontSizeForLetterhead(companyAddressLine) { if (companyAddressLine.length > 85) { return 4; } if (companyAddressLine.length > 65) { return 6; } return 8; } /** * Gets the Content of the footer. * @returns Multiple rows containing information about the company and payment data. */ getFooter() { return { columns: [ this.getFirstFooterColumn(), this.getSecondFooterColumn() ], margin: [40, 0, 0, 0], fontSize: this.FOOTER_FONT_SIZE }; } /** * Gets the first column of the footer. * @returns The column definition. */ getFirstFooterColumn() { const res = [ { text: this.BANK_DETAILS_LABEL, style: { bold: true } } ]; if (this.BANK_NAME) { res.push(this.BANK_NAME); } if (this.companyInfo.iban) { res.push(`${this.IBAN_LABEL} ${this.companyInfo.iban}`); } if (this.companyInfo.bic) { res.push(`${this.BIC_LABEL} ${this.companyInfo.bic}`); } return res; } /** * Gets the second column of the footer. * @returns The column definition. */ getSecondFooterColumn() { const res = [ { text: this.companyInfo.fullName, style: { bold: true } } ]; if (this.companyInfo.taxNumber) { res.push(`${this.TAX_NUMBER_LABEL} ${this.companyInfo.taxNumber}`); } if (this.TAX_OFFICE) { res.push(this.TAX_OFFICE); } if (this.companyInfo.ceo) { res.push(`${this.CEO_LABEL} ${this.companyInfo.ceo}`); } return res; } } exports.BasePdfService = BasePdfService; //# sourceMappingURL=base-pdf.service.js.map