working
This commit is contained in:
292
ts/formats/ubl/xrechnung/xrechnung.decoder.ts
Normal file
292
ts/formats/ubl/xrechnung/xrechnung.decoder.ts
Normal file
@ -0,0 +1,292 @@
|
||||
import { UBLBaseDecoder } from '../ubl.decoder.js';
|
||||
import type { TInvoice, TCreditNote, TDebitNote } from '../../../interfaces/common.js';
|
||||
import { business, finance } from '@tsclass/tsclass';
|
||||
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: ''
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
144
ts/formats/ubl/xrechnung/xrechnung.encoder.ts
Normal file
144
ts/formats/ubl/xrechnung/xrechnung.encoder.ts
Normal file
@ -0,0 +1,144 @@
|
||||
import { UBLBaseEncoder } from '../ubl.encoder.js';
|
||||
import type { TInvoice, TCreditNote, TDebitNote } from '../../../interfaces/common.js';
|
||||
import { UBLDocumentType } from '../ubl.types.js';
|
||||
|
||||
/**
|
||||
* Encoder for XRechnung (UBL) format
|
||||
* Implements encoding of TInvoice to XRechnung XML
|
||||
*/
|
||||
export class XRechnungEncoder extends UBLBaseEncoder {
|
||||
/**
|
||||
* Encodes a TCreditNote object to XRechnung XML
|
||||
* @param creditNote TCreditNote object to encode
|
||||
* @returns Promise resolving to XML string
|
||||
*/
|
||||
protected async encodeCreditNote(creditNote: TCreditNote): Promise<string> {
|
||||
// For now, we'll just return a simple UBL credit note template
|
||||
// In a real implementation, we would generate a proper UBL credit note
|
||||
return `<?xml version="1.0" encoding="UTF-8"?>
|
||||
<CreditNote xmlns="urn:oasis:names:specification:ubl:schema:xsd:CreditNote-2"
|
||||
xmlns:cac="urn:oasis:names:specification:ubl:schema:xsd:CommonAggregateComponents-2"
|
||||
xmlns:cbc="urn:oasis:names:specification:ubl:schema:xsd:CommonBasicComponents-2">
|
||||
<cbc:CustomizationID>urn:cen.eu:en16931:2017#compliant#urn:xoev-de:kosit:standard:xrechnung_2.0</cbc:CustomizationID>
|
||||
<cbc:ID>${creditNote.id}</cbc:ID>
|
||||
<cbc:IssueDate>${this.formatDate(creditNote.date)}</cbc:IssueDate>
|
||||
<cbc:CreditNoteTypeCode>381</cbc:CreditNoteTypeCode>
|
||||
<cbc:DocumentCurrencyCode>${creditNote.currency}</cbc:DocumentCurrencyCode>
|
||||
|
||||
<!-- Rest of the credit note XML would go here -->
|
||||
</CreditNote>`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Encodes a TDebitNote object to XRechnung XML
|
||||
* @param debitNote TDebitNote object to encode
|
||||
* @returns Promise resolving to XML string
|
||||
*/
|
||||
protected async encodeDebitNote(debitNote: TDebitNote): Promise<string> {
|
||||
// For now, we'll just return a simple UBL invoice template
|
||||
// In a real implementation, we would generate a proper UBL invoice
|
||||
return `<?xml version="1.0" encoding="UTF-8"?>
|
||||
<Invoice xmlns="urn:oasis:names:specification:ubl:schema:xsd:Invoice-2"
|
||||
xmlns:cac="urn:oasis:names:specification:ubl:schema:xsd:CommonAggregateComponents-2"
|
||||
xmlns:cbc="urn:oasis:names:specification:ubl:schema:xsd:CommonBasicComponents-2">
|
||||
<cbc:CustomizationID>urn:cen.eu:en16931:2017#compliant#urn:xoev-de:kosit:standard:xrechnung_2.0</cbc:CustomizationID>
|
||||
<cbc:ID>${debitNote.id}</cbc:ID>
|
||||
<cbc:IssueDate>${this.formatDate(debitNote.date)}</cbc:IssueDate>
|
||||
<cbc:DueDate>${this.formatDate(debitNote.date + debitNote.dueInDays * 24 * 60 * 60 * 1000)}</cbc:DueDate>
|
||||
<cbc:InvoiceTypeCode>380</cbc:InvoiceTypeCode>
|
||||
<cbc:DocumentCurrencyCode>${debitNote.currency}</cbc:DocumentCurrencyCode>
|
||||
|
||||
<cac:AccountingSupplierParty>
|
||||
<cac:Party>
|
||||
<cac:PartyName>
|
||||
<cbc:Name>${debitNote.from.name}</cbc:Name>
|
||||
</cac:PartyName>
|
||||
<cac:PostalAddress>
|
||||
<cbc:StreetName>${debitNote.from.address.streetName || ''}</cbc:StreetName>
|
||||
<cbc:BuildingNumber>${debitNote.from.address.houseNumber || ''}</cbc:BuildingNumber>
|
||||
<cbc:CityName>${debitNote.from.address.city || ''}</cbc:CityName>
|
||||
<cbc:PostalZone>${debitNote.from.address.postalCode || ''}</cbc:PostalZone>
|
||||
<cac:Country>
|
||||
<cbc:IdentificationCode>${debitNote.from.address.countryCode || ''}</cbc:IdentificationCode>
|
||||
</cac:Country>
|
||||
</cac:PostalAddress>
|
||||
${debitNote.from.registrationDetails?.vatId ? `
|
||||
<cac:PartyTaxScheme>
|
||||
<cbc:CompanyID>${debitNote.from.registrationDetails.vatId}</cbc:CompanyID>
|
||||
<cac:TaxScheme>
|
||||
<cbc:ID>VAT</cbc:ID>
|
||||
</cac:TaxScheme>
|
||||
</cac:PartyTaxScheme>` : ''}
|
||||
${debitNote.from.registrationDetails?.registrationId ? `
|
||||
<cac:PartyLegalEntity>
|
||||
<cbc:RegistrationName>${debitNote.from.registrationDetails.registrationName || debitNote.from.name}</cbc:RegistrationName>
|
||||
<cbc:CompanyID>${debitNote.from.registrationDetails.registrationId}</cbc:CompanyID>
|
||||
</cac:PartyLegalEntity>` : ''}
|
||||
</cac:Party>
|
||||
</cac:AccountingSupplierParty>
|
||||
|
||||
<cac:AccountingCustomerParty>
|
||||
<cac:Party>
|
||||
<cac:PartyName>
|
||||
<cbc:Name>${debitNote.to.name}</cbc:Name>
|
||||
</cac:PartyName>
|
||||
<cac:PostalAddress>
|
||||
<cbc:StreetName>${debitNote.to.address.streetName || ''}</cbc:StreetName>
|
||||
<cbc:BuildingNumber>${debitNote.to.address.houseNumber || ''}</cbc:BuildingNumber>
|
||||
<cbc:CityName>${debitNote.to.address.city || ''}</cbc:CityName>
|
||||
<cbc:PostalZone>${debitNote.to.address.postalCode || ''}</cbc:PostalZone>
|
||||
<cac:Country>
|
||||
<cbc:IdentificationCode>${debitNote.to.address.countryCode || ''}</cbc:IdentificationCode>
|
||||
</cac:Country>
|
||||
</cac:PostalAddress>
|
||||
${debitNote.to.registrationDetails?.vatId ? `
|
||||
<cac:PartyTaxScheme>
|
||||
<cbc:CompanyID>${debitNote.to.registrationDetails.vatId}</cbc:CompanyID>
|
||||
<cac:TaxScheme>
|
||||
<cbc:ID>VAT</cbc:ID>
|
||||
</cac:TaxScheme>
|
||||
</cac:PartyTaxScheme>` : ''}
|
||||
</cac:Party>
|
||||
</cac:AccountingCustomerParty>
|
||||
|
||||
<cac:PaymentTerms>
|
||||
<cbc:Note>Due in ${debitNote.dueInDays} days</cbc:Note>
|
||||
</cac:PaymentTerms>
|
||||
|
||||
<cac:TaxTotal>
|
||||
<cbc:TaxAmount currencyID="${debitNote.currency}">0.00</cbc:TaxAmount>
|
||||
</cac:TaxTotal>
|
||||
|
||||
<cac:LegalMonetaryTotal>
|
||||
<cbc:LineExtensionAmount currencyID="${debitNote.currency}">0.00</cbc:LineExtensionAmount>
|
||||
<cbc:TaxExclusiveAmount currencyID="${debitNote.currency}">0.00</cbc:TaxExclusiveAmount>
|
||||
<cbc:TaxInclusiveAmount currencyID="${debitNote.currency}">0.00</cbc:TaxInclusiveAmount>
|
||||
<cbc:PayableAmount currencyID="${debitNote.currency}">0.00</cbc:PayableAmount>
|
||||
</cac:LegalMonetaryTotal>
|
||||
|
||||
${debitNote.items.map((item, index) => `
|
||||
<cac:InvoiceLine>
|
||||
<cbc:ID>${index + 1}</cbc:ID>
|
||||
<cbc:InvoicedQuantity unitCode="${item.unitType}">${item.unitQuantity}</cbc:InvoicedQuantity>
|
||||
<cbc:LineExtensionAmount currencyID="${debitNote.currency}">${item.unitNetPrice * item.unitQuantity}</cbc:LineExtensionAmount>
|
||||
<cac:Item>
|
||||
<cbc:Name>${item.name}</cbc:Name>
|
||||
${item.articleNumber ? `
|
||||
<cac:SellersItemIdentification>
|
||||
<cbc:ID>${item.articleNumber}</cbc:ID>
|
||||
</cac:SellersItemIdentification>` : ''}
|
||||
<cac:ClassifiedTaxCategory>
|
||||
<cbc:ID>S</cbc:ID>
|
||||
<cbc:Percent>${item.vatPercentage}</cbc:Percent>
|
||||
<cac:TaxScheme>
|
||||
<cbc:ID>VAT</cbc:ID>
|
||||
</cac:TaxScheme>
|
||||
</cac:ClassifiedTaxCategory>
|
||||
</cac:Item>
|
||||
<cac:Price>
|
||||
<cbc:PriceAmount currencyID="${debitNote.currency}">${item.unitNetPrice}</cbc:PriceAmount>
|
||||
</cac:Price>
|
||||
</cac:InvoiceLine>`).join('')}
|
||||
</Invoice>`;
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user