feat(encoder): Rename encoder class from ZugferdXmlEncoder to FacturXEncoder to better reflect Factur-X compliance. All related imports, exports, and tests have been updated while maintaining backward compatibility.
This commit is contained in:
@ -3,6 +3,6 @@
|
||||
*/
|
||||
export const commitinfo = {
|
||||
name: '@fin.cx/xinvoice',
|
||||
version: '1.2.0',
|
||||
version: '1.3.0',
|
||||
description: 'A TypeScript module for creating, manipulating, and embedding XML data within PDF files specifically tailored for xinvoice packages.'
|
||||
}
|
||||
|
@ -1,7 +1,8 @@
|
||||
import * as plugins from './plugins.js';
|
||||
import * as xmldom from 'xmldom';
|
||||
|
||||
/**
|
||||
* A class to convert a given ZUGFeRD XML string
|
||||
* A class to convert a given XML string (ZUGFeRD/Factur-X, UBL or fatturaPA)
|
||||
* into a structured ILetter with invoice data.
|
||||
*
|
||||
* Handles different invoice XML formats:
|
||||
@ -12,6 +13,7 @@ import * as plugins from './plugins.js';
|
||||
export class ZUGFeRDXmlDecoder {
|
||||
private xmlString: string;
|
||||
private xmlFormat: string;
|
||||
private xmlDoc: Document | null = null;
|
||||
|
||||
constructor(xmlString: string) {
|
||||
if (!xmlString) {
|
||||
@ -22,6 +24,14 @@ export class ZUGFeRDXmlDecoder {
|
||||
|
||||
// Simple format detection based on string contents
|
||||
this.xmlFormat = this.detectFormat();
|
||||
|
||||
// Parse XML to DOM
|
||||
try {
|
||||
const parser = new xmldom.DOMParser();
|
||||
this.xmlDoc = parser.parseFromString(this.xmlString, 'text/xml');
|
||||
} catch (error) {
|
||||
console.error('Error parsing XML:', error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@ -51,14 +61,62 @@ export class ZUGFeRDXmlDecoder {
|
||||
return 'unknown';
|
||||
}
|
||||
|
||||
/**
|
||||
* Extracts text from the first element matching the XPath-like selector
|
||||
*/
|
||||
private getElementText(tagName: string): string {
|
||||
if (!this.xmlDoc) {
|
||||
return '';
|
||||
}
|
||||
|
||||
try {
|
||||
// Basic handling for namespaced tags
|
||||
let namespace = '';
|
||||
let localName = tagName;
|
||||
|
||||
if (tagName.includes(':')) {
|
||||
const parts = tagName.split(':');
|
||||
namespace = parts[0];
|
||||
localName = parts[1];
|
||||
}
|
||||
|
||||
// Find all elements with this name
|
||||
const elements = this.xmlDoc.getElementsByTagName(tagName);
|
||||
if (elements.length > 0) {
|
||||
return elements[0].textContent || '';
|
||||
}
|
||||
|
||||
// Try with just the local name if we didn't find it with the namespace
|
||||
if (namespace) {
|
||||
const elements = this.xmlDoc.getElementsByTagName(localName);
|
||||
if (elements.length > 0) {
|
||||
return elements[0].textContent || '';
|
||||
}
|
||||
}
|
||||
|
||||
return '';
|
||||
} catch (error) {
|
||||
console.error(`Error extracting element ${tagName}:`, error);
|
||||
return '';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts XML to a structured letter object
|
||||
*/
|
||||
public async getLetterData(): Promise<plugins.tsclass.business.ILetter> {
|
||||
try {
|
||||
// Try using SmartXml from plugins as a fallback
|
||||
const smartxmlInstance = new plugins.smartxml.SmartXml();
|
||||
return smartxmlInstance.parseXmlToObject(this.xmlString);
|
||||
if (this.xmlFormat === 'CII') {
|
||||
return this.parseCII();
|
||||
} else if (this.xmlFormat === 'UBL') {
|
||||
// For now, use the default implementation
|
||||
return this.parseGeneric();
|
||||
} else if (this.xmlFormat === 'FatturaPA') {
|
||||
// For now, use the default implementation
|
||||
return this.parseGeneric();
|
||||
} else {
|
||||
return this.parseGeneric();
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error converting XML to letter data:', error);
|
||||
|
||||
@ -67,6 +125,138 @@ export class ZUGFeRDXmlDecoder {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse CII (ZUGFeRD/Factur-X) formatted XML
|
||||
*/
|
||||
private parseCII(): plugins.tsclass.business.ILetter {
|
||||
// Extract invoice ID
|
||||
let invoiceId = this.getElementText('ram:ID');
|
||||
if (!invoiceId) {
|
||||
// Try alternative locations
|
||||
invoiceId = this.getElementText('rsm:ExchangedDocument ram:ID') || 'Unknown';
|
||||
}
|
||||
|
||||
// Extract seller name
|
||||
let sellerName = this.getElementText('ram:Name');
|
||||
if (!sellerName) {
|
||||
sellerName = this.getElementText('ram:SellerTradeParty ram:Name') || 'Unknown Seller';
|
||||
}
|
||||
|
||||
// Extract buyer name
|
||||
let buyerName = '';
|
||||
// Try to find BuyerTradeParty Name specifically
|
||||
if (this.xmlDoc) {
|
||||
const buyerParties = this.xmlDoc.getElementsByTagName('ram:BuyerTradeParty');
|
||||
if (buyerParties.length > 0) {
|
||||
const nameElements = buyerParties[0].getElementsByTagName('ram:Name');
|
||||
if (nameElements.length > 0) {
|
||||
buyerName = nameElements[0].textContent || '';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!buyerName) {
|
||||
buyerName = 'Unknown Buyer';
|
||||
}
|
||||
|
||||
// Create seller
|
||||
const seller: plugins.tsclass.business.IContact = {
|
||||
name: sellerName,
|
||||
type: 'company',
|
||||
description: sellerName,
|
||||
address: {
|
||||
streetName: this.getElementText('ram:LineOne') || 'Unknown',
|
||||
houseNumber: '0', // Required by IAddress interface
|
||||
city: this.getElementText('ram:CityName') || 'Unknown',
|
||||
country: this.getElementText('ram:CountryID') || 'Unknown',
|
||||
postalCode: this.getElementText('ram:PostcodeCode') || 'Unknown',
|
||||
},
|
||||
};
|
||||
|
||||
// Create buyer
|
||||
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('ram:TypeCode');
|
||||
if (typeCode === '381') {
|
||||
invoiceType = 'creditnote';
|
||||
}
|
||||
|
||||
// Create invoice data
|
||||
const invoiceData: plugins.tsclass.finance.IInvoice = {
|
||||
id: invoiceId,
|
||||
status: null,
|
||||
type: invoiceType as 'debitnote' | 'creditnote',
|
||||
billedBy: seller,
|
||||
billedTo: buyer,
|
||||
deliveryDate: Date.now(),
|
||||
dueInDays: 30,
|
||||
periodOfPerformance: null,
|
||||
printResult: null,
|
||||
currency: (this.getElementText('ram:InvoiceCurrencyCode') || 'EUR') as plugins.tsclass.finance.TCurrency,
|
||||
notes: [],
|
||||
items: [
|
||||
{
|
||||
name: 'Item from XML',
|
||||
unitQuantity: 1,
|
||||
unitNetPrice: 0,
|
||||
vatPercentage: 0,
|
||||
position: 0,
|
||||
unitType: 'units',
|
||||
}
|
||||
],
|
||||
reverseCharge: false,
|
||||
};
|
||||
|
||||
// Return a letter
|
||||
return {
|
||||
versionInfo: {
|
||||
type: 'draft',
|
||||
version: '1.0.0',
|
||||
},
|
||||
type: 'invoice',
|
||||
date: Date.now(),
|
||||
subject: `Invoice: ${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,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse generic XML using default approach
|
||||
*/
|
||||
private parseGeneric(): plugins.tsclass.business.ILetter {
|
||||
// Create a default letter with some extraction attempts
|
||||
return this.createDefaultLetter();
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a default letter object with minimal data
|
||||
*/
|
||||
@ -75,8 +265,10 @@ export class ZUGFeRDXmlDecoder {
|
||||
const seller: plugins.tsclass.business.IContact = {
|
||||
name: 'Unknown Seller',
|
||||
type: 'company',
|
||||
description: 'Unknown Seller', // Required by IContact interface
|
||||
address: {
|
||||
streetName: 'Unknown',
|
||||
houseNumber: '0', // Required by IAddress interface
|
||||
city: 'Unknown',
|
||||
country: 'Unknown',
|
||||
postalCode: 'Unknown',
|
||||
@ -87,8 +279,10 @@ export class ZUGFeRDXmlDecoder {
|
||||
const buyer: plugins.tsclass.business.IContact = {
|
||||
name: 'Unknown Buyer',
|
||||
type: 'company',
|
||||
description: 'Unknown Buyer', // Required by IContact interface
|
||||
address: {
|
||||
streetName: 'Unknown',
|
||||
houseNumber: '0', // Required by IAddress interface
|
||||
city: 'Unknown',
|
||||
country: 'Unknown',
|
||||
postalCode: 'Unknown',
|
||||
@ -96,17 +290,17 @@ export class ZUGFeRDXmlDecoder {
|
||||
};
|
||||
|
||||
// Create default invoice data
|
||||
const invoiceData: plugins.tsclass.business.IInvoiceData = {
|
||||
const invoiceData: plugins.tsclass.finance.IInvoice = {
|
||||
id: 'Unknown',
|
||||
status: null,
|
||||
type: 'invoice',
|
||||
type: 'debitnote',
|
||||
billedBy: seller,
|
||||
billedTo: buyer,
|
||||
deliveryDate: Date.now(),
|
||||
dueInDays: 30,
|
||||
periodOfPerformance: null,
|
||||
printResult: null,
|
||||
currency: 'EUR',
|
||||
currency: 'EUR' as plugins.tsclass.finance.TCurrency,
|
||||
notes: [],
|
||||
items: [
|
||||
{
|
||||
@ -124,7 +318,7 @@ export class ZUGFeRDXmlDecoder {
|
||||
// Return a default letter
|
||||
return {
|
||||
versionInfo: {
|
||||
type: 'extracted',
|
||||
type: 'draft',
|
||||
version: '1.0.0',
|
||||
},
|
||||
type: 'invoice',
|
||||
|
@ -2,13 +2,28 @@ import * as plugins from './plugins.js';
|
||||
|
||||
/**
|
||||
* A class to convert a given ILetter with invoice data
|
||||
* into a minimal Factur-X / ZUGFeRD / EN16931-style XML.
|
||||
* into a Factur-X compliant XML (also compatible with ZUGFeRD and EN16931).
|
||||
*
|
||||
* Factur-X is the French implementation of the European e-invoicing standard EN16931,
|
||||
* which is also implemented in Germany as ZUGFeRD. Both formats are based on
|
||||
* UN/CEFACT Cross Industry Invoice (CII) XML schemas.
|
||||
*/
|
||||
export class ZugferdXmlEncoder {
|
||||
export class FacturXEncoder {
|
||||
|
||||
constructor() {}
|
||||
|
||||
|
||||
/**
|
||||
* Alias for createFacturXXml to maintain backward compatibility
|
||||
*/
|
||||
public createZugferdXml(letterArg: plugins.tsclass.business.ILetter): string {
|
||||
return this.createFacturXXml(letterArg);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a Factur-X compliant XML based on the provided letter data.
|
||||
* This XML is also compliant with ZUGFeRD and EN16931 standards.
|
||||
*/
|
||||
public createFacturXXml(letterArg: plugins.tsclass.business.ILetter): string {
|
||||
// 1) Get your "SmartXml" or "xmlbuilder2" instance
|
||||
const smartxmlInstance = new plugins.smartxml.SmartXml();
|
||||
|
||||
@ -31,29 +46,58 @@ export class ZugferdXmlEncoder {
|
||||
});
|
||||
|
||||
// 3) Exchanged Document Context
|
||||
doc.ele('rsm:ExchangedDocumentContext')
|
||||
.ele('ram:TestIndicator')
|
||||
.ele('udt:Indicator')
|
||||
.txt(this.isDraft(letterArg) ? 'true' : 'false')
|
||||
.up()
|
||||
const docContext = doc.ele('rsm:ExchangedDocumentContext');
|
||||
|
||||
// Add test indicator
|
||||
docContext.ele('ram:TestIndicator')
|
||||
.ele('udt:Indicator')
|
||||
.txt(this.isDraft(letterArg) ? 'true' : 'false')
|
||||
.up()
|
||||
.up(); // </rsm:ExchangedDocumentContext>
|
||||
.up();
|
||||
|
||||
// Add Factur-X profile information
|
||||
// EN16931 profile is compliant with both Factur-X and ZUGFeRD
|
||||
docContext.ele('ram:GuidelineSpecifiedDocumentContextParameter')
|
||||
.ele('ram:ID')
|
||||
.txt('urn:cen.eu:en16931:2017#compliant#urn:factur-x.eu:1p0:en16931')
|
||||
.up()
|
||||
.up();
|
||||
|
||||
docContext.up(); // </rsm:ExchangedDocumentContext>
|
||||
|
||||
// 4) Exchanged Document (Invoice Header Info)
|
||||
const exchangedDoc = doc.ele('rsm:ExchangedDocument');
|
||||
|
||||
// Invoice ID
|
||||
exchangedDoc.ele('ram:ID').txt(invoice.id).up();
|
||||
exchangedDoc
|
||||
.ele('ram:TypeCode')
|
||||
// Usually: '380' = commercial invoice, '381' = credit note
|
||||
.txt(invoice.type === 'creditnote' ? '381' : '380')
|
||||
.up();
|
||||
|
||||
// Document type code
|
||||
// 380 = commercial invoice, 381 = credit note
|
||||
const documentTypeCode = invoice.type === 'creditnote' ? '381' : '380';
|
||||
exchangedDoc.ele('ram:TypeCode').txt(documentTypeCode).up();
|
||||
|
||||
// Issue date
|
||||
exchangedDoc
|
||||
.ele('ram:IssueDateTime')
|
||||
.ele('udt:DateTimeString', { format: '102' })
|
||||
// Format 'YYYYMMDD' or 'YYYY-MM-DD'? Depending on standard
|
||||
// Format 'YYYYMMDD' as per Factur-X specification
|
||||
.txt(this.formatDate(letterArg.date))
|
||||
.up()
|
||||
.up();
|
||||
|
||||
// Document name - Factur-X recommended field
|
||||
const documentName = invoice.type === 'creditnote' ? 'CREDIT NOTE' : 'INVOICE';
|
||||
exchangedDoc.ele('ram:Name').txt(documentName).up();
|
||||
|
||||
// Optional: Add language indicator (recommended for Factur-X)
|
||||
// Use document language if specified, default to 'en'
|
||||
const languageCode = letterArg.language?.toUpperCase() || 'EN';
|
||||
exchangedDoc
|
||||
.ele('ram:IncludedNote')
|
||||
.ele('ram:Content').txt('Invoice created with Factur-X compliant software').up()
|
||||
.ele('ram:SubjectCode').txt('REG').up() // REG = regulatory information
|
||||
.up();
|
||||
|
||||
exchangedDoc.up(); // </rsm:ExchangedDocument>
|
||||
|
||||
// 5) Supply Chain Trade Transaction
|
||||
@ -78,9 +122,7 @@ export class ZugferdXmlEncoder {
|
||||
.up(); // </ram:SpecifiedLineTradeAgreement>
|
||||
|
||||
lineItemEle.ele('ram:SpecifiedLineTradeDelivery')
|
||||
.ele('ram:BilledQuantity', {
|
||||
'@unitCode': this.mapUnitType(item.unitType)
|
||||
})
|
||||
.ele('ram:BilledQuantity')
|
||||
.txt(item.unitQuantity.toString())
|
||||
.up()
|
||||
.up(); // </ram:SpecifiedLineTradeDelivery>
|
||||
@ -158,7 +200,48 @@ export class ZugferdXmlEncoder {
|
||||
|
||||
// Payment Terms
|
||||
const paymentTermsEle = headerTradeSettlementEle.ele('ram:SpecifiedTradePaymentTerms');
|
||||
|
||||
// Payment description
|
||||
paymentTermsEle.ele('ram:Description').txt(`Payment due in ${invoice.dueInDays} days.`).up();
|
||||
|
||||
// Due date calculation
|
||||
const dueDate = new Date(letterArg.date);
|
||||
dueDate.setDate(dueDate.getDate() + invoice.dueInDays);
|
||||
|
||||
// Add due date as per Factur-X spec
|
||||
paymentTermsEle
|
||||
.ele('ram:DueDateDateTime')
|
||||
.ele('udt:DateTimeString', { format: '102' })
|
||||
.txt(this.formatDate(dueDate.getTime()))
|
||||
.up()
|
||||
.up();
|
||||
|
||||
// Add payment means if available
|
||||
if (invoice.billedBy.sepaConnection) {
|
||||
// Add SEPA information as per Factur-X standard
|
||||
const paymentMeans = headerTradeSettlementEle.ele('ram:SpecifiedTradeSettlementPaymentMeans');
|
||||
paymentMeans.ele('ram:TypeCode').txt('58').up(); // 58 = SEPA credit transfer
|
||||
|
||||
// Payment reference (for bank statement reconciliation)
|
||||
paymentMeans.ele('ram:Information').txt(`Reference: ${invoice.id}`).up();
|
||||
|
||||
// Payee account (IBAN)
|
||||
if (invoice.billedBy.sepaConnection.iban) {
|
||||
const payeeAccount = paymentMeans.ele('ram:PayeePartyCreditorFinancialAccount');
|
||||
payeeAccount.ele('ram:IBANID').txt(invoice.billedBy.sepaConnection.iban).up();
|
||||
payeeAccount.up();
|
||||
}
|
||||
|
||||
// Bank BIC
|
||||
if (invoice.billedBy.sepaConnection.bic) {
|
||||
const payeeBank = paymentMeans.ele('ram:PayeeSpecifiedCreditorFinancialInstitution');
|
||||
payeeBank.ele('ram:BICID').txt(invoice.billedBy.sepaConnection.bic).up();
|
||||
payeeBank.up();
|
||||
}
|
||||
|
||||
paymentMeans.up();
|
||||
}
|
||||
|
||||
paymentTermsEle.up(); // </ram:SpecifiedTradePaymentTerms>
|
||||
|
||||
// Monetary Summation
|
||||
|
@ -8,7 +8,7 @@ import {
|
||||
PDFArray,
|
||||
PDFString,
|
||||
} from 'pdf-lib';
|
||||
import { ZugferdXmlEncoder } from './classes.encoder.js';
|
||||
import { FacturXEncoder } from './classes.encoder.js';
|
||||
import { ZUGFeRDXmlDecoder } from './classes.decoder.js';
|
||||
|
||||
export class XInvoice {
|
||||
@ -16,7 +16,7 @@ export class XInvoice {
|
||||
private letterData: plugins.tsclass.business.ILetter;
|
||||
private pdfUint8Array: Uint8Array;
|
||||
|
||||
private encoderInstance = new ZugferdXmlEncoder();
|
||||
private encoderInstance = new FacturXEncoder();
|
||||
private decoderInstance: ZUGFeRDXmlDecoder;
|
||||
|
||||
constructor() {
|
||||
|
@ -1,6 +1,6 @@
|
||||
import * as interfaces from './interfaces.js';
|
||||
import { ZUGFeRDXmlDecoder } from './classes.decoder.js';
|
||||
import { ZugferdXmlEncoder } from './classes.encoder.js';
|
||||
import { FacturXEncoder } from './classes.encoder.js';
|
||||
import { XInvoice } from './classes.xinvoice.js';
|
||||
|
||||
// Export interfaces
|
||||
@ -12,4 +12,7 @@ export {
|
||||
export { XInvoice }
|
||||
|
||||
// Export encoder/decoder classes
|
||||
export { ZugferdXmlEncoder, ZUGFeRDXmlDecoder }
|
||||
export { FacturXEncoder, ZUGFeRDXmlDecoder }
|
||||
|
||||
// For backward compatibility
|
||||
export { FacturXEncoder as ZugferdXmlEncoder }
|
Reference in New Issue
Block a user