fix(compliance): Improve compliance
This commit is contained in:
@ -19,8 +19,8 @@ export class XRechnungDecoder extends UBLBaseDecoder {
|
||||
// Return the invoice data as a credit note
|
||||
return {
|
||||
...commonData,
|
||||
invoiceType: 'creditnote'
|
||||
} as TCreditNote;
|
||||
accountingDocType: 'creditnote' as const
|
||||
} as unknown as TCreditNote;
|
||||
}
|
||||
|
||||
/**
|
||||
@ -34,8 +34,8 @@ export class XRechnungDecoder extends UBLBaseDecoder {
|
||||
// Return the invoice data as a debit note
|
||||
return {
|
||||
...commonData,
|
||||
invoiceType: 'debitnote'
|
||||
} as TDebitNote;
|
||||
accountingDocType: 'debitnote' as const
|
||||
} as unknown as TDebitNote;
|
||||
}
|
||||
|
||||
/**
|
||||
@ -61,7 +61,7 @@ export class XRechnungDecoder extends UBLBaseDecoder {
|
||||
}
|
||||
|
||||
// Extract items
|
||||
const items: finance.TInvoiceItem[] = [];
|
||||
const items: finance.TAccountingDocItem[] = [];
|
||||
const invoiceLines = this.select('//cac:InvoiceLine', this.doc);
|
||||
|
||||
if (invoiceLines && Array.isArray(invoiceLines)) {
|
||||
@ -121,11 +121,12 @@ export class XRechnungDecoder extends UBLBaseDecoder {
|
||||
|
||||
// Create the common invoice data
|
||||
return {
|
||||
type: 'invoice',
|
||||
type: 'accounting-doc' as const,
|
||||
accountingDocType: 'invoice' as const,
|
||||
id: invoiceId,
|
||||
invoiceId: invoiceId,
|
||||
accountingDocId: invoiceId,
|
||||
date: issueDate,
|
||||
status: 'invoice',
|
||||
accountingDocStatus: 'issued' as const,
|
||||
versionInfo: {
|
||||
type: 'final',
|
||||
version: '1.0.0'
|
||||
@ -146,11 +147,12 @@ export class XRechnungDecoder extends UBLBaseDecoder {
|
||||
console.error('Error extracting common data:', error);
|
||||
// Return default data
|
||||
return {
|
||||
type: 'invoice',
|
||||
type: 'accounting-doc' as const,
|
||||
accountingDocType: 'invoice' as const,
|
||||
id: `INV-${Date.now()}`,
|
||||
invoiceId: `INV-${Date.now()}`,
|
||||
accountingDocId: `INV-${Date.now()}`,
|
||||
date: Date.now(),
|
||||
status: 'invoice',
|
||||
accountingDocStatus: 'issued' as const,
|
||||
versionInfo: {
|
||||
type: 'final',
|
||||
version: '1.0.0'
|
||||
|
@ -1,144 +1,149 @@
|
||||
import { UBLBaseEncoder } from '../ubl.encoder.js';
|
||||
import { UBLEncoder } from '../generic/ubl.encoder.js';
|
||||
import type { TInvoice, TCreditNote, TDebitNote } from '../../../interfaces/common.js';
|
||||
import { UBLDocumentType } from '../ubl.types.js';
|
||||
import { DOMParser, XMLSerializer } from '../../../plugins.js';
|
||||
|
||||
/**
|
||||
* Encoder for XRechnung (UBL) format
|
||||
* Implements encoding of TInvoice to XRechnung XML
|
||||
* Extends the generic UBL encoder with XRechnung-specific customizations
|
||||
*/
|
||||
export class XRechnungEncoder extends UBLBaseEncoder {
|
||||
export class XRechnungEncoder extends UBLEncoder {
|
||||
/**
|
||||
* Encodes a TCreditNote object to XRechnung XML
|
||||
* @param creditNote TCreditNote object to encode
|
||||
* @returns Promise resolving to XML string
|
||||
* Encodes a credit note into XRechnung XML
|
||||
* @param creditNote Credit note to encode
|
||||
* @returns XRechnung XML string
|
||||
*/
|
||||
protected async encodeCreditNote(creditNote: TCreditNote): Promise<string> {
|
||||
// For now, we'll just return a simple UBL credit note template
|
||||
// In a real implementation, we would generate a proper UBL credit note
|
||||
return `<?xml version="1.0" encoding="UTF-8"?>
|
||||
<CreditNote xmlns="urn:oasis:names:specification:ubl:schema:xsd:CreditNote-2"
|
||||
xmlns:cac="urn:oasis:names:specification:ubl:schema:xsd:CommonAggregateComponents-2"
|
||||
xmlns:cbc="urn:oasis:names:specification:ubl:schema:xsd:CommonBasicComponents-2">
|
||||
<cbc:CustomizationID>urn:cen.eu:en16931:2017#compliant#urn:xoev-de:kosit:standard:xrechnung_2.0</cbc:CustomizationID>
|
||||
<cbc:ID>${creditNote.id}</cbc:ID>
|
||||
<cbc:IssueDate>${this.formatDate(creditNote.date)}</cbc:IssueDate>
|
||||
<cbc:CreditNoteTypeCode>381</cbc:CreditNoteTypeCode>
|
||||
<cbc:DocumentCurrencyCode>${creditNote.currency}</cbc:DocumentCurrencyCode>
|
||||
|
||||
<!-- Rest of the credit note XML would go here -->
|
||||
</CreditNote>`;
|
||||
// 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);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Encodes a TDebitNote object to XRechnung XML
|
||||
* @param debitNote TDebitNote object to encode
|
||||
* @returns Promise resolving to XML string
|
||||
* Encodes a debit note (invoice) into XRechnung XML
|
||||
* @param debitNote Debit note to encode
|
||||
* @returns XRechnung XML string
|
||||
*/
|
||||
protected async encodeDebitNote(debitNote: TDebitNote): Promise<string> {
|
||||
// For now, we'll just return a simple UBL invoice template
|
||||
// In a real implementation, we would generate a proper UBL invoice
|
||||
return `<?xml version="1.0" encoding="UTF-8"?>
|
||||
<Invoice xmlns="urn:oasis:names:specification:ubl:schema:xsd:Invoice-2"
|
||||
xmlns:cac="urn:oasis:names:specification:ubl:schema:xsd:CommonAggregateComponents-2"
|
||||
xmlns:cbc="urn:oasis:names:specification:ubl:schema:xsd:CommonBasicComponents-2">
|
||||
<cbc:CustomizationID>urn:cen.eu:en16931:2017#compliant#urn:xoev-de:kosit:standard:xrechnung_2.0</cbc:CustomizationID>
|
||||
<cbc:ID>${debitNote.id}</cbc:ID>
|
||||
<cbc:IssueDate>${this.formatDate(debitNote.date)}</cbc:IssueDate>
|
||||
<cbc:DueDate>${this.formatDate(debitNote.date + debitNote.dueInDays * 24 * 60 * 60 * 1000)}</cbc:DueDate>
|
||||
<cbc:InvoiceTypeCode>380</cbc:InvoiceTypeCode>
|
||||
<cbc:DocumentCurrencyCode>${debitNote.currency}</cbc:DocumentCurrencyCode>
|
||||
|
||||
<cac:AccountingSupplierParty>
|
||||
<cac:Party>
|
||||
<cac:PartyName>
|
||||
<cbc:Name>${debitNote.from.name}</cbc:Name>
|
||||
</cac:PartyName>
|
||||
<cac:PostalAddress>
|
||||
<cbc:StreetName>${debitNote.from.address.streetName || ''}</cbc:StreetName>
|
||||
<cbc:BuildingNumber>${debitNote.from.address.houseNumber || ''}</cbc:BuildingNumber>
|
||||
<cbc:CityName>${debitNote.from.address.city || ''}</cbc:CityName>
|
||||
<cbc:PostalZone>${debitNote.from.address.postalCode || ''}</cbc:PostalZone>
|
||||
<cac:Country>
|
||||
<cbc:IdentificationCode>${debitNote.from.address.countryCode || ''}</cbc:IdentificationCode>
|
||||
</cac:Country>
|
||||
</cac:PostalAddress>
|
||||
${debitNote.from.registrationDetails?.vatId ? `
|
||||
<cac:PartyTaxScheme>
|
||||
<cbc:CompanyID>${debitNote.from.registrationDetails.vatId}</cbc:CompanyID>
|
||||
<cac:TaxScheme>
|
||||
<cbc:ID>VAT</cbc:ID>
|
||||
</cac:TaxScheme>
|
||||
</cac:PartyTaxScheme>` : ''}
|
||||
${debitNote.from.registrationDetails?.registrationId ? `
|
||||
<cac:PartyLegalEntity>
|
||||
<cbc:RegistrationName>${debitNote.from.registrationDetails.registrationName || debitNote.from.name}</cbc:RegistrationName>
|
||||
<cbc:CompanyID>${debitNote.from.registrationDetails.registrationId}</cbc:CompanyID>
|
||||
</cac:PartyLegalEntity>` : ''}
|
||||
</cac:Party>
|
||||
</cac:AccountingSupplierParty>
|
||||
|
||||
<cac:AccountingCustomerParty>
|
||||
<cac:Party>
|
||||
<cac:PartyName>
|
||||
<cbc:Name>${debitNote.to.name}</cbc:Name>
|
||||
</cac:PartyName>
|
||||
<cac:PostalAddress>
|
||||
<cbc:StreetName>${debitNote.to.address.streetName || ''}</cbc:StreetName>
|
||||
<cbc:BuildingNumber>${debitNote.to.address.houseNumber || ''}</cbc:BuildingNumber>
|
||||
<cbc:CityName>${debitNote.to.address.city || ''}</cbc:CityName>
|
||||
<cbc:PostalZone>${debitNote.to.address.postalCode || ''}</cbc:PostalZone>
|
||||
<cac:Country>
|
||||
<cbc:IdentificationCode>${debitNote.to.address.countryCode || ''}</cbc:IdentificationCode>
|
||||
</cac:Country>
|
||||
</cac:PostalAddress>
|
||||
${debitNote.to.registrationDetails?.vatId ? `
|
||||
<cac:PartyTaxScheme>
|
||||
<cbc:CompanyID>${debitNote.to.registrationDetails.vatId}</cbc:CompanyID>
|
||||
<cac:TaxScheme>
|
||||
<cbc:ID>VAT</cbc:ID>
|
||||
</cac:TaxScheme>
|
||||
</cac:PartyTaxScheme>` : ''}
|
||||
</cac:Party>
|
||||
</cac:AccountingCustomerParty>
|
||||
|
||||
<cac:PaymentTerms>
|
||||
<cbc:Note>Due in ${debitNote.dueInDays} days</cbc:Note>
|
||||
</cac:PaymentTerms>
|
||||
|
||||
<cac:TaxTotal>
|
||||
<cbc:TaxAmount currencyID="${debitNote.currency}">0.00</cbc:TaxAmount>
|
||||
</cac:TaxTotal>
|
||||
|
||||
<cac:LegalMonetaryTotal>
|
||||
<cbc:LineExtensionAmount currencyID="${debitNote.currency}">0.00</cbc:LineExtensionAmount>
|
||||
<cbc:TaxExclusiveAmount currencyID="${debitNote.currency}">0.00</cbc:TaxExclusiveAmount>
|
||||
<cbc:TaxInclusiveAmount currencyID="${debitNote.currency}">0.00</cbc:TaxInclusiveAmount>
|
||||
<cbc:PayableAmount currencyID="${debitNote.currency}">0.00</cbc:PayableAmount>
|
||||
</cac:LegalMonetaryTotal>
|
||||
|
||||
${debitNote.items.map((item, index) => `
|
||||
<cac:InvoiceLine>
|
||||
<cbc:ID>${index + 1}</cbc:ID>
|
||||
<cbc:InvoicedQuantity unitCode="${item.unitType}">${item.unitQuantity}</cbc:InvoicedQuantity>
|
||||
<cbc:LineExtensionAmount currencyID="${debitNote.currency}">${item.unitNetPrice * item.unitQuantity}</cbc:LineExtensionAmount>
|
||||
<cac:Item>
|
||||
<cbc:Name>${item.name}</cbc:Name>
|
||||
${item.articleNumber ? `
|
||||
<cac:SellersItemIdentification>
|
||||
<cbc:ID>${item.articleNumber}</cbc:ID>
|
||||
</cac:SellersItemIdentification>` : ''}
|
||||
<cac:ClassifiedTaxCategory>
|
||||
<cbc:ID>S</cbc:ID>
|
||||
<cbc:Percent>${item.vatPercentage}</cbc:Percent>
|
||||
<cac:TaxScheme>
|
||||
<cbc:ID>VAT</cbc:ID>
|
||||
</cac:TaxScheme>
|
||||
</cac:ClassifiedTaxCategory>
|
||||
</cac:Item>
|
||||
<cac:Price>
|
||||
<cbc:PriceAmount currencyID="${debitNote.currency}">${item.unitNetPrice}</cbc:PriceAmount>
|
||||
</cac:Price>
|
||||
</cac:InvoiceLine>`).join('')}
|
||||
</Invoice>`;
|
||||
// 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();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user