@casoon/invoice-generator
Version:
Generate PDF invoices, XRechnung XML, and ZUGFeRD PDF/A-3 from JSON data with TypeScript support
373 lines • 19.9 kB
JavaScript
;
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.ZUGFeRDGenerator = void 0;
const pdfkit_1 = __importDefault(require("pdfkit"));
const fs_1 = __importDefault(require("fs"));
class ZUGFeRDGenerator {
constructor(invoice, options = {}) {
this.invoice = invoice;
this.options = {
profile: 'BASIC',
version: '2.1.1',
...options
};
}
/**
* Generiert eine ZUGFeRD-konforme PDF/A-3 mit eingebettetem XML
*/
async generateZUGFeRDPDF(outputPath) {
return new Promise((resolve, reject) => {
try {
// Erstelle PDF/A-3 Dokument
const doc = new pdfkit_1.default({
size: 'A4',
margin: 50,
compress: false,
bufferPages: true,
info: {
Title: 'ZUGFeRD Invoice',
Author: 'Invoice Generator',
Subject: 'Electronic Invoice',
Creator: 'PDFKit',
Producer: 'PDFKit',
CreationDate: new Date(),
ModDate: new Date()
}
});
// Erstelle Write Stream
const writeStream = fs_1.default.createWriteStream(outputPath);
doc.pipe(writeStream);
// Generiere PDF-Inhalt
this.generatePDFContent(doc);
// Generiere XML-Inhalt
const xmlContent = this.generateXMLContent();
// Füge XML als Attachment hinzu
this.addXMLAttachment(doc, xmlContent);
// Finalisiere PDF
doc.end();
writeStream.on('finish', () => {
console.log(`ZUGFeRD PDF erfolgreich erstellt: ${outputPath}`);
resolve();
});
writeStream.on('error', reject);
doc.on('error', reject);
}
catch (error) {
reject(error);
}
});
}
/**
* Generiert den PDF-Inhalt (visuelle Darstellung)
*/
generatePDFContent(doc) {
let currentY = 50;
// Header
currentY = this.drawHeader(doc, currentY);
// Salutation
currentY = this.drawSalutation(doc, currentY);
// Items Table
currentY = this.drawItemsTable(doc, currentY);
// Totals
currentY = this.drawTotals(doc, currentY);
// Payment Info
currentY = this.drawPaymentInfo(doc, currentY);
// Footer
this.drawFooter(doc, currentY);
}
drawHeader(doc, currentY) {
// Logo-Bereich (rechts)
if (fs_1.default.existsSync('logo.png')) {
try {
doc.image('logo.png', 350, currentY, { width: 150 });
}
catch (error) {
this.drawLogoPlaceholder(doc, currentY);
}
}
else {
this.drawLogoPlaceholder(doc, currentY);
}
currentY += 80;
// Rechnungsdaten (rechts)
const rightX = 350;
let y = currentY - 80 + 80;
doc.font('Helvetica-Bold').fontSize(8)
.text('Rückfragen an:', rightX, y, { continued: false });
y += 12;
doc.font('Helvetica').fontSize(8)
.text(this.invoice.sender.name, rightX, y, { continued: false });
y += 12;
doc.text(`Tel: ${this.invoice.sender.contactInfo.phone}`, rightX, y, { continued: false });
y += 12;
doc.text(`E-Mail: ${this.invoice.sender.contactInfo.email}`, rightX, y, { continued: false });
y += 15;
doc.fontSize(10)
.text(`Rechnungs-Nr.: ${this.invoice.details.invoiceNumber}`, rightX, y, { continued: false });
y += 15;
doc.text(`Kunden-Nr.: ${this.invoice.details.customerNumber}`, rightX, y, { continued: false });
y += 15;
doc.text(`Rechnungsdatum: ${this.invoice.details.invoiceDate}`, rightX, y, { continued: false });
y += 15;
doc.text(`Lieferdatum: ${this.invoice.details.deliveryDate}`, rightX, y, { continued: false });
y += 15;
doc.text(`Leistungszeitraum: ${this.invoice.details.servicePeriod}`, rightX, y, { continued: false });
// Empfänger-Adresse (links)
const recipientY = Math.max(currentY, 160);
const senderLine = [
this.invoice.sender.name,
this.invoice.sender.address.street,
`${this.invoice.sender.address.postalCode} ${this.invoice.sender.address.city}`
].filter(Boolean).join(' · ');
doc.fontSize(8)
.text(senderLine, 50, recipientY - 20, { continued: false });
doc.fontSize(12)
.text(this.invoice.recipient.name, 50, recipientY, { continued: false });
doc.text(this.invoice.recipient.address.street, 50, recipientY + 15, { continued: false });
doc.text(`${this.invoice.recipient.address.postalCode} ${this.invoice.recipient.address.city}`, 50, recipientY + 30, { continued: false });
return recipientY + 55;
}
drawLogoPlaceholder(doc, currentY) {
doc.font('Helvetica-Bold').fontSize(16)
.text('LOGO', 350, currentY + 30, { continued: false });
doc.rect(350, currentY, 150, 60).stroke();
}
drawSalutation(doc, currentY) {
doc.font('Helvetica-Bold').fontSize(10)
.text('Rechnung', 50, currentY, { continued: false });
currentY += 25;
doc.font('Helvetica').fontSize(10)
.text(this.invoice.salutation.greeting, 50, currentY, { continued: false });
currentY += 15;
doc.text(this.invoice.salutation.introduction, 50, currentY, { continued: false });
return currentY + 25;
}
drawItemsTable(doc, currentY) {
const tableWidth = 480;
const colWidths = [35, 170, 45, 45, 45, 70, 70];
// Tabellenkopf
doc.font('Helvetica-Bold').fontSize(8)
.text('', 55, currentY + 8, { width: colWidths[0], continued: false })
.text('Bezeichnung', 55 + colWidths[0], currentY + 8, { width: colWidths[1], continued: false })
.text('Menge', 55 + colWidths[0] + colWidths[1], currentY + 8, { width: colWidths[2], align: 'center', continued: false })
.text('Einh.', 55 + colWidths[0] + colWidths[1] + colWidths[2], currentY + 8, { width: colWidths[3], align: 'center', continued: false })
.text('MwSt.', 55 + colWidths[0] + colWidths[1] + colWidths[2] + colWidths[3], currentY + 8, { width: colWidths[4], align: 'center', continued: false })
.text('Einzelpreis', 55 + colWidths[0] + colWidths[1] + colWidths[2] + colWidths[3] + colWidths[4], currentY + 8, { width: colWidths[5], align: 'right', continued: false })
.text('Summe', 55 + colWidths[0] + colWidths[1] + colWidths[2] + colWidths[3] + colWidths[4] + colWidths[5], currentY + 8, { width: colWidths[6], align: 'right', continued: false });
currentY += 25;
// Tabellenzeilen
this.invoice.items.forEach((item, index) => {
const topY = currentY + 5;
doc.text((index + 1).toString(), 50, topY, { width: colWidths[0], align: 'center', continued: false });
doc.text(item.description, 55 + colWidths[0], topY, { width: colWidths[1], continued: false });
doc.text(item.quantity.toString(), 55 + colWidths[0] + colWidths[1], topY, { width: colWidths[2], align: 'center', continued: false });
doc.text(item.unit, 55 + colWidths[0] + colWidths[1] + colWidths[2], topY, { width: colWidths[3], align: 'center', continued: false });
doc.text(`${item.vatRate}`, 55 + colWidths[0] + colWidths[1] + colWidths[2] + colWidths[3], topY, { width: colWidths[4], align: 'center', continued: false });
doc.text(`${item.unitPrice.toFixed(2).replace('.', ',')} ${item.currency}`, 55 + colWidths[0] + colWidths[1] + colWidths[2] + colWidths[3] + colWidths[4], topY, { width: colWidths[5], align: 'right', continued: false });
doc.font('Helvetica-Bold').text(`${item.total.toFixed(2).replace('.', ',')} ${item.currency}`, 55 + colWidths[0] + colWidths[1] + colWidths[2] + colWidths[3] + colWidths[4] + colWidths[5], topY, { width: colWidths[6], align: 'right', continued: false });
currentY += 20;
});
return currentY + 20;
}
drawTotals(doc, currentY) {
doc.moveTo(50, currentY).lineTo(550, currentY).lineWidth(0.5).stroke();
currentY += 20;
doc.font('Helvetica').fontSize(9)
.text(`Nettobetrag: ${this.invoice.totals.netAmount.toFixed(2).replace('.', ',')} ${this.invoice.totals.currency}`, 50, currentY, { continued: false })
.text(`MwSt. (${this.invoice.totals.vatRate.toFixed(2).replace('.', ',')}%): ${this.invoice.totals.vatAmount.toFixed(2).replace('.', ',')} ${this.invoice.totals.currency}`, 250, currentY, { continued: false })
.font('Helvetica-Bold').fontSize(12)
.text(`Gesamtbetrag: ${this.invoice.totals.grossAmount.toFixed(2).replace('.', ',')} ${this.invoice.totals.currency}`, 450, currentY, { continued: false });
return currentY + 40;
}
drawPaymentInfo(doc, currentY) {
const paymentText = `Zahlbar ohne Abzug bis zum ${this.invoice.paymentInfo.dueDate}.`;
doc.font('Helvetica').fontSize(10)
.text(paymentText, 50, currentY, { continued: false });
currentY += 30;
doc.text('Mit freundlichen Grüßen', 50, currentY, { continued: false });
currentY += 15;
doc.text(this.invoice.sender.name, 50, currentY, { continued: false });
return currentY + 40;
}
drawFooter(doc, currentY) {
const footerY = 720;
const footerStartY = footerY + 20;
const colWidth = 125;
// Spalte 1 - Firmeninfo
doc.font('Helvetica-Bold').fontSize(7)
.text(this.invoice.sender.company || this.invoice.sender.name, 50, footerStartY, { continued: false })
.font('Helvetica')
.text(this.invoice.sender.name, 50, footerStartY + 12, { continued: false })
.text(this.invoice.sender.address.street, 50, footerStartY + 24, { continued: false })
.text(`${this.invoice.sender.address.postalCode} ${this.invoice.sender.address.city}`, 50, footerStartY + 36, { continued: false });
// Spalte 2 - Kontaktdaten
doc.fontSize(7)
.text(`Tel.: ${this.invoice.sender.contactInfo.phone}`, 50 + colWidth, footerStartY, { continued: false })
.text(`Email: ${this.invoice.sender.contactInfo.email}`, 50 + colWidth, footerStartY + 12, { continued: false })
.text(`Web: ${this.invoice.sender.contactInfo.website}`, 50 + colWidth, footerStartY + 24, { continued: false });
// Spalte 3 - Geschäftsinhaber und USt-IdNr.
doc.fontSize(7)
.text('Geschäftsinhaber:', 50 + 2 * colWidth, footerStartY, { continued: false })
.text(this.invoice.sender.businessOwner || '', 50 + 2 * colWidth, footerStartY + 12, { continued: false })
.text('USt-IdNr.:', 50 + 2 * colWidth, footerStartY + 24, { continued: false })
.text(this.invoice.sender.vatId || '', 50 + 2 * colWidth, footerStartY + 36, { continued: false });
// Spalte 4 - Bankdaten
doc.fontSize(7)
.text(`Bank: ${this.invoice.paymentInfo.bank}`, 50 + 3 * colWidth, footerStartY, { continued: false })
.text(`Kontoinhaber: ${this.invoice.paymentInfo.accountHolder}`, 50 + 3 * colWidth, footerStartY + 12, { continued: false })
.text(`IBAN: ${this.invoice.paymentInfo.iban}`, 50 + 3 * colWidth, footerStartY + 24, { continued: false })
.text(`BIC/SWIFT-Code: ${this.invoice.paymentInfo.bic}`, 50 + 3 * colWidth, footerStartY + 36, { continued: false });
}
/**
* Generiert das XML für ZUGFeRD
*/
generateXMLContent() {
const xml = `<?xml version="1.0" encoding="UTF-8"?>
<rsm:CrossIndustryInvoice xmlns:rsm="urn:ferd:CrossIndustryDocument:invoice:2p0" xmlns:ram="urn:un:unece:uncefact:data:standard:ReusableAggregateBusinessInformationEntity:100" xmlns:udt="urn:un:unece:uncefact:data:standard:UnqualifiedDataType:100">
<rsm:ExchangedDocumentContext>
<ram:TestIndicator>
<udt:Indicator>false</udt:Indicator>
</ram:TestIndicator>
<ram:BusinessProcessSpecifiedDocumentContextParameter>
<ram:ID>urn:cen.eu:en16931:2017#compliant#urn:xoev-de:kosit:standard:xrechnung_2.3</ram:ID>
</ram:BusinessProcessSpecifiedDocumentContextParameter>
<ram:GuidelineSpecifiedDocumentContextParameter>
<ram:ID>urn:cen.eu:en16931:2017</ram:ID>
</ram:GuidelineSpecifiedDocumentContextParameter>
</rsm:ExchangedDocumentContext>
<rsm:ExchangedDocument>
<ram:ID>${this.escapeXml(this.invoice.details.invoiceNumber)}</ram:ID>
<ram:TypeCode>380</ram:TypeCode>
<ram:IssueDateTime>
<udt:DateTimeString format="102">${this.formatDate(this.invoice.details.invoiceDate)}</udt:DateTimeString>
</ram:IssueDateTime>
</rsm:ExchangedDocument>
<rsm:SupplyChainTradeTransaction>
<ram:IncludedSupplyChainTradeLineItem>
${this.generateLineItems()}
</ram:IncludedSupplyChainTradeLineItem>
<ram:ApplicableHeaderTradeAgreement>
<ram:SellerTradeParty>
<ram:Name>${this.escapeXml(this.invoice.sender.name)}</ram:Name>
<ram:PostalTradeAddress>
<ram:LineOne>${this.escapeXml(this.invoice.sender.address.street)}</ram:LineOne>
<ram:CityName>${this.escapeXml(this.invoice.sender.address.city)}</ram:CityName>
<ram:PostcodeCode>${this.escapeXml(this.invoice.sender.address.postalCode)}</ram:PostcodeCode>
<ram:CountryID>DE</ram:CountryID>
</ram:PostalTradeAddress>
<ram:SpecifiedTaxRegistration>
<ram:ID schemeID="FC">${this.escapeXml(this.invoice.sender.vatId || '')}</ram:ID>
</ram:SpecifiedTaxRegistration>
<ram:DefinedTradeContact>
<ram:PersonName>${this.escapeXml(this.invoice.sender.businessOwner || '')}</ram:PersonName>
<ram:TelephoneUniversalCommunication>
<ram:CompleteNumber>${this.escapeXml(this.invoice.sender.contactInfo.phone)}</ram:CompleteNumber>
</ram:TelephoneUniversalCommunication>
<ram:EmailURIUniversalCommunication>
<ram:URIID>${this.escapeXml(this.invoice.sender.contactInfo.email)}</ram:URIID>
</ram:EmailURIUniversalCommunication>
</ram:DefinedTradeContact>
</ram:SellerTradeParty>
<ram:BuyerTradeParty>
<ram:Name>${this.escapeXml(this.invoice.recipient.name)}</ram:Name>
<ram:PostalTradeAddress>
<ram:LineOne>${this.escapeXml(this.invoice.recipient.address.street)}</ram:LineOne>
<ram:CityName>${this.escapeXml(this.invoice.recipient.address.city)}</ram:CityName>
<ram:PostcodeCode>${this.escapeXml(this.invoice.recipient.address.postalCode)}</ram:PostcodeCode>
<ram:CountryID>DE</ram:CountryID>
</ram:PostalTradeAddress>
</ram:BuyerTradeParty>
</ram:ApplicableHeaderTradeAgreement>
<ram:ApplicableHeaderTradeSettlement>
<ram:InvoiceCurrencyCode>${this.invoice.totals.currency}</ram:InvoiceCurrencyCode>
<ram:SpecifiedTradeSettlementPaymentMeans>
<ram:TypeCode>30</ram:TypeCode>
<ram:Information>${this.escapeXml(this.invoice.paymentInfo.bank)}</ram:Information>
<ram:PayeePartyCreditorFinancialAccount>
<ram:IBANID>${this.escapeXml(this.invoice.paymentInfo.iban)}</ram:IBANID>
</ram:PayeePartyCreditorFinancialAccount>
<ram:PayeeSpecifiedCreditorFinancialInstitution>
<ram:BICID>${this.escapeXml(this.invoice.paymentInfo.bic)}</ram:BICID>
</ram:PayeeSpecifiedCreditorFinancialInstitution>
</ram:SpecifiedTradeSettlementPaymentMeans>
<ram:ApplicableTradeTax>
<ram:CalculatedAmount>${this.invoice.totals.vatAmount.toFixed(2)}</ram:CalculatedAmount>
<ram:TypeCode>VAT</ram:TypeCode>
<ram:BasisAmount>${this.invoice.totals.netAmount.toFixed(2)}</ram:BasisAmount>
<ram:ApplicablePercent>${this.invoice.totals.vatRate.toFixed(2)}</ram:ApplicablePercent>
</ram:ApplicableTradeTax>
<ram:SpecifiedTradePaymentTerms>
<ram:Description>${this.escapeXml(this.invoice.paymentInfo.paymentTerms)}</ram:Description>
</ram:SpecifiedTradePaymentTerms>
<ram:SpecifiedTradeSettlementHeaderMonetarySummation>
<ram:LineTotalAmount>${this.invoice.totals.netAmount.toFixed(2)}</ram:LineTotalAmount>
<ram:TaxBasisTotalAmount>${this.invoice.totals.netAmount.toFixed(2)}</ram:TaxBasisTotalAmount>
<ram:TaxTotalAmount>${this.invoice.totals.vatAmount.toFixed(2)}</ram:TaxTotalAmount>
<ram:GrandTotalAmount>${this.invoice.totals.grossAmount.toFixed(2)}</ram:GrandTotalAmount>
</ram:SpecifiedTradeSettlementHeaderMonetarySummation>
</ram:ApplicableHeaderTradeSettlement>
</rsm:SupplyChainTradeTransaction>
</rsm:CrossIndustryInvoice>`;
return xml;
}
generateLineItems() {
return this.invoice.items.map((item, index) => `
<ram:IncludedSupplyChainTradeLineItem>
<ram:AssociatedDocumentLineDocument>
<ram:LineID>${index + 1}</ram:LineID>
</ram:AssociatedDocumentLineDocument>
<ram:SpecifiedTradeProduct>
<ram:Name>${this.escapeXml(item.description)}</ram:Name>
</ram:SpecifiedTradeProduct>
<ram:SpecifiedLineTradeAgreement>
<ram:NetPriceProductTradePrice>
<ram:ChargeAmount>${item.unitPrice.toFixed(2)}</ram:ChargeAmount>
</ram:NetPriceProductTradePrice>
</ram:SpecifiedLineTradeAgreement>
<ram:SpecifiedLineTradeDelivery>
<ram:BilledQuantity>${item.quantity}</ram:BilledQuantity>
</ram:SpecifiedLineTradeDelivery>
<ram:SpecifiedLineTradeSettlement>
<ram:ApplicableTradeTax>
<ram:TypeCode>VAT</ram:TypeCode>
<ram:ApplicablePercent>${item.vatRate.toFixed(2)}</ram:ApplicablePercent>
</ram:ApplicableTradeTax>
<ram:SpecifiedTradeSettlementLineMonetarySummation>
<ram:LineTotalAmount>${item.total.toFixed(2)}</ram:LineTotalAmount>
</ram:SpecifiedTradeSettlementLineMonetarySummation>
</ram:SpecifiedLineTradeSettlement>
</ram:IncludedSupplyChainTradeLineItem>`).join('');
}
escapeXml(text) {
return text
.replace(/&/g, '&')
.replace(/</g, '<')
.replace(/>/g, '>')
.replace(/"/g, '"')
.replace(/'/g, ''');
}
formatDate(dateString) {
// Konvertiere DD.MM.YY zu YYYYMMDD
const parts = dateString.split('.');
if (parts.length === 3) {
const day = parts[0].padStart(2, '0');
const month = parts[1].padStart(2, '0');
const year = '20' + parts[2]; // Annahme: 20xx
return `${year}${month}${day}`;
}
return dateString;
}
/**
* Fügt XML als Attachment zum PDF hinzu
*/
addXMLAttachment(doc, xmlContent) {
// Hinweis: PDFKit unterstützt standardmäßig keine PDF/A-3 Attachments
// In einer echten Implementierung würde hier ein PDF/A-3 Library verwendet werden
console.log('XML-Inhalt für ZUGFeRD generiert (Attachment-Funktionalität erfordert PDF/A-3 Library)');
}
}
exports.ZUGFeRDGenerator = ZUGFeRDGenerator;
//# sourceMappingURL=zugferd-generator.js.map