UNPKG

@casoon/invoice-generator

Version:

Generate PDF invoices, XRechnung XML, and ZUGFeRD PDF/A-3 from JSON data with TypeScript support

466 lines 19.6 kB
"use strict"; var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { if (k2 === undefined) k2 = k; var desc = Object.getOwnPropertyDescriptor(m, k); if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { desc = { enumerable: true, get: function() { return m[k]; } }; } Object.defineProperty(o, k2, desc); }) : (function(o, m, k, k2) { if (k2 === undefined) k2 = k; o[k2] = m[k]; })); var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { Object.defineProperty(o, "default", { enumerable: true, value: v }); }) : function(o, v) { o["default"] = v; }); var __importStar = (this && this.__importStar) || (function () { var ownKeys = function(o) { ownKeys = Object.getOwnPropertyNames || function (o) { var ar = []; for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k; return ar; }; return ownKeys(o); }; return function (mod) { if (mod && mod.__esModule) return mod; var result = {}; if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]); __setModuleDefault(result, mod); return result; }; })(); var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.ZUGFeRDPDFLibGenerator = void 0; const fs_1 = __importDefault(require("fs")); class ZUGFeRDPDFLibGenerator { constructor(invoice, options = {}) { this.invoice = invoice; this.options = { profile: 'BASIC', version: '2.1.1', conformanceLevel: 'A', outputFormat: 'PDF/A-3', ...options }; } /** * Generiert eine echte ZUGFeRD-konforme PDF/A-3 mit pdf-lib * * pdf-lib bietet: * - PDF/A-3 Unterstützung * - XML-Attachments * - Metadaten-Management * - Erweiterte PDF-Features */ async generateZUGFeRDPDFA3(outputPath) { try { // Dynamischer Import von pdf-lib (falls installiert) const { PDFDocument, PDFName, PDFDict, PDFArray, PDFString, PDFHexString } = await this.importPDFLib(); // Erstelle neues PDF/A-3 Dokument const pdfDoc = await PDFDocument.create(); // Setze PDF/A-3 Metadaten this.setPDFA3Metadata(pdfDoc); // Generiere PDF-Inhalt (visuelle Darstellung) await this.generatePDFContent(pdfDoc); // Generiere XML-Inhalt const xmlContent = this.generateZUGFeRDXML(); // Füge XML als Attachment hinzu await this.addXMLAttachment(pdfDoc, xmlContent); // Speichere PDF/A-3 const pdfBytes = await pdfDoc.save(); fs_1.default.writeFileSync(outputPath, pdfBytes); console.log(`✅ Echte ZUGFeRD PDF/A-3 erstellt: ${outputPath}`); } catch (error) { console.error('❌ Fehler beim Erstellen der PDF/A-3:', error); throw error; } } /** * Dynamischer Import von pdf-lib */ async importPDFLib() { try { // Versuche pdf-lib zu importieren const pdfLib = await Promise.resolve().then(() => __importStar(require('pdf-lib'))); return pdfLib; } catch (error) { throw new Error('pdf-lib ist nicht installiert. Führe aus:\n' + ' npm install pdf-lib\n\n' + 'Alternativ verwende den Standard-ZUGFeRD-Generator:\n' + ' import { ZUGFeRDPDFA3Generator } from "./zugferd-pdfa3-generator"'); } } /** * Setzt PDF/A-3 Metadaten */ setPDFA3Metadata(pdfDoc) { // PDF/A-3 spezifische Metadaten const metadata = { Title: 'ZUGFeRD Invoice', Author: 'Invoice Generator', Subject: 'Electronic Invoice (ZUGFeRD)', Creator: 'pdf-lib', Producer: 'pdf-lib', CreationDate: new Date(), ModDate: new Date(), // PDF/A-3 spezifische Felder 'PDF/A Version': 'A-3', 'ZUGFeRD Version': this.options.version, 'ZUGFeRD Profile': this.options.profile }; // Setze Metadaten im PDF Object.entries(metadata).forEach(([key, value]) => { if (value instanceof Date) { pdfDoc.setTitle(`${key}: ${value.toISOString()}`); } else { pdfDoc.setTitle(`${key}: ${value}`); } }); } /** * Generiert den PDF-Inhalt (visuelle Darstellung) */ async generatePDFContent(pdfDoc) { // pdf-lib bietet erweiterte Text- und Layout-Features const page = pdfDoc.addPage([595.28, 841.89]); // A4 // Beispiel für erweiterte Text-Features const { rgb } = await Promise.resolve().then(() => __importStar(require('pdf-lib'))); // Header page.drawText('ZUGFeRD Invoice', { x: 50, y: 800, size: 16, color: rgb(0, 0, 0) }); // Rechnungsdaten page.drawText(`Invoice Number: ${this.invoice.details.invoiceNumber}`, { x: 50, y: 750, size: 12 }); page.drawText(`Date: ${this.invoice.details.invoiceDate}`, { x: 50, y: 730, size: 12 }); // Sender page.drawText('From:', { x: 50, y: 700, size: 12, color: rgb(0.5, 0.5, 0.5) }); page.drawText(this.invoice.sender.name, { x: 50, y: 680, size: 10 }); page.drawText(this.invoice.sender.address.street, { x: 50, y: 660, size: 10 }); page.drawText(`${this.invoice.sender.address.postalCode} ${this.invoice.sender.address.city}`, { x: 50, y: 640, size: 10 }); // Recipient page.drawText('To:', { x: 300, y: 700, size: 12, color: rgb(0.5, 0.5, 0.5) }); page.drawText(this.invoice.recipient.name, { x: 300, y: 680, size: 10 }); page.drawText(this.invoice.recipient.address.street, { x: 300, y: 660, size: 10 }); page.drawText(`${this.invoice.recipient.address.postalCode} ${this.invoice.recipient.address.city}`, { x: 300, y: 640, size: 10 }); // Items Table let yPosition = 550; // Table Header page.drawText('Pos', { x: 50, y: yPosition, size: 10 }); page.drawText('Description', { x: 100, y: yPosition, size: 10 }); page.drawText('Qty', { x: 300, y: yPosition, size: 10 }); page.drawText('Price', { x: 350, y: yPosition, size: 10 }); page.drawText('Total', { x: 450, y: yPosition, size: 10 }); yPosition -= 20; // Table Rows this.invoice.items.forEach((item, index) => { page.drawText((index + 1).toString(), { x: 50, y: yPosition, size: 9 }); page.drawText(item.description, { x: 100, y: yPosition, size: 9 }); page.drawText(item.quantity.toString(), { x: 300, y: yPosition, size: 9 }); page.drawText(`${item.unitPrice.toFixed(2)} ${item.currency}`, { x: 350, y: yPosition, size: 9 }); page.drawText(`${item.total.toFixed(2)} ${item.currency}`, { x: 450, y: yPosition, size: 9 }); yPosition -= 15; }); // Totals yPosition -= 20; page.drawText(`Net Amount: ${this.invoice.totals.netAmount.toFixed(2)} ${this.invoice.totals.currency}`, { x: 350, y: yPosition, size: 12 }); yPosition -= 20; page.drawText(`VAT (${this.invoice.totals.vatRate}%): ${this.invoice.totals.vatAmount.toFixed(2)} ${this.invoice.totals.currency}`, { x: 350, y: yPosition, size: 12 }); yPosition -= 20; page.drawText(`Total: ${this.invoice.totals.grossAmount.toFixed(2)} ${this.invoice.totals.currency}`, { x: 350, y: yPosition, size: 14, color: rgb(0, 0, 0) }); } /** * Fügt XML als Attachment zum PDF hinzu (echte PDF/A-3 Funktionalität) */ async addXMLAttachment(pdfDoc, xmlContent) { const { PDFName, PDFDict, PDFArray, PDFString, PDFHexString } = await Promise.resolve().then(() => __importStar(require('pdf-lib'))); // Erstelle XML-Attachment const xmlBytes = new TextEncoder().encode(xmlContent); // Füge XML als Embedded File hinzu const embeddedFile = pdfDoc.context.obj({ Type: PDFName.of('EmbeddedFile'), Subtype: PDFName.of('application/xml'), Params: pdfDoc.context.obj({ Size: xmlBytes.length, CreationDate: new Date(), ModDate: new Date() }) }); // Erstelle File Specification const fileSpec = pdfDoc.context.obj({ Type: PDFName.of('Filespec'), F: PDFString.of('zugferd.xml'), EF: pdfDoc.context.obj({ F: embeddedFile }), AFRelationship: PDFName.of('Alternative') }); // Füge zum PDF hinzu if (!pdfDoc.catalog.get(PDFName.of('Names'))) { pdfDoc.catalog.set(PDFName.of('Names'), pdfDoc.context.obj({})); } const names = pdfDoc.catalog.get(PDFName.of('Names')); if (!names.get(PDFName.of('EmbeddedFiles'))) { names.set(PDFName.of('EmbeddedFiles'), pdfDoc.context.obj({ Type: PDFName.of('Names'), Names: PDFArray.withContext(pdfDoc.context) })); } const embeddedFiles = names.get(PDFName.of('EmbeddedFiles')); const namesArray = embeddedFiles.get(PDFName.of('Names')); namesArray.push(PDFString.of('zugferd.xml')); namesArray.push(fileSpec); } /** * Generiert das ZUGFeRD-konforme XML */ generateZUGFeRDXML() { const profile = this.getProfileIdentifier(); 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>${profile}</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(''); } getProfileIdentifier() { const { profile, version } = this.options; const profileMap = { '2.1.1': { 'BASIC': 'urn:ferd:CrossIndustryDocument:invoice:2p0:basic', 'COMFORT': 'urn:ferd:CrossIndustryDocument:invoice:2p0:comfort', 'EXTENDED': 'urn:ferd:CrossIndustryDocument:invoice:2p0:extended' }, '2.2.0': { 'BASIC': 'urn:ferd:CrossIndustryDocument:invoice:2p0:basic', 'COMFORT': 'urn:ferd:CrossIndustryDocument:invoice:2p0:comfort', 'EXTENDED': 'urn:ferd:CrossIndustryDocument:invoice:2p0:extended' } }; return profileMap[version]?.[profile] || profileMap['2.1.1']['BASIC']; } escapeXml(text) { return text .replace(/&/g, '&amp;') .replace(/</g, '&lt;') .replace(/>/g, '&gt;') .replace(/"/g, '&quot;') .replace(/'/g, '&apos;'); } formatDate(dateString) { 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]; return `${year}${month}${day}`; } return dateString; } /** * Validiert die ZUGFeRD-Konformität */ validateZUGFeRDConformance() { const errors = []; // Grundlegende Validierungen if (!this.invoice.details.invoiceNumber) { errors.push('Rechnungsnummer ist erforderlich'); } if (!this.invoice.sender.vatId) { errors.push('USt-IdNr. des Absenders ist erforderlich'); } if (!this.invoice.paymentInfo.iban) { errors.push('IBAN ist erforderlich'); } if (this.invoice.items.length === 0) { errors.push('Mindestens eine Rechnungsposition ist erforderlich'); } // Profil-spezifische Validierungen if (this.options.profile === 'COMFORT' || this.options.profile === 'EXTENDED') { if (!this.invoice.sender.businessOwner) { errors.push('Geschäftsinhaber ist für COMFORT/EXTENDED Profile erforderlich'); } } return { isValid: errors.length === 0, errors }; } } exports.ZUGFeRDPDFLibGenerator = ZUGFeRDPDFLibGenerator; //# sourceMappingURL=zugferd-pdflib-generator.js.map