xinvoice/ts/formats/ubl/xrechnung/xrechnung.decoder.ts

293 lines
9.0 KiB
TypeScript

import { UBLBaseDecoder } from '../ubl.decoder.js';
import type { TInvoice, TCreditNote, TDebitNote } from '../../../interfaces/common.js';
import { business, finance } from '../../../plugins.js';
import { UBLDocumentType } from '../ubl.types.js';
/**
* Decoder for XRechnung (UBL) format
* Implements decoding of XRechnung invoices to TInvoice
*/
export class XRechnungDecoder extends UBLBaseDecoder {
/**
* Decodes a UBL credit note
* @returns Promise resolving to a TCreditNote object
*/
protected async decodeCreditNote(): Promise<TCreditNote> {
// Extract common data
const commonData = await this.extractCommonData();
// Return the invoice data as a credit note
return {
...commonData,
invoiceType: 'creditnote'
} as TCreditNote;
}
/**
* Decodes a UBL debit note (invoice)
* @returns Promise resolving to a TDebitNote object
*/
protected async decodeDebitNote(): Promise<TDebitNote> {
// Extract common data
const commonData = await this.extractCommonData();
// Return the invoice data as a debit note
return {
...commonData,
invoiceType: 'debitnote'
} as TDebitNote;
}
/**
* Extracts common invoice data from XRechnung XML
* @returns Common invoice data
*/
private async extractCommonData(): Promise<Partial<TInvoice>> {
try {
// Default values
const invoiceId = this.getText('//cbc:ID', this.doc) || `INV-${Date.now()}`;
const issueDateText = this.getText('//cbc:IssueDate', this.doc);
const issueDate = issueDateText ? new Date(issueDateText).getTime() : Date.now();
const currencyCode = this.getText('//cbc:DocumentCurrencyCode', this.doc) || 'EUR';
// Extract payment terms
let dueInDays = 30; // Default
const dueDateText = this.getText('//cac:PaymentTerms/cbc:PaymentDueDate', this.doc);
if (dueDateText) {
const dueDateObj = new Date(dueDateText);
const issueDateObj = new Date(issueDate);
const diffTime = Math.abs(dueDateObj.getTime() - issueDateObj.getTime());
dueInDays = Math.ceil(diffTime / (1000 * 60 * 60 * 24));
}
// Extract items
const items: finance.TInvoiceItem[] = [];
const invoiceLines = this.select('//cac:InvoiceLine', this.doc);
if (invoiceLines && Array.isArray(invoiceLines)) {
for (let i = 0; i < invoiceLines.length; i++) {
const line = invoiceLines[i];
const position = i + 1;
const name = this.getText('./cac:Item/cbc:Name', line) || `Item ${position}`;
const articleNumber = this.getText('./cac:Item/cac:SellersItemIdentification/cbc:ID', line) || '';
const unitType = this.getText('./cbc:InvoicedQuantity/@unitCode', line) || 'EA';
let unitQuantity = 1;
const quantityText = this.getText('./cbc:InvoicedQuantity', line);
if (quantityText) {
unitQuantity = parseFloat(quantityText) || 1;
}
let unitNetPrice = 0;
const priceText = this.getText('./cac:Price/cbc:PriceAmount', line);
if (priceText) {
unitNetPrice = parseFloat(priceText) || 0;
}
let vatPercentage = 0;
const percentText = this.getText('./cac:Item/cac:ClassifiedTaxCategory/cbc:Percent', line);
if (percentText) {
vatPercentage = parseFloat(percentText) || 0;
}
items.push({
position,
name,
articleNumber,
unitType,
unitQuantity,
unitNetPrice,
vatPercentage
});
}
}
// Extract notes
const notes: string[] = [];
const noteNodes = this.select('//cbc:Note', this.doc);
if (noteNodes && Array.isArray(noteNodes)) {
for (let i = 0; i < noteNodes.length; i++) {
const noteText = noteNodes[i].textContent || '';
if (noteText) {
notes.push(noteText);
}
}
}
// Extract seller and buyer information
const seller = this.extractParty('//cac:AccountingSupplierParty/cac:Party');
const buyer = this.extractParty('//cac:AccountingCustomerParty/cac:Party');
// Create the common invoice data
return {
type: 'invoice',
id: invoiceId,
invoiceId: invoiceId,
date: issueDate,
status: 'invoice',
versionInfo: {
type: 'final',
version: '1.0.0'
},
language: 'en',
incidenceId: invoiceId,
from: seller,
to: buyer,
subject: `Invoice ${invoiceId}`,
items: items,
dueInDays: dueInDays,
reverseCharge: false,
currency: currencyCode as finance.TCurrency,
notes: notes,
objectActions: []
};
} catch (error) {
console.error('Error extracting common data:', error);
// Return default data
return {
type: 'invoice',
id: `INV-${Date.now()}`,
invoiceId: `INV-${Date.now()}`,
date: Date.now(),
status: 'invoice',
versionInfo: {
type: 'final',
version: '1.0.0'
},
language: 'en',
incidenceId: `INV-${Date.now()}`,
from: this.createEmptyContact(),
to: this.createEmptyContact(),
subject: 'Invoice',
items: [],
dueInDays: 30,
reverseCharge: false,
currency: 'EUR',
notes: [],
objectActions: []
};
}
}
/**
* Extracts party information from XML
* @param partyPath XPath to the party element
* @returns TContact object
*/
private extractParty(partyPath: string): business.TContact {
try {
// Default values
let name = '';
let streetName = '';
let houseNumber = '0';
let city = '';
let postalCode = '';
let country = '';
let countryCode = '';
let vatId = '';
let registrationId = '';
let registrationName = '';
// Try to extract party information
const partyNodes = this.select(partyPath, this.doc);
if (partyNodes && Array.isArray(partyNodes) && partyNodes.length > 0) {
const party = partyNodes[0];
// Extract name
name = this.getText('./cac:PartyName/cbc:Name', party) || '';
// Extract address
const addressNodes = this.select('./cac:PostalAddress', party);
if (addressNodes && Array.isArray(addressNodes) && addressNodes.length > 0) {
const address = addressNodes[0];
streetName = this.getText('./cbc:StreetName', address) || '';
houseNumber = this.getText('./cbc:BuildingNumber', address) || '0';
city = this.getText('./cbc:CityName', address) || '';
postalCode = this.getText('./cbc:PostalZone', address) || '';
const countryNodes = this.select('./cac:Country', address);
if (countryNodes && Array.isArray(countryNodes) && countryNodes.length > 0) {
const countryNode = countryNodes[0];
country = this.getText('./cbc:Name', countryNode) || '';
countryCode = this.getText('./cbc:IdentificationCode', countryNode) || '';
}
}
// Extract tax information
const taxSchemeNodes = this.select('./cac:PartyTaxScheme', party);
if (taxSchemeNodes && Array.isArray(taxSchemeNodes) && taxSchemeNodes.length > 0) {
vatId = this.getText('./cbc:CompanyID', taxSchemeNodes[0]) || '';
}
// Extract registration information
const legalEntityNodes = this.select('./cac:PartyLegalEntity', party);
if (legalEntityNodes && Array.isArray(legalEntityNodes) && legalEntityNodes.length > 0) {
registrationId = this.getText('./cbc:CompanyID', legalEntityNodes[0]) || '';
registrationName = this.getText('./cbc:RegistrationName', legalEntityNodes[0]) || name;
}
}
return {
type: 'company',
name: name,
description: '',
address: {
streetName: streetName,
houseNumber: houseNumber,
city: city,
postalCode: postalCode,
country: country,
countryCode: countryCode
},
status: 'active',
foundedDate: {
year: 2000,
month: 1,
day: 1
},
registrationDetails: {
vatId: vatId,
registrationId: registrationId,
registrationName: registrationName
}
};
} catch (error) {
console.error('Error extracting party information:', error);
return this.createEmptyContact();
}
}
/**
* Creates an empty TContact object
* @returns Empty TContact object
*/
private createEmptyContact(): business.TContact {
return {
type: 'company',
name: '',
description: '',
address: {
streetName: '',
houseNumber: '0',
city: '',
country: '',
postalCode: ''
},
status: 'active',
foundedDate: {
year: 2000,
month: 1,
day: 1
},
registrationDetails: {
vatId: '',
registrationId: '',
registrationName: ''
}
};
}
}