293 lines
9.0 KiB
TypeScript
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: ''
|
|
}
|
|
};
|
|
}
|
|
}
|