feat(compliance): improve compliance
This commit is contained in:
@ -109,6 +109,9 @@ export class UBLEncoder extends UBLBaseEncoder {
|
||||
|
||||
// Add line items
|
||||
this.addInvoiceLines(doc, root, invoice);
|
||||
|
||||
// Preserve metadata if available
|
||||
this.preserveMetadata(doc, root, invoice);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -516,4 +519,402 @@ export class UBLEncoder extends UBLBaseEncoder {
|
||||
if (!countryName) return 'XX';
|
||||
return countryName.length >= 2 ? countryName.substring(0, 2).toUpperCase() : 'XX';
|
||||
}
|
||||
|
||||
/**
|
||||
* Preserves metadata from invoice to enhance UBL XML output
|
||||
* @param doc XML document
|
||||
* @param root Root element
|
||||
* @param invoice Invoice data
|
||||
*/
|
||||
private preserveMetadata(doc: Document, root: Element, invoice: TInvoice): void {
|
||||
// Extract metadata if available
|
||||
const metadata = (invoice as any).metadata?.extensions;
|
||||
if (!metadata) return;
|
||||
|
||||
// Preserve business references
|
||||
this.addBusinessReferencesToUBL(doc, root, metadata.businessReferences);
|
||||
|
||||
// Preserve payment information
|
||||
this.enhancePaymentInformationUBL(doc, root, metadata.paymentInformation);
|
||||
|
||||
// Preserve date information
|
||||
this.addDateInformationUBL(doc, root, metadata.dateInformation);
|
||||
|
||||
// Enhance party information with contact details
|
||||
this.enhancePartyInformationUBL(doc, invoice);
|
||||
|
||||
// Enhance line items with metadata
|
||||
this.enhanceLineItemsUBL(doc, invoice);
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds business references from metadata to UBL document
|
||||
* @param doc XML document
|
||||
* @param root Root element
|
||||
* @param businessReferences Business references from metadata
|
||||
*/
|
||||
private addBusinessReferencesToUBL(doc: Document, root: Element, businessReferences?: any): void {
|
||||
if (!businessReferences) return;
|
||||
|
||||
// Add OrderReference
|
||||
if (businessReferences.orderReference && !root.getElementsByTagName('cac:OrderReference')[0]) {
|
||||
const orderRef = doc.createElement('cac:OrderReference');
|
||||
const orderId = doc.createElement('cbc:ID');
|
||||
orderId.textContent = businessReferences.orderReference;
|
||||
orderRef.appendChild(orderId);
|
||||
|
||||
// Insert after DocumentCurrencyCode
|
||||
const currencyCode = root.getElementsByTagName('cbc:DocumentCurrencyCode')[0];
|
||||
if (currencyCode && currencyCode.parentNode) {
|
||||
currencyCode.parentNode.insertBefore(orderRef, currencyCode.nextSibling);
|
||||
}
|
||||
}
|
||||
|
||||
// Add ContractDocumentReference
|
||||
if (businessReferences.contractReference && !root.getElementsByTagName('cac:ContractDocumentReference')[0]) {
|
||||
const contractRef = doc.createElement('cac:ContractDocumentReference');
|
||||
const contractId = doc.createElement('cbc:ID');
|
||||
contractId.textContent = businessReferences.contractReference;
|
||||
contractRef.appendChild(contractId);
|
||||
|
||||
// Insert after OrderReference or DocumentCurrencyCode
|
||||
const orderRef = root.getElementsByTagName('cac:OrderReference')[0];
|
||||
const insertAfter = orderRef || root.getElementsByTagName('cbc:DocumentCurrencyCode')[0];
|
||||
if (insertAfter && insertAfter.parentNode) {
|
||||
insertAfter.parentNode.insertBefore(contractRef, insertAfter.nextSibling);
|
||||
}
|
||||
}
|
||||
|
||||
// Add ProjectReference
|
||||
if (businessReferences.projectReference && !root.getElementsByTagName('cac:ProjectReference')[0]) {
|
||||
const projectRef = doc.createElement('cac:ProjectReference');
|
||||
const projectId = doc.createElement('cbc:ID');
|
||||
projectId.textContent = businessReferences.projectReference;
|
||||
projectRef.appendChild(projectId);
|
||||
|
||||
// Insert after ContractDocumentReference or other refs
|
||||
const contractRef = root.getElementsByTagName('cac:ContractDocumentReference')[0];
|
||||
const orderRef = root.getElementsByTagName('cac:OrderReference')[0];
|
||||
const insertAfter = contractRef || orderRef || root.getElementsByTagName('cbc:DocumentCurrencyCode')[0];
|
||||
if (insertAfter && insertAfter.parentNode) {
|
||||
insertAfter.parentNode.insertBefore(projectRef, insertAfter.nextSibling);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Enhances payment information from metadata in UBL document
|
||||
* @param doc XML document
|
||||
* @param root Root element
|
||||
* @param paymentInfo Payment information from metadata
|
||||
*/
|
||||
private enhancePaymentInformationUBL(doc: Document, root: Element, paymentInfo?: any): void {
|
||||
if (!paymentInfo) return;
|
||||
|
||||
let paymentMeans = root.getElementsByTagName('cac:PaymentMeans')[0];
|
||||
|
||||
// Create PaymentMeans if it doesn't exist
|
||||
if (!paymentMeans) {
|
||||
paymentMeans = doc.createElement('cac:PaymentMeans');
|
||||
// Insert before TaxTotal
|
||||
const taxTotal = root.getElementsByTagName('cac:TaxTotal')[0];
|
||||
if (taxTotal && taxTotal.parentNode) {
|
||||
taxTotal.parentNode.insertBefore(paymentMeans, taxTotal);
|
||||
}
|
||||
}
|
||||
|
||||
// Add PaymentMeansCode
|
||||
if (paymentInfo.paymentMeansCode && !paymentMeans.getElementsByTagName('cbc:PaymentMeansCode')[0]) {
|
||||
const meansCode = doc.createElement('cbc:PaymentMeansCode');
|
||||
meansCode.textContent = paymentInfo.paymentMeansCode;
|
||||
paymentMeans.appendChild(meansCode);
|
||||
}
|
||||
|
||||
// Add PaymentID
|
||||
if (paymentInfo.paymentID && !paymentMeans.getElementsByTagName('cbc:PaymentID')[0]) {
|
||||
const paymentId = doc.createElement('cbc:PaymentID');
|
||||
paymentId.textContent = paymentInfo.paymentID;
|
||||
paymentMeans.appendChild(paymentId);
|
||||
}
|
||||
|
||||
// Add IBAN and BIC
|
||||
if (paymentInfo.iban || paymentInfo.bic) {
|
||||
let payeeAccount = paymentMeans.getElementsByTagName('cac:PayeeFinancialAccount')[0];
|
||||
if (!payeeAccount) {
|
||||
payeeAccount = doc.createElement('cac:PayeeFinancialAccount');
|
||||
paymentMeans.appendChild(payeeAccount);
|
||||
}
|
||||
|
||||
// Add IBAN
|
||||
if (paymentInfo.iban && !payeeAccount.getElementsByTagName('cbc:ID')[0]) {
|
||||
const iban = doc.createElement('cbc:ID');
|
||||
iban.textContent = paymentInfo.iban;
|
||||
payeeAccount.appendChild(iban);
|
||||
}
|
||||
|
||||
// Add BIC
|
||||
if (paymentInfo.bic) {
|
||||
let finInstBranch = payeeAccount.getElementsByTagName('cac:FinancialInstitutionBranch')[0];
|
||||
if (!finInstBranch) {
|
||||
finInstBranch = doc.createElement('cac:FinancialInstitutionBranch');
|
||||
payeeAccount.appendChild(finInstBranch);
|
||||
}
|
||||
|
||||
let finInst = finInstBranch.getElementsByTagName('cac:FinancialInstitution')[0];
|
||||
if (!finInst) {
|
||||
finInst = doc.createElement('cac:FinancialInstitution');
|
||||
finInstBranch.appendChild(finInst);
|
||||
}
|
||||
|
||||
if (!finInst.getElementsByTagName('cbc:ID')[0]) {
|
||||
const bic = doc.createElement('cbc:ID');
|
||||
bic.textContent = paymentInfo.bic;
|
||||
finInst.appendChild(bic);
|
||||
}
|
||||
}
|
||||
|
||||
// Add account name
|
||||
if (paymentInfo.accountName && !payeeAccount.getElementsByTagName('cbc:Name')[0]) {
|
||||
const accountName = doc.createElement('cbc:Name');
|
||||
accountName.textContent = paymentInfo.accountName;
|
||||
// Insert after ID
|
||||
const id = payeeAccount.getElementsByTagName('cbc:ID')[0];
|
||||
if (id && id.nextSibling) {
|
||||
payeeAccount.insertBefore(accountName, id.nextSibling);
|
||||
} else {
|
||||
payeeAccount.appendChild(accountName);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Add payment terms with discount if available
|
||||
if (paymentInfo.paymentTermsNote && paymentInfo.paymentTermsNote.includes('early payment')) {
|
||||
let paymentTerms = root.getElementsByTagName('cac:PaymentTerms')[0];
|
||||
if (!paymentTerms) {
|
||||
paymentTerms = doc.createElement('cac:PaymentTerms');
|
||||
// Insert before PaymentMeans
|
||||
const paymentMeans = root.getElementsByTagName('cac:PaymentMeans')[0];
|
||||
if (paymentMeans && paymentMeans.parentNode) {
|
||||
paymentMeans.parentNode.insertBefore(paymentTerms, paymentMeans);
|
||||
}
|
||||
}
|
||||
|
||||
// Update or add note
|
||||
let note = paymentTerms.getElementsByTagName('cbc:Note')[0];
|
||||
if (!note) {
|
||||
note = doc.createElement('cbc:Note');
|
||||
paymentTerms.appendChild(note);
|
||||
}
|
||||
note.textContent = paymentInfo.paymentTermsNote;
|
||||
|
||||
// Add discount percent if available
|
||||
if (paymentInfo.discountPercent && !paymentTerms.getElementsByTagName('cbc:SettlementDiscountPercent')[0]) {
|
||||
const discountElement = doc.createElement('cbc:SettlementDiscountPercent');
|
||||
discountElement.textContent = paymentInfo.discountPercent;
|
||||
paymentTerms.appendChild(discountElement);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds date information from metadata to UBL document
|
||||
* @param doc XML document
|
||||
* @param root Root element
|
||||
* @param dateInfo Date information from metadata
|
||||
*/
|
||||
private addDateInformationUBL(doc: Document, root: Element, dateInfo?: any): void {
|
||||
if (!dateInfo) return;
|
||||
|
||||
// Add InvoicePeriod
|
||||
if ((dateInfo.periodStart || dateInfo.periodEnd) && !root.getElementsByTagName('cac:InvoicePeriod')[0]) {
|
||||
const invoicePeriod = doc.createElement('cac:InvoicePeriod');
|
||||
|
||||
if (dateInfo.periodStart) {
|
||||
const startDate = doc.createElement('cbc:StartDate');
|
||||
startDate.textContent = dateInfo.periodStart;
|
||||
invoicePeriod.appendChild(startDate);
|
||||
}
|
||||
|
||||
if (dateInfo.periodEnd) {
|
||||
const endDate = doc.createElement('cbc:EndDate');
|
||||
endDate.textContent = dateInfo.periodEnd;
|
||||
invoicePeriod.appendChild(endDate);
|
||||
}
|
||||
|
||||
// Insert after business references or DocumentCurrencyCode
|
||||
const projectRef = root.getElementsByTagName('cac:ProjectReference')[0];
|
||||
const contractRef = root.getElementsByTagName('cac:ContractDocumentReference')[0];
|
||||
const orderRef = root.getElementsByTagName('cac:OrderReference')[0];
|
||||
const insertAfter = projectRef || contractRef || orderRef || root.getElementsByTagName('cbc:DocumentCurrencyCode')[0];
|
||||
if (insertAfter && insertAfter.parentNode) {
|
||||
insertAfter.parentNode.insertBefore(invoicePeriod, insertAfter.nextSibling);
|
||||
}
|
||||
}
|
||||
|
||||
// Add Delivery with ActualDeliveryDate
|
||||
if (dateInfo.deliveryDate && !root.getElementsByTagName('cac:Delivery')[0]) {
|
||||
const delivery = doc.createElement('cac:Delivery');
|
||||
const deliveryDate = doc.createElement('cbc:ActualDeliveryDate');
|
||||
deliveryDate.textContent = dateInfo.deliveryDate;
|
||||
delivery.appendChild(deliveryDate);
|
||||
|
||||
// Insert before PaymentMeans
|
||||
const paymentMeans = root.getElementsByTagName('cac:PaymentMeans')[0];
|
||||
if (paymentMeans && paymentMeans.parentNode) {
|
||||
paymentMeans.parentNode.insertBefore(delivery, paymentMeans);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Enhances party information with contact details from metadata
|
||||
* @param doc XML document
|
||||
* @param invoice Invoice data
|
||||
*/
|
||||
private enhancePartyInformationUBL(doc: Document, invoice: TInvoice): void {
|
||||
// Enhance supplier party
|
||||
this.addContactToPartyUBL(doc, 'cac:AccountingSupplierParty', (invoice.from as any)?.metadata?.contactInformation);
|
||||
|
||||
// Enhance customer party
|
||||
this.addContactToPartyUBL(doc, 'cac:AccountingCustomerParty', (invoice.to as any)?.metadata?.contactInformation);
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds contact information to a party in UBL document
|
||||
* @param doc XML document
|
||||
* @param partySelector Party selector
|
||||
* @param contactInfo Contact information from metadata
|
||||
*/
|
||||
private addContactToPartyUBL(doc: Document, partySelector: string, contactInfo?: any): void {
|
||||
if (!contactInfo) return;
|
||||
|
||||
const partyContainer = doc.getElementsByTagName(partySelector)[0];
|
||||
if (!partyContainer) return;
|
||||
|
||||
const party = partyContainer.getElementsByTagName('cac:Party')[0];
|
||||
if (!party) return;
|
||||
|
||||
// Check if Contact already exists
|
||||
let contact = party.getElementsByTagName('cac:Contact')[0];
|
||||
if (!contact && (contactInfo.name || contactInfo.phone || contactInfo.email)) {
|
||||
contact = doc.createElement('cac:Contact');
|
||||
|
||||
// Insert after PartyName
|
||||
const partyName = party.getElementsByTagName('cac:PartyName')[0];
|
||||
if (partyName && partyName.parentNode) {
|
||||
partyName.parentNode.insertBefore(contact, partyName.nextSibling);
|
||||
} else {
|
||||
party.appendChild(contact);
|
||||
}
|
||||
}
|
||||
|
||||
if (contact) {
|
||||
// Add contact name
|
||||
if (contactInfo.name && !contact.getElementsByTagName('cbc:Name')[0]) {
|
||||
const name = doc.createElement('cbc:Name');
|
||||
name.textContent = contactInfo.name;
|
||||
contact.appendChild(name);
|
||||
}
|
||||
|
||||
// Add telephone
|
||||
if (contactInfo.phone && !contact.getElementsByTagName('cbc:Telephone')[0]) {
|
||||
const phone = doc.createElement('cbc:Telephone');
|
||||
phone.textContent = contactInfo.phone;
|
||||
contact.appendChild(phone);
|
||||
}
|
||||
|
||||
// Add email
|
||||
if (contactInfo.email && !contact.getElementsByTagName('cbc:ElectronicMail')[0]) {
|
||||
const email = doc.createElement('cbc:ElectronicMail');
|
||||
email.textContent = contactInfo.email;
|
||||
contact.appendChild(email);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Enhances line items with metadata in UBL document
|
||||
* @param doc XML document
|
||||
* @param invoice Invoice data
|
||||
*/
|
||||
private enhanceLineItemsUBL(doc: Document, invoice: TInvoice): void {
|
||||
const invoiceLines = doc.getElementsByTagName('cac:InvoiceLine');
|
||||
|
||||
for (let i = 0; i < invoiceLines.length && i < invoice.items.length; i++) {
|
||||
const line = invoiceLines[i];
|
||||
const item = invoice.items[i];
|
||||
const itemMetadata = (item as any).metadata;
|
||||
|
||||
if (!itemMetadata) continue;
|
||||
|
||||
const itemElement = line.getElementsByTagName('cac:Item')[0];
|
||||
if (!itemElement) continue;
|
||||
|
||||
// Add item description
|
||||
if (itemMetadata.description && !itemElement.getElementsByTagName('cbc:Description')[0]) {
|
||||
const desc = doc.createElement('cbc:Description');
|
||||
desc.textContent = itemMetadata.description;
|
||||
// Insert before Name
|
||||
const name = itemElement.getElementsByTagName('cbc:Name')[0];
|
||||
if (name && name.parentNode) {
|
||||
name.parentNode.insertBefore(desc, name);
|
||||
} else {
|
||||
itemElement.appendChild(desc);
|
||||
}
|
||||
}
|
||||
|
||||
// Add SellersItemIdentification
|
||||
if (item.articleNumber && !itemElement.getElementsByTagName('cac:SellersItemIdentification')[0]) {
|
||||
const sellerId = doc.createElement('cac:SellersItemIdentification');
|
||||
const id = doc.createElement('cbc:ID');
|
||||
id.textContent = item.articleNumber;
|
||||
sellerId.appendChild(id);
|
||||
itemElement.appendChild(sellerId);
|
||||
}
|
||||
|
||||
// Add BuyersItemIdentification
|
||||
if (itemMetadata.buyerItemID && !itemElement.getElementsByTagName('cac:BuyersItemIdentification')[0]) {
|
||||
const buyerId = doc.createElement('cac:BuyersItemIdentification');
|
||||
const id = doc.createElement('cbc:ID');
|
||||
id.textContent = itemMetadata.buyerItemID;
|
||||
buyerId.appendChild(id);
|
||||
itemElement.appendChild(buyerId);
|
||||
}
|
||||
|
||||
// Add StandardItemIdentification
|
||||
if (itemMetadata.standardItemID && !itemElement.getElementsByTagName('cac:StandardItemIdentification')[0]) {
|
||||
const standardId = doc.createElement('cac:StandardItemIdentification');
|
||||
const id = doc.createElement('cbc:ID');
|
||||
id.textContent = itemMetadata.standardItemID;
|
||||
standardId.appendChild(id);
|
||||
itemElement.appendChild(standardId);
|
||||
}
|
||||
|
||||
// Add CommodityClassification
|
||||
if (itemMetadata.commodityClassification && !itemElement.getElementsByTagName('cac:CommodityClassification')[0]) {
|
||||
const classification = doc.createElement('cac:CommodityClassification');
|
||||
const code = doc.createElement('cbc:ItemClassificationCode');
|
||||
code.textContent = itemMetadata.commodityClassification;
|
||||
classification.appendChild(code);
|
||||
itemElement.appendChild(classification);
|
||||
}
|
||||
|
||||
// Add additional item properties
|
||||
if (itemMetadata.additionalProperties) {
|
||||
for (const [propName, propValue] of Object.entries(itemMetadata.additionalProperties)) {
|
||||
const additionalProp = doc.createElement('cac:AdditionalItemProperty');
|
||||
|
||||
const nameElement = doc.createElement('cbc:Name');
|
||||
nameElement.textContent = propName;
|
||||
additionalProp.appendChild(nameElement);
|
||||
|
||||
const valueElement = doc.createElement('cbc:Value');
|
||||
valueElement.textContent = propValue as string;
|
||||
additionalProp.appendChild(valueElement);
|
||||
|
||||
itemElement.appendChild(additionalProp);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -70,7 +70,11 @@ export class XRechnungDecoder extends UBLBaseDecoder {
|
||||
|
||||
const position = i + 1;
|
||||
const name = this.getText('./cac:Item/cbc:Name', line) || `Item ${position}`;
|
||||
const description = this.getText('./cac:Item/cbc:Description', line) || '';
|
||||
const articleNumber = this.getText('./cac:Item/cac:SellersItemIdentification/cbc:ID', line) || '';
|
||||
const buyerItemID = this.getText('./cac:Item/cac:BuyersItemIdentification/cbc:ID', line) || '';
|
||||
const standardItemID = this.getText('./cac:Item/cac:StandardItemIdentification/cbc:ID', line) || '';
|
||||
const commodityClassification = this.getText('./cac:Item/cac:CommodityClassification/cbc:ItemClassificationCode', line) || '';
|
||||
const unitType = this.getText('./cbc:InvoicedQuantity/@unitCode', line) || 'EA';
|
||||
|
||||
let unitQuantity = 1;
|
||||
@ -91,7 +95,8 @@ export class XRechnungDecoder extends UBLBaseDecoder {
|
||||
vatPercentage = parseFloat(percentText) || 0;
|
||||
}
|
||||
|
||||
items.push({
|
||||
// Create item with extended metadata
|
||||
const item: finance.TAccountingDocItem & { metadata?: any } = {
|
||||
position,
|
||||
name,
|
||||
articleNumber,
|
||||
@ -99,10 +104,57 @@ export class XRechnungDecoder extends UBLBaseDecoder {
|
||||
unitQuantity,
|
||||
unitNetPrice,
|
||||
vatPercentage
|
||||
});
|
||||
};
|
||||
|
||||
// Extract additional item properties
|
||||
const additionalProps: Record<string, string> = {};
|
||||
const propNodes = this.select('./cac:Item/cac:AdditionalItemProperty', line);
|
||||
if (propNodes && Array.isArray(propNodes)) {
|
||||
for (const propNode of propNodes) {
|
||||
const propName = this.getText('./cbc:Name', propNode);
|
||||
const propValue = this.getText('./cbc:Value', propNode);
|
||||
if (propName && propValue) {
|
||||
additionalProps[propName] = propValue;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Store additional item data in metadata
|
||||
if (description || buyerItemID || standardItemID || commodityClassification || Object.keys(additionalProps).length > 0) {
|
||||
item.metadata = {
|
||||
description,
|
||||
buyerItemID,
|
||||
standardItemID,
|
||||
commodityClassification,
|
||||
additionalProperties: additionalProps
|
||||
};
|
||||
}
|
||||
|
||||
items.push(item);
|
||||
}
|
||||
}
|
||||
|
||||
// Extract business references
|
||||
const orderReference = this.getText('//cac:OrderReference/cbc:ID', this.doc);
|
||||
const contractReference = this.getText('//cac:ContractDocumentReference/cbc:ID', this.doc);
|
||||
const projectReference = this.getText('//cac:ProjectReference/cbc:ID', this.doc);
|
||||
|
||||
// Extract payment information
|
||||
const paymentMeansCode = this.getText('//cac:PaymentMeans/cbc:PaymentMeansCode', this.doc);
|
||||
const paymentID = this.getText('//cac:PaymentMeans/cbc:PaymentID', this.doc);
|
||||
const iban = this.getText('//cac:PaymentMeans/cac:PayeeFinancialAccount/cbc:ID', this.doc);
|
||||
const bic = this.getText('//cac:PaymentMeans/cac:PayeeFinancialAccount/cac:FinancialInstitutionBranch/cac:FinancialInstitution/cbc:ID', this.doc);
|
||||
const accountName = this.getText('//cac:PaymentMeans/cac:PayeeFinancialAccount/cbc:Name', this.doc);
|
||||
|
||||
// Extract payment terms with discount
|
||||
const paymentTermsNote = this.getText('//cac:PaymentTerms/cbc:Note', this.doc);
|
||||
const discountPercent = this.getText('//cac:PaymentTerms/cbc:SettlementDiscountPercent', this.doc);
|
||||
|
||||
// Extract period information
|
||||
const periodStart = this.getText('//cac:InvoicePeriod/cbc:StartDate', this.doc);
|
||||
const periodEnd = this.getText('//cac:InvoicePeriod/cbc:EndDate', this.doc);
|
||||
const deliveryDate = this.getText('//cac:Delivery/cbc:ActualDeliveryDate', this.doc);
|
||||
|
||||
// Extract notes
|
||||
const notes: string[] = [];
|
||||
const noteNodes = this.select('//cbc:Note', this.doc);
|
||||
@ -119,8 +171,8 @@ export class XRechnungDecoder extends UBLBaseDecoder {
|
||||
const seller = this.extractParty('//cac:AccountingSupplierParty/cac:Party');
|
||||
const buyer = this.extractParty('//cac:AccountingCustomerParty/cac:Party');
|
||||
|
||||
// Create the common invoice data
|
||||
return {
|
||||
// Create the common invoice data with metadata for business references
|
||||
const invoiceData: any = {
|
||||
type: 'accounting-doc' as const,
|
||||
accountingDocType: 'invoice' as const,
|
||||
id: invoiceId,
|
||||
@ -141,8 +193,35 @@ export class XRechnungDecoder extends UBLBaseDecoder {
|
||||
reverseCharge: false,
|
||||
currency: currencyCode as finance.TCurrency,
|
||||
notes: notes,
|
||||
objectActions: []
|
||||
objectActions: [],
|
||||
metadata: {
|
||||
format: 'xrechnung' as any,
|
||||
version: '1.0.0',
|
||||
extensions: {
|
||||
businessReferences: {
|
||||
orderReference,
|
||||
contractReference,
|
||||
projectReference
|
||||
},
|
||||
paymentInformation: {
|
||||
paymentMeansCode,
|
||||
paymentID,
|
||||
iban,
|
||||
bic,
|
||||
accountName,
|
||||
paymentTermsNote,
|
||||
discountPercent
|
||||
},
|
||||
dateInformation: {
|
||||
periodStart,
|
||||
periodEnd,
|
||||
deliveryDate
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
return invoiceData;
|
||||
} catch (error) {
|
||||
console.error('Error extracting common data:', error);
|
||||
// Return default data
|
||||
@ -190,6 +269,9 @@ export class XRechnungDecoder extends UBLBaseDecoder {
|
||||
let vatId = '';
|
||||
let registrationId = '';
|
||||
let registrationName = '';
|
||||
let contactPhone = '';
|
||||
let contactEmail = '';
|
||||
let contactName = '';
|
||||
|
||||
// Try to extract party information
|
||||
const partyNodes = this.select(partyPath, this.doc);
|
||||
@ -230,9 +312,19 @@ export class XRechnungDecoder extends UBLBaseDecoder {
|
||||
registrationId = this.getText('./cbc:CompanyID', legalEntityNodes[0]) || '';
|
||||
registrationName = this.getText('./cbc:RegistrationName', legalEntityNodes[0]) || name;
|
||||
}
|
||||
|
||||
// Extract contact information
|
||||
const contactNodes = this.select('./cac:Contact', party);
|
||||
if (contactNodes && Array.isArray(contactNodes) && contactNodes.length > 0) {
|
||||
const contact = contactNodes[0];
|
||||
contactPhone = this.getText('./cbc:Telephone', contact) || '';
|
||||
contactEmail = this.getText('./cbc:ElectronicMail', contact) || '';
|
||||
contactName = this.getText('./cbc:Name', contact) || '';
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
// Create contact with additional metadata for contact information
|
||||
const contact: business.TContact & { metadata?: any } = {
|
||||
type: 'company',
|
||||
name: name,
|
||||
description: '',
|
||||
@ -256,6 +348,19 @@ export class XRechnungDecoder extends UBLBaseDecoder {
|
||||
registrationName: registrationName
|
||||
}
|
||||
};
|
||||
|
||||
// Store contact information in metadata if available
|
||||
if (contactPhone || contactEmail || contactName) {
|
||||
contact.metadata = {
|
||||
contactInformation: {
|
||||
phone: contactPhone,
|
||||
email: contactEmail,
|
||||
name: contactName
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
return contact;
|
||||
} catch (error) {
|
||||
console.error('Error extracting party information:', error);
|
||||
return this.createEmptyContact();
|
||||
|
@ -49,6 +49,9 @@ export class XRechnungEncoder extends UBLEncoder {
|
||||
private applyXRechnungCustomizations(doc: Document, invoice: TInvoice): void {
|
||||
const root = doc.documentElement;
|
||||
|
||||
// Extract metadata if available
|
||||
const metadata = (invoice as any).metadata?.extensions;
|
||||
|
||||
// Update Customization ID to XRechnung 2.0
|
||||
const customizationId = root.getElementsByTagName('cbc:CustomizationID')[0];
|
||||
if (customizationId) {
|
||||
@ -95,6 +98,21 @@ export class XRechnungEncoder extends UBLEncoder {
|
||||
|
||||
// Add country code handling for German addresses
|
||||
this.fixGermanCountryCodes(doc);
|
||||
|
||||
// Preserve business references from metadata
|
||||
this.addBusinessReferences(doc, metadata?.businessReferences);
|
||||
|
||||
// Preserve payment information from metadata
|
||||
this.enhancePaymentInformation(doc, metadata?.paymentInformation);
|
||||
|
||||
// Preserve date information from metadata
|
||||
this.addDateInformation(doc, metadata?.dateInformation);
|
||||
|
||||
// Enhance party information with contact details
|
||||
this.enhancePartyInformation(doc, invoice);
|
||||
|
||||
// Enhance line items with metadata
|
||||
this.enhanceLineItems(doc, invoice);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -146,4 +164,377 @@ export class XRechnungEncoder extends UBLEncoder {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds business references from metadata to the document
|
||||
* @param doc XML document
|
||||
* @param businessReferences Business references from metadata
|
||||
*/
|
||||
private addBusinessReferences(doc: Document, businessReferences?: any): void {
|
||||
if (!businessReferences) return;
|
||||
|
||||
const root = doc.documentElement;
|
||||
|
||||
// Add OrderReference
|
||||
if (businessReferences.orderReference && !root.getElementsByTagName('cac:OrderReference')[0]) {
|
||||
const orderRef = doc.createElement('cac:OrderReference');
|
||||
const orderId = doc.createElement('cbc:ID');
|
||||
orderId.textContent = businessReferences.orderReference;
|
||||
orderRef.appendChild(orderId);
|
||||
|
||||
// Insert after DocumentCurrencyCode
|
||||
const currencyCode = root.getElementsByTagName('cbc:DocumentCurrencyCode')[0];
|
||||
if (currencyCode && currencyCode.parentNode) {
|
||||
currencyCode.parentNode.insertBefore(orderRef, currencyCode.nextSibling);
|
||||
}
|
||||
}
|
||||
|
||||
// Add ContractDocumentReference
|
||||
if (businessReferences.contractReference && !root.getElementsByTagName('cac:ContractDocumentReference')[0]) {
|
||||
const contractRef = doc.createElement('cac:ContractDocumentReference');
|
||||
const contractId = doc.createElement('cbc:ID');
|
||||
contractId.textContent = businessReferences.contractReference;
|
||||
contractRef.appendChild(contractId);
|
||||
|
||||
// Insert after OrderReference or DocumentCurrencyCode
|
||||
const orderRef = root.getElementsByTagName('cac:OrderReference')[0];
|
||||
const insertAfter = orderRef || root.getElementsByTagName('cbc:DocumentCurrencyCode')[0];
|
||||
if (insertAfter && insertAfter.parentNode) {
|
||||
insertAfter.parentNode.insertBefore(contractRef, insertAfter.nextSibling);
|
||||
}
|
||||
}
|
||||
|
||||
// Add ProjectReference
|
||||
if (businessReferences.projectReference && !root.getElementsByTagName('cac:ProjectReference')[0]) {
|
||||
const projectRef = doc.createElement('cac:ProjectReference');
|
||||
const projectId = doc.createElement('cbc:ID');
|
||||
projectId.textContent = businessReferences.projectReference;
|
||||
projectRef.appendChild(projectId);
|
||||
|
||||
// Insert after ContractDocumentReference or other refs
|
||||
const contractRef = root.getElementsByTagName('cac:ContractDocumentReference')[0];
|
||||
const orderRef = root.getElementsByTagName('cac:OrderReference')[0];
|
||||
const insertAfter = contractRef || orderRef || root.getElementsByTagName('cbc:DocumentCurrencyCode')[0];
|
||||
if (insertAfter && insertAfter.parentNode) {
|
||||
insertAfter.parentNode.insertBefore(projectRef, insertAfter.nextSibling);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Enhances payment information from metadata
|
||||
* @param doc XML document
|
||||
* @param paymentInfo Payment information from metadata
|
||||
*/
|
||||
private enhancePaymentInformation(doc: Document, paymentInfo?: any): void {
|
||||
if (!paymentInfo) return;
|
||||
|
||||
const root = doc.documentElement;
|
||||
let paymentMeans = root.getElementsByTagName('cac:PaymentMeans')[0];
|
||||
|
||||
// Create PaymentMeans if it doesn't exist
|
||||
if (!paymentMeans) {
|
||||
paymentMeans = doc.createElement('cac:PaymentMeans');
|
||||
// Insert before TaxTotal
|
||||
const taxTotal = root.getElementsByTagName('cac:TaxTotal')[0];
|
||||
if (taxTotal && taxTotal.parentNode) {
|
||||
taxTotal.parentNode.insertBefore(paymentMeans, taxTotal);
|
||||
}
|
||||
}
|
||||
|
||||
// Add PaymentMeansCode
|
||||
if (paymentInfo.paymentMeansCode && !paymentMeans.getElementsByTagName('cbc:PaymentMeansCode')[0]) {
|
||||
const meansCode = doc.createElement('cbc:PaymentMeansCode');
|
||||
meansCode.textContent = paymentInfo.paymentMeansCode;
|
||||
paymentMeans.appendChild(meansCode);
|
||||
}
|
||||
|
||||
// Add PaymentID
|
||||
if (paymentInfo.paymentID && !paymentMeans.getElementsByTagName('cbc:PaymentID')[0]) {
|
||||
const paymentId = doc.createElement('cbc:PaymentID');
|
||||
paymentId.textContent = paymentInfo.paymentID;
|
||||
paymentMeans.appendChild(paymentId);
|
||||
}
|
||||
|
||||
// Add IBAN and BIC
|
||||
if (paymentInfo.iban || paymentInfo.bic) {
|
||||
let payeeAccount = paymentMeans.getElementsByTagName('cac:PayeeFinancialAccount')[0];
|
||||
if (!payeeAccount) {
|
||||
payeeAccount = doc.createElement('cac:PayeeFinancialAccount');
|
||||
paymentMeans.appendChild(payeeAccount);
|
||||
}
|
||||
|
||||
// Add IBAN
|
||||
if (paymentInfo.iban && !payeeAccount.getElementsByTagName('cbc:ID')[0]) {
|
||||
const iban = doc.createElement('cbc:ID');
|
||||
iban.textContent = paymentInfo.iban;
|
||||
payeeAccount.appendChild(iban);
|
||||
}
|
||||
|
||||
// Add BIC
|
||||
if (paymentInfo.bic) {
|
||||
let finInstBranch = payeeAccount.getElementsByTagName('cac:FinancialInstitutionBranch')[0];
|
||||
if (!finInstBranch) {
|
||||
finInstBranch = doc.createElement('cac:FinancialInstitutionBranch');
|
||||
payeeAccount.appendChild(finInstBranch);
|
||||
}
|
||||
|
||||
let finInst = finInstBranch.getElementsByTagName('cac:FinancialInstitution')[0];
|
||||
if (!finInst) {
|
||||
finInst = doc.createElement('cac:FinancialInstitution');
|
||||
finInstBranch.appendChild(finInst);
|
||||
}
|
||||
|
||||
if (!finInst.getElementsByTagName('cbc:ID')[0]) {
|
||||
const bic = doc.createElement('cbc:ID');
|
||||
bic.textContent = paymentInfo.bic;
|
||||
finInst.appendChild(bic);
|
||||
}
|
||||
}
|
||||
|
||||
// Add account name
|
||||
if (paymentInfo.accountName && !payeeAccount.getElementsByTagName('cbc:Name')[0]) {
|
||||
const accountName = doc.createElement('cbc:Name');
|
||||
accountName.textContent = paymentInfo.accountName;
|
||||
// Insert after ID
|
||||
const id = payeeAccount.getElementsByTagName('cbc:ID')[0];
|
||||
if (id && id.nextSibling) {
|
||||
payeeAccount.insertBefore(accountName, id.nextSibling);
|
||||
} else {
|
||||
payeeAccount.appendChild(accountName);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Add payment terms with discount if available
|
||||
if (paymentInfo.paymentTermsNote && paymentInfo.paymentTermsNote.includes('early payment')) {
|
||||
let paymentTerms = root.getElementsByTagName('cac:PaymentTerms')[0];
|
||||
if (!paymentTerms) {
|
||||
paymentTerms = doc.createElement('cac:PaymentTerms');
|
||||
// Insert before PaymentMeans
|
||||
const paymentMeans = root.getElementsByTagName('cac:PaymentMeans')[0];
|
||||
if (paymentMeans && paymentMeans.parentNode) {
|
||||
paymentMeans.parentNode.insertBefore(paymentTerms, paymentMeans);
|
||||
}
|
||||
}
|
||||
|
||||
// Update or add note
|
||||
let note = paymentTerms.getElementsByTagName('cbc:Note')[0];
|
||||
if (!note) {
|
||||
note = doc.createElement('cbc:Note');
|
||||
paymentTerms.appendChild(note);
|
||||
}
|
||||
note.textContent = paymentInfo.paymentTermsNote;
|
||||
|
||||
// Add discount percent if available
|
||||
if (paymentInfo.discountPercent && !paymentTerms.getElementsByTagName('cbc:SettlementDiscountPercent')[0]) {
|
||||
const discountElement = doc.createElement('cbc:SettlementDiscountPercent');
|
||||
discountElement.textContent = paymentInfo.discountPercent;
|
||||
paymentTerms.appendChild(discountElement);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds date information from metadata
|
||||
* @param doc XML document
|
||||
* @param dateInfo Date information from metadata
|
||||
*/
|
||||
private addDateInformation(doc: Document, dateInfo?: any): void {
|
||||
if (!dateInfo) return;
|
||||
|
||||
const root = doc.documentElement;
|
||||
|
||||
// Add InvoicePeriod
|
||||
if ((dateInfo.periodStart || dateInfo.periodEnd) && !root.getElementsByTagName('cac:InvoicePeriod')[0]) {
|
||||
const invoicePeriod = doc.createElement('cac:InvoicePeriod');
|
||||
|
||||
if (dateInfo.periodStart) {
|
||||
const startDate = doc.createElement('cbc:StartDate');
|
||||
startDate.textContent = dateInfo.periodStart;
|
||||
invoicePeriod.appendChild(startDate);
|
||||
}
|
||||
|
||||
if (dateInfo.periodEnd) {
|
||||
const endDate = doc.createElement('cbc:EndDate');
|
||||
endDate.textContent = dateInfo.periodEnd;
|
||||
invoicePeriod.appendChild(endDate);
|
||||
}
|
||||
|
||||
// Insert after business references or DocumentCurrencyCode
|
||||
const projectRef = root.getElementsByTagName('cac:ProjectReference')[0];
|
||||
const contractRef = root.getElementsByTagName('cac:ContractDocumentReference')[0];
|
||||
const orderRef = root.getElementsByTagName('cac:OrderReference')[0];
|
||||
const insertAfter = projectRef || contractRef || orderRef || root.getElementsByTagName('cbc:DocumentCurrencyCode')[0];
|
||||
if (insertAfter && insertAfter.parentNode) {
|
||||
insertAfter.parentNode.insertBefore(invoicePeriod, insertAfter.nextSibling);
|
||||
}
|
||||
}
|
||||
|
||||
// Add Delivery with ActualDeliveryDate
|
||||
if (dateInfo.deliveryDate && !root.getElementsByTagName('cac:Delivery')[0]) {
|
||||
const delivery = doc.createElement('cac:Delivery');
|
||||
const deliveryDate = doc.createElement('cbc:ActualDeliveryDate');
|
||||
deliveryDate.textContent = dateInfo.deliveryDate;
|
||||
delivery.appendChild(deliveryDate);
|
||||
|
||||
// Insert before PaymentMeans
|
||||
const paymentMeans = root.getElementsByTagName('cac:PaymentMeans')[0];
|
||||
if (paymentMeans && paymentMeans.parentNode) {
|
||||
paymentMeans.parentNode.insertBefore(delivery, paymentMeans);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Enhances party information with contact details from metadata
|
||||
* @param doc XML document
|
||||
* @param invoice Invoice data
|
||||
*/
|
||||
private enhancePartyInformation(doc: Document, invoice: TInvoice): void {
|
||||
// Enhance supplier party
|
||||
this.addContactToParty(doc, 'cac:AccountingSupplierParty', (invoice.from as any)?.metadata?.contactInformation);
|
||||
|
||||
// Enhance customer party
|
||||
this.addContactToParty(doc, 'cac:AccountingCustomerParty', (invoice.to as any)?.metadata?.contactInformation);
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds contact information to a party
|
||||
* @param doc XML document
|
||||
* @param partySelector Party selector
|
||||
* @param contactInfo Contact information from metadata
|
||||
*/
|
||||
private addContactToParty(doc: Document, partySelector: string, contactInfo?: any): void {
|
||||
if (!contactInfo) return;
|
||||
|
||||
const partyContainer = doc.getElementsByTagName(partySelector)[0];
|
||||
if (!partyContainer) return;
|
||||
|
||||
const party = partyContainer.getElementsByTagName('cac:Party')[0];
|
||||
if (!party) return;
|
||||
|
||||
// Check if Contact already exists
|
||||
let contact = party.getElementsByTagName('cac:Contact')[0];
|
||||
if (!contact && (contactInfo.name || contactInfo.phone || contactInfo.email)) {
|
||||
contact = doc.createElement('cac:Contact');
|
||||
|
||||
// Insert after PartyName
|
||||
const partyName = party.getElementsByTagName('cac:PartyName')[0];
|
||||
if (partyName && partyName.parentNode) {
|
||||
partyName.parentNode.insertBefore(contact, partyName.nextSibling);
|
||||
} else {
|
||||
party.appendChild(contact);
|
||||
}
|
||||
}
|
||||
|
||||
if (contact) {
|
||||
// Add contact name
|
||||
if (contactInfo.name && !contact.getElementsByTagName('cbc:Name')[0]) {
|
||||
const name = doc.createElement('cbc:Name');
|
||||
name.textContent = contactInfo.name;
|
||||
contact.appendChild(name);
|
||||
}
|
||||
|
||||
// Add telephone
|
||||
if (contactInfo.phone && !contact.getElementsByTagName('cbc:Telephone')[0]) {
|
||||
const phone = doc.createElement('cbc:Telephone');
|
||||
phone.textContent = contactInfo.phone;
|
||||
contact.appendChild(phone);
|
||||
}
|
||||
|
||||
// Add email
|
||||
if (contactInfo.email && !contact.getElementsByTagName('cbc:ElectronicMail')[0]) {
|
||||
const email = doc.createElement('cbc:ElectronicMail');
|
||||
email.textContent = contactInfo.email;
|
||||
contact.appendChild(email);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Enhances line items with metadata
|
||||
* @param doc XML document
|
||||
* @param invoice Invoice data
|
||||
*/
|
||||
private enhanceLineItems(doc: Document, invoice: TInvoice): void {
|
||||
const invoiceLines = doc.getElementsByTagName('cac:InvoiceLine');
|
||||
|
||||
for (let i = 0; i < invoiceLines.length && i < invoice.items.length; i++) {
|
||||
const line = invoiceLines[i];
|
||||
const item = invoice.items[i];
|
||||
const itemMetadata = (item as any).metadata;
|
||||
|
||||
if (!itemMetadata) continue;
|
||||
|
||||
const itemElement = line.getElementsByTagName('cac:Item')[0];
|
||||
if (!itemElement) continue;
|
||||
|
||||
// Add item description
|
||||
if (itemMetadata.description && !itemElement.getElementsByTagName('cbc:Description')[0]) {
|
||||
const desc = doc.createElement('cbc:Description');
|
||||
desc.textContent = itemMetadata.description;
|
||||
// Insert before Name
|
||||
const name = itemElement.getElementsByTagName('cbc:Name')[0];
|
||||
if (name && name.parentNode) {
|
||||
name.parentNode.insertBefore(desc, name);
|
||||
} else {
|
||||
itemElement.appendChild(desc);
|
||||
}
|
||||
}
|
||||
|
||||
// Add SellersItemIdentification
|
||||
if (itemMetadata.buyerItemID && !itemElement.getElementsByTagName('cac:SellersItemIdentification')[0]) {
|
||||
const sellerId = doc.createElement('cac:SellersItemIdentification');
|
||||
const id = doc.createElement('cbc:ID');
|
||||
id.textContent = item.articleNumber || itemMetadata.buyerItemID;
|
||||
sellerId.appendChild(id);
|
||||
itemElement.appendChild(sellerId);
|
||||
}
|
||||
|
||||
// Add BuyersItemIdentification
|
||||
if (itemMetadata.buyerItemID && !itemElement.getElementsByTagName('cac:BuyersItemIdentification')[0]) {
|
||||
const buyerId = doc.createElement('cac:BuyersItemIdentification');
|
||||
const id = doc.createElement('cbc:ID');
|
||||
id.textContent = itemMetadata.buyerItemID;
|
||||
buyerId.appendChild(id);
|
||||
itemElement.appendChild(buyerId);
|
||||
}
|
||||
|
||||
// Add StandardItemIdentification
|
||||
if (itemMetadata.standardItemID && !itemElement.getElementsByTagName('cac:StandardItemIdentification')[0]) {
|
||||
const standardId = doc.createElement('cac:StandardItemIdentification');
|
||||
const id = doc.createElement('cbc:ID');
|
||||
id.textContent = itemMetadata.standardItemID;
|
||||
standardId.appendChild(id);
|
||||
itemElement.appendChild(standardId);
|
||||
}
|
||||
|
||||
// Add CommodityClassification
|
||||
if (itemMetadata.commodityClassification && !itemElement.getElementsByTagName('cac:CommodityClassification')[0]) {
|
||||
const classification = doc.createElement('cac:CommodityClassification');
|
||||
const code = doc.createElement('cbc:ItemClassificationCode');
|
||||
code.textContent = itemMetadata.commodityClassification;
|
||||
classification.appendChild(code);
|
||||
itemElement.appendChild(classification);
|
||||
}
|
||||
|
||||
// Add additional item properties
|
||||
if (itemMetadata.additionalProperties) {
|
||||
for (const [propName, propValue] of Object.entries(itemMetadata.additionalProperties)) {
|
||||
const additionalProp = doc.createElement('cac:AdditionalItemProperty');
|
||||
|
||||
const nameElement = doc.createElement('cbc:Name');
|
||||
nameElement.textContent = propName;
|
||||
additionalProp.appendChild(nameElement);
|
||||
|
||||
const valueElement = doc.createElement('cbc:Value');
|
||||
valueElement.textContent = propValue as string;
|
||||
additionalProp.appendChild(valueElement);
|
||||
|
||||
itemElement.appendChild(additionalProp);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user