234 lines
7.2 KiB
TypeScript
234 lines
7.2 KiB
TypeScript
import { CIIBaseDecoder } from '../cii.decoder.js';
|
|
import type { TInvoice, TCreditNote, TDebitNote } from '../../../interfaces/common.js';
|
|
import { business, finance } from '@tsclass/tsclass';
|
|
|
|
/**
|
|
* Decoder for ZUGFeRD invoice format
|
|
*/
|
|
export class ZUGFeRDDecoder extends CIIBaseDecoder {
|
|
/**
|
|
* Decodes a ZUGFeRD 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 ZUGFeRD 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 ZUGFeRD 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 (not used in this implementation but could be useful)
|
|
// 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 ZUGFeRD 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 streetName = this.getText(`${partyXPath}/ram:PostalTradeAddress/ram:LineOne`);
|
|
const city = this.getText(`${partyXPath}/ram:PostalTradeAddress/ram:CityName`);
|
|
const postalCode = this.getText(`${partyXPath}/ram:PostalTradeAddress/ram:PostcodeCode`);
|
|
const country = this.getText(`${partyXPath}/ram:PostalTradeAddress/ram:CountryID`);
|
|
|
|
// Try to extract house number from street if possible
|
|
let houseNumber = '';
|
|
const streetParts = streetName.match(/^(.*?)\s+(\d+.*)$/);
|
|
if (streetParts) {
|
|
// If we can split into street name and house number
|
|
houseNumber = streetParts[2];
|
|
}
|
|
|
|
// Create address object
|
|
const address = {
|
|
streetName: streetName,
|
|
houseNumber: houseNumber,
|
|
city: city,
|
|
postalCode: postalCode,
|
|
country: country
|
|
};
|
|
|
|
// 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 ZUGFeRD 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 ZUGFeRD 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 for empty date fields
|
|
* @returns Default date as timestamp
|
|
*/
|
|
private createDefaultDate(): any {
|
|
// Create a date object that will be compatible with TContact
|
|
return {
|
|
year: 2000,
|
|
month: 1,
|
|
day: 1
|
|
};
|
|
}
|
|
}
|