update
This commit is contained in:
326
ts/formats/xinvoice.decoder.ts
Normal file
326
ts/formats/xinvoice.decoder.ts
Normal file
@ -0,0 +1,326 @@
|
||||
import * as plugins from '../plugins.js';
|
||||
import * as xmldom from 'xmldom';
|
||||
import { BaseDecoder } from './base.decoder.js';
|
||||
|
||||
/**
|
||||
* A decoder specifically for XInvoice/XRechnung format.
|
||||
* XRechnung is the German implementation of the European standard EN16931
|
||||
* for electronic invoices to the German public sector.
|
||||
*/
|
||||
export class XInvoiceDecoder extends BaseDecoder {
|
||||
private xmlDoc: Document | null = null;
|
||||
private namespaces: { [key: string]: string } = {
|
||||
cbc: 'urn:oasis:names:specification:ubl:schema:xsd:CommonBasicComponents-2',
|
||||
cac: 'urn:oasis:names:specification:ubl:schema:xsd:CommonAggregateComponents-2',
|
||||
ubl: 'urn:oasis:names:specification:ubl:schema:xsd:Invoice-2'
|
||||
};
|
||||
|
||||
constructor(xmlString: string) {
|
||||
super(xmlString);
|
||||
|
||||
// Parse XML to DOM
|
||||
try {
|
||||
const parser = new xmldom.DOMParser();
|
||||
this.xmlDoc = parser.parseFromString(this.xmlString, 'text/xml');
|
||||
|
||||
// Try to detect if this is actually UBL (which XRechnung is based on)
|
||||
if (this.xmlString.includes('oasis:names:specification:ubl')) {
|
||||
// Set up appropriate namespaces
|
||||
this.setupNamespaces();
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error parsing XInvoice XML:', error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Set up namespaces from the document
|
||||
*/
|
||||
private setupNamespaces(): void {
|
||||
if (!this.xmlDoc) return;
|
||||
|
||||
// Try to extract namespaces from the document
|
||||
const root = this.xmlDoc.documentElement;
|
||||
if (root) {
|
||||
// Look for common UBL namespaces
|
||||
for (let i = 0; i < root.attributes.length; i++) {
|
||||
const attr = root.attributes[i];
|
||||
if (attr.name.startsWith('xmlns:')) {
|
||||
const prefix = attr.name.substring(6);
|
||||
this.namespaces[prefix] = attr.value;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract element text by tag name with namespace awareness
|
||||
*/
|
||||
private getElementText(tagName: string): string {
|
||||
if (!this.xmlDoc) {
|
||||
return '';
|
||||
}
|
||||
|
||||
try {
|
||||
// Handle namespace prefixes
|
||||
if (tagName.includes(':')) {
|
||||
const [nsPrefix, localName] = tagName.split(':');
|
||||
|
||||
// Find elements with this tag name
|
||||
const elements = this.xmlDoc.getElementsByTagNameNS(this.namespaces[nsPrefix] || '', localName);
|
||||
if (elements.length > 0) {
|
||||
return elements[0].textContent || '';
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback to direct tag name lookup
|
||||
const elements = this.xmlDoc.getElementsByTagName(tagName);
|
||||
if (elements.length > 0) {
|
||||
return elements[0].textContent || '';
|
||||
}
|
||||
|
||||
return '';
|
||||
} catch (error) {
|
||||
console.error(`Error extracting XInvoice element ${tagName}:`, error);
|
||||
return '';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts XInvoice/XRechnung XML to a structured letter object
|
||||
*/
|
||||
public async getLetterData(): Promise<plugins.tsclass.business.ILetter> {
|
||||
try {
|
||||
// Extract invoice ID - typically in cbc:ID or Invoice/cbc:ID
|
||||
let invoiceId = this.getElementText('cbc:ID');
|
||||
if (!invoiceId) {
|
||||
invoiceId = this.getElementText('Invoice/cbc:ID') || 'Unknown';
|
||||
}
|
||||
|
||||
// Extract invoice issue date
|
||||
const issueDateStr = this.getElementText('cbc:IssueDate') || '';
|
||||
const issueDate = issueDateStr ? new Date(issueDateStr).getTime() : Date.now();
|
||||
|
||||
// Extract seller information
|
||||
const sellerName = this.getElementText('cac:AccountingSupplierParty/cac:Party/cac:PartyName/cbc:Name') ||
|
||||
this.getElementText('cac:SellerSupplierParty/cac:Party/cac:PartyName/cbc:Name') ||
|
||||
'Unknown Seller';
|
||||
|
||||
// Extract seller address
|
||||
const sellerStreet = this.getElementText('cac:AccountingSupplierParty/cac:Party/cac:PostalAddress/cbc:StreetName') || 'Unknown';
|
||||
const sellerCity = this.getElementText('cac:AccountingSupplierParty/cac:Party/cac:PostalAddress/cbc:CityName') || 'Unknown';
|
||||
const sellerPostcode = this.getElementText('cac:AccountingSupplierParty/cac:Party/cac:PostalAddress/cbc:PostalZone') || 'Unknown';
|
||||
const sellerCountry = this.getElementText('cac:AccountingSupplierParty/cac:Party/cac:PostalAddress/cac:Country/cbc:IdentificationCode') || 'Unknown';
|
||||
|
||||
// Extract buyer information
|
||||
const buyerName = this.getElementText('cac:AccountingCustomerParty/cac:Party/cac:PartyName/cbc:Name') ||
|
||||
this.getElementText('cac:BuyerCustomerParty/cac:Party/cac:PartyName/cbc:Name') ||
|
||||
'Unknown Buyer';
|
||||
|
||||
// Create seller contact
|
||||
const seller: plugins.tsclass.business.IContact = {
|
||||
name: sellerName,
|
||||
type: 'company',
|
||||
description: sellerName,
|
||||
address: {
|
||||
streetName: sellerStreet,
|
||||
houseNumber: '0', // Required by IAddress interface
|
||||
city: sellerCity,
|
||||
country: sellerCountry,
|
||||
postalCode: sellerPostcode,
|
||||
},
|
||||
};
|
||||
|
||||
// Create buyer contact
|
||||
const buyer: plugins.tsclass.business.IContact = {
|
||||
name: buyerName,
|
||||
type: 'company',
|
||||
description: buyerName,
|
||||
address: {
|
||||
streetName: 'Unknown',
|
||||
houseNumber: '0',
|
||||
city: 'Unknown',
|
||||
country: 'Unknown',
|
||||
postalCode: 'Unknown',
|
||||
},
|
||||
};
|
||||
|
||||
// Extract invoice type
|
||||
let invoiceType = 'debitnote';
|
||||
const typeCode = this.getElementText('cbc:InvoiceTypeCode');
|
||||
if (typeCode === '380') {
|
||||
invoiceType = 'debitnote'; // Standard invoice
|
||||
} else if (typeCode === '381') {
|
||||
invoiceType = 'creditnote'; // Credit note
|
||||
}
|
||||
|
||||
// Create invoice data
|
||||
const invoiceData: plugins.tsclass.finance.IInvoice = {
|
||||
id: invoiceId,
|
||||
status: null,
|
||||
type: invoiceType as 'debitnote' | 'creditnote',
|
||||
billedBy: seller,
|
||||
billedTo: buyer,
|
||||
deliveryDate: issueDate,
|
||||
dueInDays: 30,
|
||||
periodOfPerformance: null,
|
||||
printResult: null,
|
||||
currency: (this.getElementText('cbc:DocumentCurrencyCode') || 'EUR') as plugins.tsclass.finance.TCurrency,
|
||||
notes: [],
|
||||
items: this.extractInvoiceItems(),
|
||||
reverseCharge: false,
|
||||
};
|
||||
|
||||
// Return a letter
|
||||
return {
|
||||
versionInfo: {
|
||||
type: 'draft',
|
||||
version: '1.0.0',
|
||||
},
|
||||
type: 'invoice',
|
||||
date: issueDate,
|
||||
subject: `XInvoice: ${invoiceId}`,
|
||||
from: seller,
|
||||
to: buyer,
|
||||
content: {
|
||||
invoiceData: invoiceData,
|
||||
textData: null,
|
||||
timesheetData: null,
|
||||
contractData: null,
|
||||
},
|
||||
needsCoverSheet: false,
|
||||
objectActions: [],
|
||||
pdf: null,
|
||||
incidenceId: null,
|
||||
language: null,
|
||||
legalContact: null,
|
||||
logoUrl: null,
|
||||
pdfAttachments: null,
|
||||
accentColor: null,
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('Error converting XInvoice XML to letter data:', error);
|
||||
return this.createDefaultLetter();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Extracts invoice items from XInvoice document
|
||||
*/
|
||||
private extractInvoiceItems(): plugins.tsclass.finance.IInvoiceItem[] {
|
||||
if (!this.xmlDoc) {
|
||||
return [
|
||||
{
|
||||
name: 'Unknown Item',
|
||||
unitQuantity: 1,
|
||||
unitNetPrice: 0,
|
||||
vatPercentage: 0,
|
||||
position: 0,
|
||||
unitType: 'units',
|
||||
}
|
||||
];
|
||||
}
|
||||
|
||||
try {
|
||||
const items: plugins.tsclass.finance.IInvoiceItem[] = [];
|
||||
|
||||
// Get all invoice line elements
|
||||
const lines = this.xmlDoc.getElementsByTagName('cac:InvoiceLine');
|
||||
if (!lines || lines.length === 0) {
|
||||
// Fallback to a default item
|
||||
return [
|
||||
{
|
||||
name: 'Item from XInvoice XML',
|
||||
unitQuantity: 1,
|
||||
unitNetPrice: 0,
|
||||
vatPercentage: 0,
|
||||
position: 0,
|
||||
unitType: 'units',
|
||||
}
|
||||
];
|
||||
}
|
||||
|
||||
// Process each line
|
||||
for (let i = 0; i < lines.length; i++) {
|
||||
const line = lines[i];
|
||||
|
||||
// Extract item details
|
||||
let name = '';
|
||||
let quantity = 1;
|
||||
let price = 0;
|
||||
let vatRate = 0;
|
||||
|
||||
// Find description element
|
||||
const descElements = line.getElementsByTagName('cbc:Description');
|
||||
if (descElements.length > 0) {
|
||||
name = descElements[0].textContent || '';
|
||||
}
|
||||
|
||||
// Fallback to item name if description is empty
|
||||
if (!name) {
|
||||
const itemNameElements = line.getElementsByTagName('cbc:Name');
|
||||
if (itemNameElements.length > 0) {
|
||||
name = itemNameElements[0].textContent || '';
|
||||
}
|
||||
}
|
||||
|
||||
// Find quantity
|
||||
const quantityElements = line.getElementsByTagName('cbc:InvoicedQuantity');
|
||||
if (quantityElements.length > 0) {
|
||||
const quantityText = quantityElements[0].textContent || '1';
|
||||
quantity = parseFloat(quantityText) || 1;
|
||||
}
|
||||
|
||||
// Find price
|
||||
const priceElements = line.getElementsByTagName('cbc:PriceAmount');
|
||||
if (priceElements.length > 0) {
|
||||
const priceText = priceElements[0].textContent || '0';
|
||||
price = parseFloat(priceText) || 0;
|
||||
}
|
||||
|
||||
// Find VAT rate - this is a bit more complex in UBL/XRechnung
|
||||
const taxCategoryElements = line.getElementsByTagName('cac:ClassifiedTaxCategory');
|
||||
if (taxCategoryElements.length > 0) {
|
||||
const rateElements = taxCategoryElements[0].getElementsByTagName('cbc:Percent');
|
||||
if (rateElements.length > 0) {
|
||||
const rateText = rateElements[0].textContent || '0';
|
||||
vatRate = parseFloat(rateText) || 0;
|
||||
}
|
||||
}
|
||||
|
||||
// Add the item to the list
|
||||
items.push({
|
||||
name: name || `Item ${i+1}`,
|
||||
unitQuantity: quantity,
|
||||
unitNetPrice: price,
|
||||
vatPercentage: vatRate,
|
||||
position: i,
|
||||
unitType: 'units',
|
||||
});
|
||||
}
|
||||
|
||||
return items.length > 0 ? items : [
|
||||
{
|
||||
name: 'Item from XInvoice XML',
|
||||
unitQuantity: 1,
|
||||
unitNetPrice: 0,
|
||||
vatPercentage: 0,
|
||||
position: 0,
|
||||
unitType: 'units',
|
||||
}
|
||||
];
|
||||
} catch (error) {
|
||||
console.error('Error extracting XInvoice items:', error);
|
||||
return [
|
||||
{
|
||||
name: 'Error extracting items',
|
||||
unitQuantity: 1,
|
||||
unitNetPrice: 0,
|
||||
vatPercentage: 0,
|
||||
position: 0,
|
||||
unitType: 'units',
|
||||
}
|
||||
];
|
||||
}
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user