update
This commit is contained in:
220
ts/formats/cii/facturx/facturx.decoder.ts
Normal file
220
ts/formats/cii/facturx/facturx.decoder.ts
Normal file
@ -0,0 +1,220 @@
|
||||
import { CIIBaseDecoder } from '../cii.decoder.js';
|
||||
import type { TInvoice, TCreditNote, TDebitNote } from '../../../interfaces/common.js';
|
||||
import { FACTURX_PROFILE_IDS } from './facturx.types.js';
|
||||
import { business, finance, general } from '@tsclass/tsclass';
|
||||
|
||||
/**
|
||||
* Decoder for Factur-X invoice format
|
||||
*/
|
||||
export class FacturXDecoder extends CIIBaseDecoder {
|
||||
/**
|
||||
* Decodes a Factur-X credit note
|
||||
* @returns Promise resolving to a TCreditNote object
|
||||
*/
|
||||
protected async decodeCreditNote(): Promise<TCreditNote> {
|
||||
// Get common invoice data
|
||||
const commonData = await this.extractCommonData();
|
||||
|
||||
// Create a credit note with the common data
|
||||
return {
|
||||
...commonData,
|
||||
invoiceType: 'creditnote'
|
||||
} as TCreditNote;
|
||||
}
|
||||
|
||||
/**
|
||||
* Decodes a Factur-X debit note (invoice)
|
||||
* @returns Promise resolving to a TDebitNote object
|
||||
*/
|
||||
protected async decodeDebitNote(): Promise<TDebitNote> {
|
||||
// Get common invoice data
|
||||
const commonData = await this.extractCommonData();
|
||||
|
||||
// Create a debit note with the common data
|
||||
return {
|
||||
...commonData,
|
||||
invoiceType: 'debitnote'
|
||||
} as TDebitNote;
|
||||
}
|
||||
|
||||
/**
|
||||
* Extracts common invoice data from Factur-X XML
|
||||
* @returns Common invoice data
|
||||
*/
|
||||
private async extractCommonData(): Promise<Partial<TInvoice>> {
|
||||
// Extract invoice ID
|
||||
const invoiceId = this.getText('//rsm:ExchangedDocument/ram:ID');
|
||||
|
||||
// Extract issue date
|
||||
const issueDateStr = this.getText('//ram:IssueDateTime/udt:DateTimeString');
|
||||
const issueDate = issueDateStr ? new Date(issueDateStr).getTime() : Date.now();
|
||||
|
||||
// Extract seller information
|
||||
const seller = this.extractParty('//ram:SellerTradeParty');
|
||||
|
||||
// Extract buyer information
|
||||
const buyer = this.extractParty('//ram:BuyerTradeParty');
|
||||
|
||||
// Extract items
|
||||
const items = this.extractItems();
|
||||
|
||||
// Extract due date
|
||||
const dueDateStr = this.getText('//ram:SpecifiedTradePaymentTerms/ram:DueDateDateTime/udt:DateTimeString');
|
||||
const dueDate = dueDateStr ? new Date(dueDateStr).getTime() : Date.now();
|
||||
const dueInDays = Math.round((dueDate - issueDate) / (1000 * 60 * 60 * 24));
|
||||
|
||||
// Extract currency
|
||||
const currencyCode = this.getText('//ram:InvoiceCurrencyCode') || 'EUR';
|
||||
|
||||
// Extract total amount
|
||||
const totalAmount = this.getNumber('//ram:GrandTotalAmount');
|
||||
|
||||
// Extract notes
|
||||
const notes = this.extractNotes();
|
||||
|
||||
// Check for reverse charge
|
||||
const reverseCharge = this.exists('//ram:SpecifiedTradeAllowanceCharge/ram:ReasonCode[text()="62"]');
|
||||
|
||||
// Create the common invoice data
|
||||
return {
|
||||
type: 'invoice',
|
||||
id: 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: reverseCharge,
|
||||
currency: currencyCode as finance.TCurrency,
|
||||
notes: notes,
|
||||
deliveryDate: issueDate,
|
||||
objectActions: [],
|
||||
invoiceType: 'debitnote' // Default to debit note, will be overridden in decode methods
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Extracts party information from Factur-X XML
|
||||
* @param partyXPath XPath to the party node
|
||||
* @returns Party information as TContact
|
||||
*/
|
||||
private extractParty(partyXPath: string): business.TContact {
|
||||
// Extract name
|
||||
const name = this.getText(`${partyXPath}/ram:Name`);
|
||||
|
||||
// Extract address
|
||||
const address: business.IAddress = {
|
||||
streetName: this.getText(`${partyXPath}/ram:PostalTradeAddress/ram:LineOne`) || '',
|
||||
houseNumber: this.getText(`${partyXPath}/ram:PostalTradeAddress/ram:LineTwo`) || '0',
|
||||
postalCode: this.getText(`${partyXPath}/ram:PostalTradeAddress/ram:PostcodeCode`) || '',
|
||||
city: this.getText(`${partyXPath}/ram:PostalTradeAddress/ram:CityName`) || '',
|
||||
country: this.getText(`${partyXPath}/ram:PostalTradeAddress/ram:CountryID`) || '',
|
||||
countryCode: this.getText(`${partyXPath}/ram:PostalTradeAddress/ram:CountryID`) || ''
|
||||
};
|
||||
|
||||
// Extract VAT ID
|
||||
const vatId = this.getText(`${partyXPath}/ram:SpecifiedTaxRegistration/ram:ID[@schemeID="VA"]`) || '';
|
||||
|
||||
// Extract registration ID
|
||||
const registrationId = this.getText(`${partyXPath}/ram:SpecifiedTaxRegistration/ram:ID[@schemeID="FC"]`) || '';
|
||||
|
||||
// Create contact object
|
||||
return {
|
||||
type: 'company',
|
||||
name: name,
|
||||
description: '',
|
||||
address: address,
|
||||
status: 'active',
|
||||
foundedDate: this.createDefaultDate(),
|
||||
registrationDetails: {
|
||||
vatId: vatId,
|
||||
registrationId: registrationId,
|
||||
registrationName: ''
|
||||
}
|
||||
} as business.TContact;
|
||||
}
|
||||
|
||||
/**
|
||||
* Extracts invoice items from Factur-X XML
|
||||
* @returns Array of invoice items
|
||||
*/
|
||||
private extractItems(): finance.TInvoiceItem[] {
|
||||
const items: finance.TInvoiceItem[] = [];
|
||||
|
||||
// Get all item nodes
|
||||
const itemNodes = this.select('//ram:IncludedSupplyChainTradeLineItem', this.doc);
|
||||
|
||||
// Process each item
|
||||
if (Array.isArray(itemNodes)) {
|
||||
for (let i = 0; i < itemNodes.length; i++) {
|
||||
const itemNode = itemNodes[i];
|
||||
|
||||
// Extract item data
|
||||
const name = this.getText('ram:SpecifiedTradeProduct/ram:Name', itemNode);
|
||||
const articleNumber = this.getText('ram:SpecifiedTradeProduct/ram:SellerAssignedID', itemNode);
|
||||
const unitQuantity = this.getNumber('ram:SpecifiedLineTradeDelivery/ram:BilledQuantity', itemNode);
|
||||
const unitType = this.getText('ram:SpecifiedLineTradeDelivery/ram:BilledQuantity/@unitCode', itemNode) || 'EA';
|
||||
const unitNetPrice = this.getNumber('ram:SpecifiedLineTradeAgreement/ram:NetPriceProductTradePrice/ram:ChargeAmount', itemNode);
|
||||
const vatPercentage = this.getNumber('ram:SpecifiedLineTradeSettlement/ram:ApplicableTradeTax/ram:RateApplicablePercent', itemNode);
|
||||
|
||||
// Create item object
|
||||
items.push({
|
||||
position: i + 1,
|
||||
name: name,
|
||||
articleNumber: articleNumber,
|
||||
unitType: unitType,
|
||||
unitQuantity: unitQuantity,
|
||||
unitNetPrice: unitNetPrice,
|
||||
vatPercentage: vatPercentage
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return items;
|
||||
}
|
||||
|
||||
/**
|
||||
* Extracts notes from Factur-X XML
|
||||
* @returns Array of notes
|
||||
*/
|
||||
private extractNotes(): string[] {
|
||||
const notes: string[] = [];
|
||||
|
||||
// Get all note nodes
|
||||
const noteNodes = this.select('//ram:IncludedNote', this.doc);
|
||||
|
||||
// Process each note
|
||||
if (Array.isArray(noteNodes)) {
|
||||
for (let i = 0; i < noteNodes.length; i++) {
|
||||
const noteNode = noteNodes[i];
|
||||
const noteText = this.getText('ram:Content', noteNode);
|
||||
|
||||
if (noteText) {
|
||||
notes.push(noteText);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return notes;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a default date object
|
||||
* @returns Default date object
|
||||
*/
|
||||
private createDefaultDate(): general.IDate {
|
||||
return {
|
||||
year: 2000,
|
||||
month: 1,
|
||||
day: 1
|
||||
};
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user