UNPKG

@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
"use strict"; 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, '&amp;') .replace(/</g, '&lt;') .replace(/>/g, '&gt;') .replace(/"/g, '&quot;') .replace(/'/g, '&apos;'); } 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