619 lines
25 KiB
TypeScript
619 lines
25 KiB
TypeScript
import { UBLEncoder } from '../generic/ubl.encoder.js';
|
|
import type { TInvoice, TCreditNote, TDebitNote } from '../../../interfaces/common.js';
|
|
import { DOMParser, XMLSerializer } from '../../../plugins.js';
|
|
|
|
/**
|
|
* Encoder for XRechnung (UBL) format
|
|
* Extends the generic UBL encoder with XRechnung-specific customizations
|
|
*/
|
|
export class XRechnungEncoder extends UBLEncoder {
|
|
/**
|
|
* Encodes a credit note into XRechnung XML
|
|
* @param creditNote Credit note to encode
|
|
* @returns XRechnung XML string
|
|
*/
|
|
protected async encodeCreditNote(creditNote: TCreditNote): Promise<string> {
|
|
// 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 debit note (invoice) into XRechnung XML
|
|
* @param debitNote Debit note to encode
|
|
* @returns XRechnung XML string
|
|
*/
|
|
protected async encodeDebitNote(debitNote: TDebitNote): Promise<string> {
|
|
// 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;
|
|
|
|
// 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) {
|
|
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];
|
|
const buyerReferenceValue = (invoice as any).buyerReference || metadata?.businessReferences?.buyerReference || invoice.id;
|
|
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 = buyerReferenceValue;
|
|
currencyCode.parentNode!.insertBefore(buyerRef, currencyCode.nextSibling);
|
|
}
|
|
} else if (!buyerRef.textContent || buyerRef.textContent.trim() === '') {
|
|
buyerRef.textContent = buyerReferenceValue;
|
|
}
|
|
|
|
// 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);
|
|
|
|
// 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);
|
|
}
|
|
|
|
/**
|
|
* 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);
|
|
}
|
|
}
|
|
|
|
// Add GLN (Global Location Number) if available
|
|
if (party.gln && !existingEndpoint) {
|
|
const endpointNode = doc.createElement('cbc:EndpointID');
|
|
endpointNode.setAttribute('schemeID', '0088'); // GLN scheme ID
|
|
endpointNode.textContent = party.gln;
|
|
|
|
// Insert as first child of party element
|
|
if (partyElement.firstChild) {
|
|
partyElement.insertBefore(endpointNode, partyElement.firstChild);
|
|
} else {
|
|
partyElement.appendChild(endpointNode);
|
|
}
|
|
}
|
|
|
|
// Add PartyIdentification for additional identifiers
|
|
if (party.additionalIdentifiers) {
|
|
for (const identifier of party.additionalIdentifiers) {
|
|
const partyId = doc.createElement('cac:PartyIdentification');
|
|
const id = doc.createElement('cbc:ID');
|
|
if (identifier.scheme) {
|
|
id.setAttribute('schemeID', identifier.scheme);
|
|
}
|
|
id.textContent = identifier.value;
|
|
partyId.appendChild(id);
|
|
|
|
// Insert after EndpointID or at beginning
|
|
const endpoint = partyElement.getElementsByTagName('cbc:EndpointID')[0];
|
|
if (endpoint && endpoint.nextSibling) {
|
|
partyElement.insertBefore(partyId, endpoint.nextSibling);
|
|
} else if (partyElement.firstChild) {
|
|
partyElement.insertBefore(partyId, partyElement.firstChild);
|
|
} else {
|
|
partyElement.appendChild(partyId);
|
|
}
|
|
}
|
|
}
|
|
|
|
// Add company registration number to PartyLegalEntity
|
|
if (party.registrationDetails?.registrationId) {
|
|
let legalEntity = partyElement.getElementsByTagName('cac:PartyLegalEntity')[0];
|
|
if (!legalEntity) {
|
|
legalEntity = doc.createElement('cac:PartyLegalEntity');
|
|
// Insert after PostalAddress
|
|
const postalAddress = partyElement.getElementsByTagName('cac:PostalAddress')[0];
|
|
if (postalAddress && postalAddress.nextSibling) {
|
|
partyElement.insertBefore(legalEntity, postalAddress.nextSibling);
|
|
} else {
|
|
partyElement.appendChild(legalEntity);
|
|
}
|
|
}
|
|
|
|
// Add registration name if not present
|
|
if (!legalEntity.getElementsByTagName('cbc:RegistrationName')[0]) {
|
|
const regName = doc.createElement('cbc:RegistrationName');
|
|
regName.textContent = party.registrationDetails.registrationName || party.name;
|
|
legalEntity.appendChild(regName);
|
|
}
|
|
|
|
// Add company ID if not present
|
|
if (!legalEntity.getElementsByTagName('cbc:CompanyID')[0]) {
|
|
const companyId = doc.createElement('cbc:CompanyID');
|
|
companyId.textContent = party.registrationDetails.registrationId;
|
|
legalEntity.appendChild(companyId);
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 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();
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 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 PaymentDueDate
|
|
if (paymentInfo.paymentDueDate && !paymentMeans.getElementsByTagName('cbc:PaymentDueDate')[0]) {
|
|
const dueDate = doc.createElement('cbc:PaymentDueDate');
|
|
dueDate.textContent = paymentInfo.paymentDueDate;
|
|
paymentMeans.appendChild(dueDate);
|
|
}
|
|
|
|
// 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 account name (must come after ID but before FinancialInstitutionBranch)
|
|
if (paymentInfo.accountName && !payeeAccount.getElementsByTagName('cbc:Name')[0]) {
|
|
const accountName = doc.createElement('cbc:Name');
|
|
accountName.textContent = paymentInfo.accountName;
|
|
// Insert after ID but before FinancialInstitutionBranch
|
|
const id = payeeAccount.getElementsByTagName('cbc:ID')[0];
|
|
const finInstBranch = payeeAccount.getElementsByTagName('cac:FinancialInstitutionBranch')[0];
|
|
if (finInstBranch) {
|
|
payeeAccount.insertBefore(accountName, finInstBranch);
|
|
} else if (id && id.nextSibling) {
|
|
payeeAccount.insertBefore(accountName, id.nextSibling);
|
|
} else {
|
|
payeeAccount.appendChild(accountName);
|
|
}
|
|
}
|
|
|
|
// Add BIC and bank name
|
|
if (paymentInfo.bic || paymentInfo.bankName) {
|
|
let finInstBranch = payeeAccount.getElementsByTagName('cac:FinancialInstitutionBranch')[0];
|
|
if (!finInstBranch) {
|
|
finInstBranch = doc.createElement('cac:FinancialInstitutionBranch');
|
|
payeeAccount.appendChild(finInstBranch);
|
|
}
|
|
|
|
// Add BIC as branch ID
|
|
if (paymentInfo.bic && !finInstBranch.getElementsByTagName('cbc:ID')[0]) {
|
|
const bicElement = doc.createElement('cbc:ID');
|
|
bicElement.textContent = paymentInfo.bic;
|
|
finInstBranch.appendChild(bicElement);
|
|
}
|
|
|
|
// Add bank name
|
|
if (paymentInfo.bankName && !finInstBranch.getElementsByTagName('cbc:Name')[0]) {
|
|
const bankNameElement = doc.createElement('cbc:Name');
|
|
bankNameElement.textContent = paymentInfo.bankName;
|
|
finInstBranch.appendChild(bankNameElement);
|
|
}
|
|
}
|
|
}
|
|
|
|
// 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);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
} |