einvoice/ts/formats/ubl/xrechnung/xrechnung.encoder.ts

149 lines
5.7 KiB
TypeScript
Raw Normal View History

2025-05-26 10:17:50 +00:00
import { UBLEncoder } from '../generic/ubl.encoder.js';
2025-04-03 16:41:10 +00:00
import type { TInvoice, TCreditNote, TDebitNote } from '../../../interfaces/common.js';
2025-05-26 10:17:50 +00:00
import { DOMParser, XMLSerializer } from '../../../plugins.js';
2025-04-03 16:41:10 +00:00
/**
* Encoder for XRechnung (UBL) format
2025-05-26 10:17:50 +00:00
* Extends the generic UBL encoder with XRechnung-specific customizations
2025-04-03 16:41:10 +00:00
*/
2025-05-26 10:17:50 +00:00
export class XRechnungEncoder extends UBLEncoder {
2025-04-03 16:41:10 +00:00
/**
2025-05-26 10:17:50 +00:00
* Encodes a credit note into XRechnung XML
* @param creditNote Credit note to encode
* @returns XRechnung XML string
2025-04-03 16:41:10 +00:00
*/
protected async encodeCreditNote(creditNote: TCreditNote): Promise<string> {
2025-05-26 10:17:50 +00:00
// First get the base UBL XML
const baseXml = await super.encodeCreditNote(creditNote);
// Parse and modify for XRechnung
const doc = new DOMParser().parseFromString(baseXml, 'application/xml');
this.applyXRechnungCustomizations(doc, creditNote as unknown as TInvoice);
// Serialize back to string
return new XMLSerializer().serializeToString(doc);
2025-04-03 16:41:10 +00:00
}
2025-05-26 10:17:50 +00:00
2025-04-03 16:41:10 +00:00
/**
2025-05-26 10:17:50 +00:00
* Encodes a debit note (invoice) into XRechnung XML
* @param debitNote Debit note to encode
* @returns XRechnung XML string
2025-04-03 16:41:10 +00:00
*/
protected async encodeDebitNote(debitNote: TDebitNote): Promise<string> {
2025-05-26 10:17:50 +00:00
// First get the base UBL XML
const baseXml = await super.encodeDebitNote(debitNote);
// Parse and modify for XRechnung
const doc = new DOMParser().parseFromString(baseXml, 'application/xml');
this.applyXRechnungCustomizations(doc, debitNote as unknown as TInvoice);
// Serialize back to string
return new XMLSerializer().serializeToString(doc);
}
/**
* Applies XRechnung-specific customizations to the document
* @param doc XML document
* @param invoice Invoice data
*/
private applyXRechnungCustomizations(doc: Document, invoice: TInvoice): void {
const root = doc.documentElement;
// Update Customization ID to XRechnung 2.0
const customizationId = root.getElementsByTagName('cbc:CustomizationID')[0];
if (customizationId) {
customizationId.textContent = 'urn:cen.eu:en16931:2017#compliant#urn:xoev-de:kosit:standard:xrechnung_2.0';
}
// Add or update Buyer Reference (required for XRechnung)
let buyerRef = root.getElementsByTagName('cbc:BuyerReference')[0];
if (!buyerRef) {
// Find where to insert it (after DocumentCurrencyCode)
const currencyCode = root.getElementsByTagName('cbc:DocumentCurrencyCode')[0];
if (currencyCode) {
buyerRef = doc.createElement('cbc:BuyerReference');
buyerRef.textContent = invoice.buyerReference || invoice.id;
currencyCode.parentNode!.insertBefore(buyerRef, currencyCode.nextSibling);
}
} else if (!buyerRef.textContent || buyerRef.textContent.trim() === '') {
buyerRef.textContent = invoice.buyerReference || invoice.id;
}
// Update payment terms to German
const paymentTermsNotes = root.getElementsByTagName('cac:PaymentTerms');
if (paymentTermsNotes.length > 0) {
const noteElement = paymentTermsNotes[0].getElementsByTagName('cbc:Note')[0];
if (noteElement && noteElement.textContent) {
noteElement.textContent = `Zahlung innerhalb von ${invoice.dueInDays || 30} Tagen`;
}
}
// Add electronic address for parties if available
this.addElectronicAddressToParty(doc, 'cac:AccountingSupplierParty', invoice.from);
this.addElectronicAddressToParty(doc, 'cac:AccountingCustomerParty', invoice.to);
// Ensure payment reference is set
const paymentMeans = root.getElementsByTagName('cac:PaymentMeans')[0];
if (paymentMeans) {
let paymentId = paymentMeans.getElementsByTagName('cbc:PaymentID')[0];
if (!paymentId) {
paymentId = doc.createElement('cbc:PaymentID');
paymentId.textContent = invoice.id;
paymentMeans.appendChild(paymentId);
}
}
// Add country code handling for German addresses
this.fixGermanCountryCodes(doc);
}
/**
* Adds electronic address to party if not already present
* @param doc XML document
* @param partyType Party type selector
* @param party Party data
*/
private addElectronicAddressToParty(doc: Document, partyType: string, party: any): void {
const partyContainer = doc.getElementsByTagName(partyType)[0];
if (!partyContainer) return;
const partyElement = partyContainer.getElementsByTagName('cac:Party')[0];
if (!partyElement) return;
// Check if electronic address already exists
const existingEndpoint = partyElement.getElementsByTagName('cbc:EndpointID')[0];
if (!existingEndpoint && party.electronicAddress) {
// Add electronic address at the beginning of party element
const endpointNode = doc.createElement('cbc:EndpointID');
endpointNode.setAttribute('schemeID', party.electronicAddress.scheme || '0204');
endpointNode.textContent = party.electronicAddress.value;
// Insert as first child of party element
if (partyElement.firstChild) {
partyElement.insertBefore(endpointNode, partyElement.firstChild);
} else {
partyElement.appendChild(endpointNode);
}
}
}
/**
* Fixes German country codes in the document
* @param doc XML document
*/
private fixGermanCountryCodes(doc: Document): void {
const countryNodes = doc.getElementsByTagName('cbc:IdentificationCode');
for (let i = 0; i < countryNodes.length; i++) {
const node = countryNodes[i];
if (node.textContent) {
const text = node.textContent.toLowerCase();
if (text === 'germany' || text === 'deutschland' || text === 'de') {
node.textContent = 'DE';
} else if (text.length > 2) {
// Try to use first 2 characters as country code
node.textContent = text.substring(0, 2).toUpperCase();
}
}
}
2025-04-03 16:41:10 +00:00
}
2025-05-26 10:17:50 +00:00
}