lbx-invoice
Version:
Provides functionality around generating invoices.
628 lines • 21.8 kB
JavaScript
"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