UNPKG

@casoon/invoice-generator

Version:

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

194 lines (183 loc) 8.94 kB
import { Invoice } from '../../types/invoice.types'; import fs from 'fs'; export class XRechnungGenerator { private invoice: Invoice; constructor(invoice: Invoice) { this.invoice = invoice; } /** * Generiert eine XRechnung-XML-Datei nach dem deutschen E-Rechnungs-Standard */ public generateXML(): string { 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:BuyerOrderReferencedDocument> <ram:IssuerAssignedID>${this.escapeXml(this.invoice.details.customerNumber)}</ram:IssuerAssignedID> </ram:BuyerOrderReferencedDocument> </ram:ApplicableHeaderTradeAgreement> <ram:ApplicableHeaderTradeDelivery> <ram:ActualDeliverySupplyChainEvent> <ram:OccurrenceDateTime> <udt:DateTimeString format="102">${this.formatDate(this.invoice.details.deliveryDate)}</udt:DateTimeString> </ram:OccurrenceDateTime> </ram:ActualDeliverySupplyChainEvent> </ram:ApplicableHeaderTradeDelivery> <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:PayerPartyDebtorFinancialAccount> <ram:IBANID>${this.escapeXml(this.invoice.paymentInfo.iban)}</ram:IBANID> </ram:PayerPartyDebtorFinancialAccount> <ram:PayerSpecifiedDebtorFinancialInstitution> <ram:BICID>${this.escapeXml(this.invoice.paymentInfo.bic)}</ram:BICID> </ram:PayerSpecifiedDebtorFinancialInstitution> </ram:SpecifiedTradeSettlementPaymentMeans> <ram:ApplicableTradeSettlementFinancialMonetarySummation> <ram:LineTotalAmount>${this.invoice.totals.netAmount.toFixed(2)}</ram:LineTotalAmount> <ram:TaxBasisTotalAmount>${this.invoice.totals.netAmount.toFixed(2)}</ram:TaxBasisTotalAmount> <ram:TaxTotalAmount currencyID="${this.invoice.totals.currency}">${this.invoice.totals.vatAmount.toFixed(2)}</ram:TaxTotalAmount> <ram:GrandTotalAmount>${this.invoice.totals.grossAmount.toFixed(2)}</ram:GrandTotalAmount> <ram:TotalPrepaidAmount>0.00</ram:TotalPrepaidAmount> <ram:DuePayableAmount>${this.invoice.totals.grossAmount.toFixed(2)}</ram:DuePayableAmount> </ram:ApplicableTradeSettlementFinancialMonetarySummation> <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:CategoryCode>S</ram:CategoryCode> <ram:RateApplicablePercent>${this.invoice.totals.vatRate.toFixed(2)}</ram:RateApplicablePercent> </ram:ApplicableTradeTax> </ram:ApplicableHeaderTradeSettlement> </rsm:SupplyChainTradeTransaction> </rsm:CrossIndustryInvoice>`; return xml; } /** * Generiert die XML-Datei und speichert sie */ public async generateXMLFile(outputPath: string): Promise<void> { const xml = this.generateXML(); return new Promise((resolve, reject) => { fs.writeFile(outputPath, xml, 'utf8', (err) => { if (err) { reject(err); } else { resolve(); } }); }); } private generateLineItems(): string { return this.invoice.items.map((item, index) => ` <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:GrossPriceProductTradePrice> <ram:ChargeAmount>${item.unitPrice.toFixed(2)}</ram:ChargeAmount> <ram:BasisQuantity unitCode="${item.unit}">${item.quantity}</ram:BasisQuantity> </ram:GrossPriceProductTradePrice> </ram:SpecifiedLineTradeAgreement> <ram:SpecifiedLineTradeDelivery> <ram:BilledQuantity unitCode="${item.unit}">${item.quantity}</ram:BilledQuantity> </ram:SpecifiedLineTradeDelivery> <ram:SpecifiedLineTradeSettlement> <ram:ApplicableTradeTax> <ram:TypeCode>VAT</ram:TypeCode> <ram:CategoryCode>S</ram:CategoryCode> <ram:RateApplicablePercent>${item.vatRate.toFixed(2)}</ram:RateApplicablePercent> </ram:ApplicableTradeTax> <ram:SpecifiedTradeSettlementLineMonetarySummation> <ram:LineTotalAmount>${item.total.toFixed(2)}</ram:LineTotalAmount> </ram:SpecifiedTradeSettlementLineMonetarySummation> </ram:SpecifiedLineTradeSettlement>`).join(''); } private escapeXml(text: string): string { return text .replace(/&/g, '&amp;') .replace(/</g, '&lt;') .replace(/>/g, '&gt;') .replace(/"/g, '&quot;') .replace(/'/g, '&apos;'); } private formatDate(dateString: string): string { // Konvertiert "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: YY ist 20YY return `${year}${month}${day}`; } return dateString; } } /** * Convenience-Funktionen für einfache Verwendung */ export async function generateXRechnungXML(invoice: Invoice): Promise<string> { const generator = new XRechnungGenerator(invoice); return generator.generateXML(); } export async function generateXRechnungXMLFile(invoice: Invoice, outputPath: string): Promise<void> { const generator = new XRechnungGenerator(invoice); return generator.generateXMLFile(outputPath); }