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
|
|
|
}
|