@fin.cx/einvoice 7.1.0 → 8.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (90) hide show
  1. package/dist_ts/00_commitinfo_data.js +1 -1
  2. package/dist_ts/einvoice.d.ts +10 -9
  3. package/dist_ts/einvoice.js +40 -10
  4. package/dist_ts/formats/base/base.decoder.d.ts +6 -6
  5. package/dist_ts/formats/base/base.decoder.js +1 -1
  6. package/dist_ts/formats/base/base.encoder.d.ts +4 -4
  7. package/dist_ts/formats/base/base.validator.d.ts +5 -0
  8. package/dist_ts/formats/base/base.validator.js +28 -1
  9. package/dist_ts/formats/cii/cii.decoder.d.ts +8 -7
  10. package/dist_ts/formats/cii/cii.decoder.js +8 -9
  11. package/dist_ts/formats/cii/cii.encoder.d.ts +7 -7
  12. package/dist_ts/formats/cii/cii.encoder.js +5 -5
  13. package/dist_ts/formats/cii/cii.validator.d.ts +11 -0
  14. package/dist_ts/formats/cii/cii.validator.js +35 -1
  15. package/dist_ts/formats/cii/facturx/facturx.decoder.d.ts +6 -4
  16. package/dist_ts/formats/cii/facturx/facturx.decoder.js +14 -21
  17. package/dist_ts/formats/cii/facturx/facturx.encoder.d.ts +5 -5
  18. package/dist_ts/formats/cii/facturx/facturx.encoder.js +14 -10
  19. package/dist_ts/formats/cii/facturx/facturx.validator.js +8 -4
  20. package/dist_ts/formats/cii/zugferd/zugferd.decoder.d.ts +6 -4
  21. package/dist_ts/formats/cii/zugferd/zugferd.decoder.js +14 -21
  22. package/dist_ts/formats/cii/zugferd/zugferd.encoder.d.ts +4 -4
  23. package/dist_ts/formats/cii/zugferd/zugferd.encoder.js +13 -9
  24. package/dist_ts/formats/cii/zugferd/zugferd.v1.decoder.d.ts +6 -4
  25. package/dist_ts/formats/cii/zugferd/zugferd.v1.decoder.js +14 -21
  26. package/dist_ts/formats/cii/zugferd/zugferd.validator.js +5 -4
  27. package/dist_ts/formats/converters/xml-to-einvoice.converter.d.ts +5 -0
  28. package/dist_ts/formats/converters/xml-to-einvoice.converter.js +11 -2
  29. package/dist_ts/formats/semantic/semantic.adapter.d.ts +0 -8
  30. package/dist_ts/formats/semantic/semantic.adapter.js +5 -34
  31. package/dist_ts/formats/ubl/en16931.ubl.validator.js +5 -1
  32. package/dist_ts/formats/ubl/generic/ubl.encoder.d.ts +4 -4
  33. package/dist_ts/formats/ubl/generic/ubl.encoder.js +13 -14
  34. package/dist_ts/formats/ubl/ubl.decoder.d.ts +8 -7
  35. package/dist_ts/formats/ubl/ubl.decoder.js +11 -8
  36. package/dist_ts/formats/ubl/ubl.encoder.d.ts +7 -7
  37. package/dist_ts/formats/ubl/ubl.encoder.js +5 -5
  38. package/dist_ts/formats/ubl/ubl.validator.d.ts +11 -0
  39. package/dist_ts/formats/ubl/ubl.validator.js +35 -1
  40. package/dist_ts/formats/ubl/xrechnung/xrechnung.decoder.d.ts +6 -4
  41. package/dist_ts/formats/ubl/xrechnung/xrechnung.decoder.js +14 -21
  42. package/dist_ts/formats/ubl/xrechnung/xrechnung.encoder.d.ts +4 -4
  43. package/dist_ts/formats/ubl/xrechnung/xrechnung.encoder.js +6 -6
  44. package/dist_ts/formats/utils/document.typecode.d.ts +18 -0
  45. package/dist_ts/formats/utils/document.typecode.js +52 -0
  46. package/dist_ts/formats/utils/unit.codes.d.ts +40 -0
  47. package/dist_ts/formats/utils/unit.codes.js +193 -0
  48. package/dist_ts/formats/validation/codelist.validator.d.ts +1 -1
  49. package/dist_ts/formats/validation/codelist.validator.js +10 -12
  50. package/dist_ts/formats/validation/en16931.business-rules.validator.js +13 -13
  51. package/dist_ts/formats/validation/validation.types.d.ts +0 -4
  52. package/dist_ts/formats/validation/validation.types.js +1 -28
  53. package/dist_ts/index.d.ts +7 -1
  54. package/dist_ts/index.js +11 -1
  55. package/dist_ts/interfaces/common.d.ts +11 -0
  56. package/dist_ts_install/download-schematron.js +3 -5
  57. package/package.json +7 -7
  58. package/readme.md +99 -2
  59. package/ts/00_commitinfo_data.ts +1 -1
  60. package/ts/einvoice.ts +59 -18
  61. package/ts/formats/base/base.decoder.ts +6 -6
  62. package/ts/formats/base/base.encoder.ts +4 -4
  63. package/ts/formats/base/base.validator.ts +28 -0
  64. package/ts/formats/cii/cii.decoder.ts +18 -14
  65. package/ts/formats/cii/cii.encoder.ts +11 -11
  66. package/ts/formats/cii/cii.validator.ts +47 -0
  67. package/ts/formats/cii/facturx/facturx.decoder.ts +23 -23
  68. package/ts/formats/cii/facturx/facturx.encoder.ts +20 -16
  69. package/ts/formats/cii/facturx/facturx.validator.ts +9 -3
  70. package/ts/formats/cii/zugferd/zugferd.decoder.ts +23 -23
  71. package/ts/formats/cii/zugferd/zugferd.encoder.ts +21 -17
  72. package/ts/formats/cii/zugferd/zugferd.v1.decoder.ts +23 -23
  73. package/ts/formats/cii/zugferd/zugferd.validator.ts +4 -3
  74. package/ts/formats/converters/xml-to-einvoice.converter.ts +11 -1
  75. package/ts/formats/semantic/semantic.adapter.ts +4 -35
  76. package/ts/formats/ubl/en16931.ubl.validator.ts +6 -0
  77. package/ts/formats/ubl/generic/ubl.encoder.ts +29 -24
  78. package/ts/formats/ubl/ubl.decoder.ts +23 -14
  79. package/ts/formats/ubl/ubl.encoder.ts +11 -11
  80. package/ts/formats/ubl/ubl.validator.ts +47 -0
  81. package/ts/formats/ubl/xrechnung/xrechnung.decoder.ts +23 -23
  82. package/ts/formats/ubl/xrechnung/xrechnung.encoder.ts +10 -10
  83. package/ts/formats/utils/document.typecode.ts +62 -0
  84. package/ts/formats/utils/unit.codes.ts +215 -0
  85. package/ts/formats/validation/codelist.validator.ts +12 -23
  86. package/ts/formats/validation/en16931.business-rules.validator.ts +13 -13
  87. package/ts/formats/validation/validation.types.ts +0 -28
  88. package/ts/index.ts +25 -0
  89. package/ts/interfaces/common.ts +17 -0
  90. package/ts/readme.md +3 -2
@@ -1,5 +1,10 @@
1
1
  import { CIIBaseDecoder } from '../cii.decoder.js';
2
- import type { TInvoice, TCreditNote, TDebitNote } from '../../../interfaces/common.js';
2
+ import type {
3
+ TAccountingDocEnvelope,
4
+ TAccountingDocType,
5
+ TCreditNote,
6
+ TInvoiceDocument,
7
+ } from '../../../interfaces/common.js';
3
8
  import { ZUGFERD_V1_NAMESPACES } from '../cii.types.js';
4
9
  import { business, finance } from '../../../plugins.js';
5
10
  import { EN16931Validator } from '../../validation/en16931.validator.js';
@@ -28,36 +33,28 @@ export class ZUGFeRDV1Decoder extends CIIBaseDecoder {
28
33
  * @returns Promise resolving to a TCreditNote object
29
34
  */
30
35
  protected async decodeCreditNote(): Promise<TCreditNote> {
31
- // Get common invoice data
32
- const commonData = await this.extractCommonData();
33
-
34
- // Create a credit note with the common data
35
- return {
36
- ...commonData,
37
- accountingDocType: 'creditnote' as const
38
- } as unknown as TCreditNote;
36
+ return this.extractCommonData('creditnote');
39
37
  }
40
38
 
41
39
  /**
42
- * Decodes a ZUGFeRD v1 debit note (invoice)
43
- * @returns Promise resolving to a TDebitNote object
40
+ * Decodes a ZUGFeRD v1 invoice document: an invoice, a debit note or a self-billed invoice
41
+ * @param accountingDocType The type the document type code stands for
42
+ * @returns Promise resolving to the invoice document
44
43
  */
45
- protected async decodeDebitNote(): Promise<TDebitNote> {
46
- // Get common invoice data
47
- const commonData = await this.extractCommonData();
48
-
49
- // Create a debit note with the common data
50
- return {
51
- ...commonData,
52
- accountingDocType: 'debitnote' as const
53
- } as unknown as TDebitNote;
44
+ protected async decodeInvoice(
45
+ accountingDocType: TInvoiceDocument['accountingDocType'],
46
+ ): Promise<TInvoiceDocument> {
47
+ return this.extractCommonData(accountingDocType);
54
48
  }
55
49
 
56
50
  /**
57
51
  * Extracts common invoice data from ZUGFeRD v1 XML
52
+ * @param accountingDocType The type the document is decoded as
58
53
  * @returns Common invoice data
59
54
  */
60
- private async extractCommonData(): Promise<Partial<TInvoice>> {
55
+ private async extractCommonData<TYPE extends TAccountingDocType>(
56
+ accountingDocType: TYPE,
57
+ ): Promise<TAccountingDocEnvelope<TYPE, {}>> {
61
58
  // Extract invoice ID
62
59
  const invoiceId = this.getText('//ram:ID');
63
60
 
@@ -108,10 +105,12 @@ export class ZUGFeRDV1Decoder extends CIIBaseDecoder {
108
105
  // Create the common invoice data
109
106
  const invoiceData = {
110
107
  type: 'accounting-doc' as const,
111
- accountingDocType: 'invoice' as const,
108
+ accountingDocType,
112
109
  id: invoiceId,
113
110
  accountingDocId: invoiceId,
114
111
  date: issueDate,
112
+ // a document read from XML has been issued; the envelope carries that as the document and the accounting status
113
+ status: 'issued',
115
114
  accountingDocStatus: 'issued' as const,
116
115
  versionInfo: {
117
116
  type: 'final' as const,
@@ -207,7 +206,8 @@ export class ZUGFeRDV1Decoder extends CIIBaseDecoder {
207
206
  const name = this.getText('ram:SpecifiedTradeProduct/ram:Name', itemNode);
208
207
  const articleNumber = this.getText('ram:SpecifiedTradeProduct/ram:SellerAssignedID', itemNode);
209
208
  const unitQuantity = this.getNumber('ram:SpecifiedLineTradeDelivery/ram:BilledQuantity', itemNode);
210
- const unitType = this.getText('ram:SpecifiedLineTradeDelivery/ram:BilledQuantity/@unitCode', itemNode) || 'EA';
209
+ // an absent unit code (BT-130) stays absent, so that BR-23 reports it
210
+ const unitType = this.getText('ram:SpecifiedLineTradeDelivery/ram:BilledQuantity/@unitCode', itemNode);
211
211
  const unitNetPrice = this.getNumber('ram:SpecifiedLineTradeAgreement/ram:NetPriceProductTradePrice/ram:ChargeAmount', itemNode);
212
212
  const vatPercentage = this.getNumber('ram:SpecifiedLineTradeSettlement/ram:ApplicableTradeTax/ram:RateApplicablePercent', itemNode);
213
213
 
@@ -24,8 +24,9 @@ export class ZUGFeRDValidator extends CIIBaseValidator {
24
24
  * @returns True if business validation passed
25
25
  */
26
26
  protected validateBusinessRules(): boolean {
27
- // Implement ZUGFeRD-specific business rules
28
- // For now, we'll just use the base CII validation
29
- return true;
27
+ // BR-23: every line has a unit code
28
+ const linesHaveUnits = this.validateLineUnitCodePresence();
29
+ // BR-CL-23: unit codes of UN/ECE Recommendation 20 with the Recommendation 21 extension
30
+ return this.validateUnitCodes() && linesHaveUnits;
30
31
  }
31
32
  }
@@ -100,7 +100,8 @@ export class XMLToEInvoiceConverter {
100
100
  position: i,
101
101
  name: this.getElementTextFromNode(line, 'cbc:Name') || `Item ${i + 1}`,
102
102
  unitQuantity: parseFloat(this.getElementTextFromNode(line, 'cbc:InvoicedQuantity') || '1'),
103
- unitType: 'C62',
103
+ // the unit code (BT-130) the document states, or none, which BR-23 reports
104
+ unitType: this.getAttributeFromNode(line, 'cbc:InvoicedQuantity', 'unitCode'),
104
105
  unitNetPrice: parseFloat(this.getElementTextFromNode(line, 'cbc:PriceAmount') || '100'),
105
106
  vatPercentage: parseFloat(this.getElementTextFromNode(line, 'cbc:Percent') || '19')
106
107
  };
@@ -118,6 +119,15 @@ export class XMLToEInvoiceConverter {
118
119
  return mockInvoice as unknown as EInvoice;
119
120
  }
120
121
 
122
+ /**
123
+ * Helper to get an attribute of the first matching element below a node, or '' when the element
124
+ * or the attribute is absent
125
+ */
126
+ private getAttributeFromNode(node: Element, tagName: string, attributeName: string): string {
127
+ const element = node.getElementsByTagName(tagName)[0];
128
+ return element?.getAttribute(attributeName) ?? '';
129
+ }
130
+
121
131
  /**
122
132
  * Helper to get element text from a node
123
133
  */
@@ -4,6 +4,7 @@
4
4
  */
5
5
 
6
6
  import { EInvoice } from '../../einvoice.js';
7
+ import { getAccountingDocType, getDocumentTypeCode } from '../utils/document.typecode.js';
7
8
  import type {
8
9
  EN16931SemanticModel,
9
10
  Seller,
@@ -36,7 +37,7 @@ export class SemanticModelAdapter {
36
37
  documentInformation: {
37
38
  invoiceNumber: invoice.accountingDocId,
38
39
  issueDate: invoice.issueDate,
39
- typeCode: this.mapInvoiceType(invoice.accountingDocType),
40
+ typeCode: getDocumentTypeCode(invoice.accountingDocType),
40
41
  currencyCode: invoice.currency,
41
42
  notes: invoice.notes ? this.mapNotes(invoice.notes) : undefined
42
43
  },
@@ -126,7 +127,7 @@ export class SemanticModelAdapter {
126
127
  const invoice = new EInvoice();
127
128
  invoice.accountingDocId = model.documentInformation.invoiceNumber;
128
129
  invoice.issueDate = model.documentInformation.issueDate;
129
- invoice.accountingDocType = this.reverseMapInvoiceType(model.documentInformation.typeCode) as 'invoice';
130
+ invoice.accountingDocType = getAccountingDocType(model.documentInformation.typeCode);
130
131
  invoice.currency = model.documentInformation.currencyCode as any;
131
132
  invoice.from = this.reverseMapSeller(model.seller);
132
133
  invoice.to = this.reverseMapBuyer(model.buyer);
@@ -231,38 +232,6 @@ export class SemanticModelAdapter {
231
232
  return invoice;
232
233
  }
233
234
 
234
- /**
235
- * Map invoice type code
236
- */
237
- private mapInvoiceType(type: string): string {
238
- const typeMap: Record<string, string> = {
239
- 'invoice': '380',
240
- 'creditNote': '381',
241
- 'debitNote': '383',
242
- 'correctedInvoice': '384',
243
- 'prepaymentInvoice': '386',
244
- 'selfBilledInvoice': '389',
245
- 'invoice_380': '380',
246
- 'credit_note_381': '381'
247
- };
248
- return typeMap[type] || '380';
249
- }
250
-
251
- /**
252
- * Reverse map invoice type code
253
- */
254
- private reverseMapInvoiceType(code: string): string {
255
- const typeMap: Record<string, string> = {
256
- '380': 'invoice',
257
- '381': 'creditNote',
258
- '383': 'debitNote',
259
- '384': 'correctedInvoice',
260
- '386': 'prepaymentInvoice',
261
- '389': 'selfBilledInvoice'
262
- };
263
- return typeMap[code] || 'invoice';
264
- }
265
-
266
235
  /**
267
236
  * Map notes
268
237
  */
@@ -473,7 +442,7 @@ export class SemanticModelAdapter {
473
442
  identifier: (index + 1).toString(),
474
443
  note: (item as any).description || (item as any).text || '',
475
444
  invoicedQuantity: item.unitQuantity,
476
- invoicedQuantityUnitOfMeasureCode: item.unitType || 'C62',
445
+ invoicedQuantityUnitOfMeasureCode: item.unitType,
477
446
  lineExtensionAmount: item.unitNetPrice * item.unitQuantity,
478
447
  purchaseOrderLineReference: (item as any).purchaseOrderLineRef,
479
448
  buyerAccountingReference: (item as any).buyerAccountingRef,
@@ -148,6 +148,12 @@ export class EN16931UBLValidator extends UBLBaseValidator {
148
148
  valid = false;
149
149
  }
150
150
 
151
+ // BR-23: every line has a unit code
152
+ valid = this.validateLineUnitCodePresence() && valid;
153
+
154
+ // BR-CL-23: unit codes of UN/ECE Recommendation 20 with the Recommendation 21 extension
155
+ valid = this.validateUnitCodes() && valid;
156
+
151
157
  // Validate calculation rules if we have the necessary data
152
158
  if (this.exists('//cac:LegalMonetaryTotal/cbc:LineExtensionAmount')) {
153
159
  valid = this.validateCalculationRules() && valid;
@@ -1,7 +1,8 @@
1
1
  import { UBLBaseEncoder } from '../ubl.encoder.js';
2
- import type { TInvoice, TCreditNote, TDebitNote } from '../../../interfaces/common.js';
2
+ import type { TAccountingDoc, TCreditNote, TInvoiceDocument } from '../../../interfaces/common.js';
3
3
  import { UBLDocumentType } from '../ubl.types.js';
4
4
  import { DOMParser, XMLSerializer } from '../../../plugins.js';
5
+ import { getDocumentTypeCode } from '../../utils/document.typecode.js';
5
6
 
6
7
  /**
7
8
  * UBL Encoder implementation
@@ -19,7 +20,7 @@ export class UBLEncoder extends UBLBaseEncoder {
19
20
  const doc = new DOMParser().parseFromString(xmlString, 'application/xml');
20
21
 
21
22
  // Add common document elements
22
- this.addCommonElements(doc, creditNote as unknown as TInvoice, UBLDocumentType.CREDIT_NOTE);
23
+ this.addCommonElements(doc, creditNote, UBLDocumentType.CREDIT_NOTE);
23
24
 
24
25
  // Add credit note specific data
25
26
  this.addCreditNoteSpecificData(doc, creditNote);
@@ -29,20 +30,20 @@ export class UBLEncoder extends UBLBaseEncoder {
29
30
  }
30
31
 
31
32
  /**
32
- * Encodes a debit note (invoice) into UBL XML
33
- * @param debitNote Debit note to encode
33
+ * Encodes an invoice document (invoice, debit note or self-billed invoice) into UBL XML
34
+ * @param invoice Invoice document to encode
34
35
  * @returns UBL XML string
35
36
  */
36
- protected async encodeDebitNote(debitNote: TDebitNote): Promise<string> {
37
+ protected async encodeInvoice(invoice: TInvoiceDocument): Promise<string> {
37
38
  // Create XML document from template
38
39
  const xmlString = this.createXmlRoot(UBLDocumentType.INVOICE);
39
40
  const doc = new DOMParser().parseFromString(xmlString, 'application/xml');
40
41
 
41
42
  // Add common document elements
42
- this.addCommonElements(doc, debitNote as unknown as TInvoice, UBLDocumentType.INVOICE);
43
+ this.addCommonElements(doc, invoice, UBLDocumentType.INVOICE);
43
44
 
44
45
  // Add invoice specific data
45
- this.addInvoiceSpecificData(doc, debitNote);
46
+ this.addInvoiceSpecificData(doc, invoice);
46
47
 
47
48
  // Serialize to string
48
49
  return new XMLSerializer().serializeToString(doc);
@@ -54,7 +55,7 @@ export class UBLEncoder extends UBLBaseEncoder {
54
55
  * @param invoice Invoice or credit note data
55
56
  * @param documentType Document type (Invoice or CreditNote)
56
57
  */
57
- private addCommonElements(doc: Document, invoice: TInvoice, documentType: UBLDocumentType): void {
58
+ private addCommonElements(doc: Document, invoice: TAccountingDoc, documentType: UBLDocumentType): void {
58
59
  const root = doc.documentElement;
59
60
 
60
61
  // UBL Version ID (2.1 is standard for EN16931)
@@ -79,12 +80,13 @@ export class UBLEncoder extends UBLBaseEncoder {
79
80
  dueDate.setDate(dueDate.getDate() + (invoice.dueInDays || 30));
80
81
  this.appendElement(doc, root, 'cbc:DueDate', this.formatDate(dueDate.getTime()));
81
82
  }
82
- // Document Type Code (380 invoice, 381 credit note; the element differs per schema)
83
- if (documentType === UBLDocumentType.INVOICE) {
84
- this.appendElement(doc, root, 'cbc:InvoiceTypeCode', '380');
85
- } else {
86
- this.appendElement(doc, root, 'cbc:CreditNoteTypeCode', '381');
87
- }
83
+ // Document Type Code (380 invoice, 381 credit note, 383 debit note, 389 self-billed invoice; the element differs per schema)
84
+ this.appendElement(
85
+ doc,
86
+ root,
87
+ documentType === UBLDocumentType.INVOICE ? 'cbc:InvoiceTypeCode' : 'cbc:CreditNoteTypeCode',
88
+ getDocumentTypeCode(invoice.accountingDocType),
89
+ );
88
90
 
89
91
  // Notes - include subject as first note if it exists
90
92
  const allNotes: string[] = [];
@@ -140,7 +142,7 @@ export class UBLEncoder extends UBLBaseEncoder {
140
142
  * @param doc XML document
141
143
  * @param invoice Invoice data
142
144
  */
143
- private addInvoiceSpecificData(doc: Document, invoice: TDebitNote): void {
145
+ private addInvoiceSpecificData(doc: Document, invoice: TInvoiceDocument): void {
144
146
  // For now, there's no specific data to add for invoices that's not already covered
145
147
  // If needed, additional invoice specific fields would be added here
146
148
  }
@@ -247,7 +249,7 @@ export class UBLEncoder extends UBLBaseEncoder {
247
249
  * @param parentElement Parent element
248
250
  * @param invoice Invoice data
249
251
  */
250
- private addPaymentTerms(doc: Document, parentElement: Element, invoice: TInvoice): void {
252
+ private addPaymentTerms(doc: Document, parentElement: Element, invoice: TAccountingDoc): void {
251
253
  const paymentTermsNode = doc.createElement('cac:PaymentTerms');
252
254
  parentElement.appendChild(paymentTermsNode);
253
255
 
@@ -266,7 +268,7 @@ export class UBLEncoder extends UBLBaseEncoder {
266
268
  * @param parentElement Parent element
267
269
  * @param invoice Invoice data
268
270
  */
269
- private addPaymentMeans(doc: Document, parentElement: Element, invoice: TInvoice): void {
271
+ private addPaymentMeans(doc: Document, parentElement: Element, invoice: TAccountingDoc): void {
270
272
  const paymentOptions = invoice.paymentOptions;
271
273
  if (!paymentOptions) {
272
274
  return;
@@ -315,7 +317,7 @@ export class UBLEncoder extends UBLBaseEncoder {
315
317
  * @param parentElement Parent element
316
318
  * @param invoice Invoice data
317
319
  */
318
- private addTaxTotal(doc: Document, parentElement: Element, invoice: TInvoice): void {
320
+ private addTaxTotal(doc: Document, parentElement: Element, invoice: TAccountingDoc): void {
319
321
  const taxTotalNode = doc.createElement('cac:TaxTotal');
320
322
  parentElement.appendChild(taxTotalNode);
321
323
 
@@ -392,7 +394,7 @@ export class UBLEncoder extends UBLBaseEncoder {
392
394
  * @param parentElement Parent element
393
395
  * @param invoice Invoice data
394
396
  */
395
- private addLegalMonetaryTotal(doc: Document, parentElement: Element, invoice: TInvoice): void {
397
+ private addLegalMonetaryTotal(doc: Document, parentElement: Element, invoice: TAccountingDoc): void {
396
398
  const legalMonetaryTotalNode = doc.createElement('cac:LegalMonetaryTotal');
397
399
  parentElement.appendChild(legalMonetaryTotalNode);
398
400
 
@@ -444,7 +446,7 @@ export class UBLEncoder extends UBLBaseEncoder {
444
446
  * @param parentElement Parent element
445
447
  * @param invoice Invoice data
446
448
  */
447
- private addInvoiceLines(doc: Document, parentElement: Element, invoice: TInvoice, documentType: UBLDocumentType = UBLDocumentType.INVOICE): void {
449
+ private addInvoiceLines(doc: Document, parentElement: Element, invoice: TAccountingDoc, documentType: UBLDocumentType = UBLDocumentType.INVOICE): void {
448
450
  if (!invoice.items) return;
449
451
  const creditNote = documentType === UBLDocumentType.CREDIT_NOTE;
450
452
 
@@ -457,7 +459,10 @@ export class UBLEncoder extends UBLBaseEncoder {
457
459
 
458
460
  // Invoiced quantity
459
461
  const quantityElement = doc.createElement(creditNote ? 'cbc:CreditedQuantity' : 'cbc:InvoicedQuantity');
460
- quantityElement.setAttribute('unitCode', item.unitType);
462
+ // a line without a unit is written without one, so that BR-23 reports it
463
+ if (item.unitType) {
464
+ quantityElement.setAttribute('unitCode', item.unitType);
465
+ }
461
466
  quantityElement.textContent = item.unitQuantity.toString();
462
467
  invoiceLineNode.appendChild(quantityElement);
463
468
 
@@ -544,7 +549,7 @@ export class UBLEncoder extends UBLBaseEncoder {
544
549
  * @param root Root element
545
550
  * @param invoice Invoice data
546
551
  */
547
- private preserveMetadata(doc: Document, root: Element, invoice: TInvoice): void {
552
+ private preserveMetadata(doc: Document, root: Element, invoice: TAccountingDoc): void {
548
553
  // Extract metadata if available
549
554
  const metadata = (invoice as any).metadata?.extensions;
550
555
  if (!metadata) return;
@@ -815,7 +820,7 @@ export class UBLEncoder extends UBLBaseEncoder {
815
820
  * @param doc XML document
816
821
  * @param invoice Invoice data
817
822
  */
818
- private enhancePartyInformationUBL(doc: Document, invoice: TInvoice): void {
823
+ private enhancePartyInformationUBL(doc: Document, invoice: TAccountingDoc): void {
819
824
  // Enhance supplier party
820
825
  this.enhancePartyUBL(doc, 'cac:AccountingSupplierParty', invoice.from);
821
826
 
@@ -940,7 +945,7 @@ export class UBLEncoder extends UBLBaseEncoder {
940
945
  * @param doc XML document
941
946
  * @param invoice Invoice data
942
947
  */
943
- private enhanceLineItemsUBL(doc: Document, invoice: TInvoice): void {
948
+ private enhanceLineItemsUBL(doc: Document, invoice: TAccountingDoc): void {
944
949
  const invoiceLines = doc.getElementsByTagName('cac:InvoiceLine');
945
950
 
946
951
  for (let i = 0; i < invoiceLines.length && i < invoice.items.length; i++) {
@@ -1,8 +1,9 @@
1
1
  import { BaseDecoder } from '../base/base.decoder.js';
2
- import type { TInvoice, TCreditNote, TDebitNote } from '../../interfaces/common.js';
2
+ import type { TAccountingDoc, TCreditNote, TInvoiceDocument } from '../../interfaces/common.js';
3
3
  import { UBLDocumentType, UBL_NAMESPACES } from './ubl.types.js';
4
4
  import { DOMParser, xpath } from '../../plugins.js';
5
5
  import { EInvoiceParsingError } from '../../errors.js';
6
+ import { getAccountingDocType } from '../utils/document.typecode.js';
6
7
 
7
8
  const ublParser = new DOMParser();
8
9
  const ublNamespaces = {
@@ -37,18 +38,23 @@ export abstract class UBLBaseDecoder extends BaseDecoder {
37
38
  }
38
39
 
39
40
  /**
40
- * Decodes UBL XML into a TInvoice object
41
- * @returns Promise resolving to a TInvoice object
41
+ * Decodes UBL XML into an accounting document
42
+ * @returns Promise resolving to the accounting document
42
43
  */
43
- public async decode(): Promise<TInvoice> {
44
- // Determine document type
45
- const documentType = this.getDocumentType();
46
-
47
- if (documentType === UBLDocumentType.CREDIT_NOTE) {
48
- return this.decodeCreditNote() as unknown as TInvoice;
49
- } else {
50
- return this.decodeDebitNote() as unknown as TInvoice;
44
+ public async decode(): Promise<TAccountingDoc> {
45
+ // a CreditNote root is a credit note whatever its type code says
46
+ if (this.getDocumentType() === UBLDocumentType.CREDIT_NOTE) {
47
+ return this.decodeCreditNote();
51
48
  }
49
+
50
+ // an Invoice root takes its type from the document type code (BT-3)
51
+ const accountingDocType = getAccountingDocType(
52
+ this.getText('./cbc:InvoiceTypeCode', this.doc.documentElement),
53
+ );
54
+ if (accountingDocType === 'creditnote') {
55
+ return this.decodeCreditNote();
56
+ }
57
+ return this.decodeInvoice(accountingDocType);
52
58
  }
53
59
 
54
60
  /**
@@ -83,10 +89,13 @@ export abstract class UBLBaseDecoder extends BaseDecoder {
83
89
  protected abstract decodeCreditNote(): Promise<TCreditNote>;
84
90
 
85
91
  /**
86
- * Decodes a UBL debit note (invoice)
87
- * @returns Promise resolving to a TDebitNote object
92
+ * Decodes a UBL invoice document: an invoice, a debit note or a self-billed invoice
93
+ * @param accountingDocType The type the document type code stands for
94
+ * @returns Promise resolving to the invoice document
88
95
  */
89
- protected abstract decodeDebitNote(): Promise<TDebitNote>;
96
+ protected abstract decodeInvoice(
97
+ accountingDocType: TInvoiceDocument['accountingDocType'],
98
+ ): Promise<TInvoiceDocument>;
90
99
 
91
100
  /**
92
101
  * Gets a text value from an XPath expression
@@ -1,5 +1,5 @@
1
1
  import { BaseEncoder } from '../base/base.encoder.js';
2
- import type { TInvoice, TCreditNote, TDebitNote } from '../../interfaces/common.js';
2
+ import type { TAccountingDoc, TCreditNote, TInvoiceDocument } from '../../interfaces/common.js';
3
3
  import { UBLDocumentType, UBL_NAMESPACES } from './ubl.types.js';
4
4
 
5
5
  /**
@@ -7,16 +7,16 @@ import { UBLDocumentType, UBL_NAMESPACES } from './ubl.types.js';
7
7
  */
8
8
  export abstract class UBLBaseEncoder extends BaseEncoder {
9
9
  /**
10
- * Encodes a TInvoice object into UBL XML
11
- * @param invoice TInvoice object to encode
10
+ * Encodes an accounting document into UBL XML
11
+ * @param invoice Accounting document to encode
12
12
  * @returns UBL XML string
13
13
  */
14
- public async encode(invoice: TInvoice): Promise<string> {
15
- // a credit note takes the credit-note path (type code 381); everything else is encoded as an invoice
16
- if ((invoice as { accountingDocType?: string }).accountingDocType === 'creditnote') {
17
- return this.encodeCreditNote(invoice as unknown as TCreditNote);
14
+ public async encode(invoice: TAccountingDoc): Promise<string> {
15
+ // a credit note is a CreditNote document; an invoice, a debit note and a self-billed invoice are Invoice documents that differ in their type code
16
+ if (invoice.accountingDocType === 'creditnote') {
17
+ return this.encodeCreditNote(invoice);
18
18
  }
19
- return this.encodeDebitNote(invoice as unknown as TDebitNote);
19
+ return this.encodeInvoice(invoice);
20
20
  }
21
21
 
22
22
  /**
@@ -27,11 +27,11 @@ export abstract class UBLBaseEncoder extends BaseEncoder {
27
27
  protected abstract encodeCreditNote(creditNote: TCreditNote): Promise<string>;
28
28
 
29
29
  /**
30
- * Encodes a TDebitNote object into UBL XML
31
- * @param debitNote TDebitNote object to encode
30
+ * Encodes an invoice document (invoice, debit note or self-billed invoice) into UBL XML
31
+ * @param invoice Invoice document to encode
32
32
  * @returns UBL XML string
33
33
  */
34
- protected abstract encodeDebitNote(debitNote: TDebitNote): Promise<string>;
34
+ protected abstract encodeInvoice(invoice: TInvoiceDocument): Promise<string>;
35
35
 
36
36
  /**
37
37
  * Creates the XML declaration and root element
@@ -3,6 +3,7 @@ import { ValidationLevel } from '../../interfaces/common.js';
3
3
  import type { ValidationResult } from '../../interfaces/common.js';
4
4
  import { UBLDocumentType, UBL_NAMESPACES } from './ubl.types.js';
5
5
  import { DOMParser, xpath } from '../../plugins.js';
6
+ import { meetsUnitCodeRule } from '../utils/unit.codes.js';
6
7
 
7
8
  const ublValidatorParser = new DOMParser();
8
9
  const ublValidatorNamespaces = {
@@ -104,6 +105,52 @@ export abstract class UBLBaseValidator extends BaseValidator {
104
105
  */
105
106
  protected abstract validateStructure(): boolean;
106
107
 
108
+ /**
109
+ * BR-23: every invoice or credit note line has a unit code on its invoiced or credited quantity.
110
+ * @returns True if every line has one
111
+ */
112
+ protected validateLineUnitCodePresence(): boolean {
113
+ // BR-23: every invoice or credit note line (BG-25) has an invoiced quantity unit of measure code (BT-130)
114
+ const lines = this.select('//cac:InvoiceLine | //cac:CreditNoteLine', this.doc) as Element[];
115
+ let valid = true;
116
+ for (const line of lines) {
117
+ if (!this.exists('./cbc:InvoicedQuantity/@unitCode | ./cbc:CreditedQuantity/@unitCode', line)) {
118
+ this.addError(
119
+ 'BR-23',
120
+ 'An Invoice line (BG-25) shall have an Invoiced quantity unit of measure code (BT-130)',
121
+ this.locationOf(line)
122
+ );
123
+ valid = false;
124
+ }
125
+ }
126
+ return valid;
127
+ }
128
+
129
+ /**
130
+ * BR-CL-23: every unit code of an invoiced, credited or base quantity (BT-130, BT-150) is a
131
+ * code of UN/ECE Recommendation 20 with the Recommendation 21 extension.
132
+ * @returns True if every unit code passed
133
+ */
134
+ protected validateUnitCodes(): boolean {
135
+ const quantities = this.select(
136
+ '//cbc:InvoicedQuantity[@unitCode] | //cbc:CreditedQuantity[@unitCode] | //cbc:BaseQuantity[@unitCode]',
137
+ this.doc
138
+ ) as Element[];
139
+ let valid = true;
140
+ for (const quantity of quantities) {
141
+ const unitCode = quantity.getAttribute('unitCode') ?? '';
142
+ if (!meetsUnitCodeRule(unitCode)) {
143
+ this.addError(
144
+ 'BR-CL-23',
145
+ `Unit code MUST be coded according to the UN/ECE Recommendation 20 with Rec 21 extension; found "${unitCode}"`,
146
+ `${this.locationOf(quantity)}/@unitCode`
147
+ );
148
+ valid = false;
149
+ }
150
+ }
151
+ return valid;
152
+ }
153
+
107
154
  /**
108
155
  * Gets a text value from an XPath expression
109
156
  * @param xpath XPath expression
@@ -1,5 +1,10 @@
1
1
  import { UBLBaseDecoder } from '../ubl.decoder.js';
2
- import type { TInvoice, TCreditNote, TDebitNote } from '../../../interfaces/common.js';
2
+ import type {
3
+ TAccountingDocEnvelope,
4
+ TAccountingDocType,
5
+ TCreditNote,
6
+ TInvoiceDocument,
7
+ } from '../../../interfaces/common.js';
3
8
  import { business, finance } from '../../../plugins.js';
4
9
  import { UBLDocumentType } from '../ubl.types.js';
5
10
  import { EN16931Validator } from '../../validation/en16931.validator.js';
@@ -14,36 +19,28 @@ export class XRechnungDecoder extends UBLBaseDecoder {
14
19
  * @returns Promise resolving to a TCreditNote object
15
20
  */
16
21
  protected async decodeCreditNote(): Promise<TCreditNote> {
17
- // Extract common data
18
- const commonData = await this.extractCommonData();
19
-
20
- // Return the invoice data as a credit note
21
- return {
22
- ...commonData,
23
- accountingDocType: 'creditnote' as const
24
- } as unknown as TCreditNote;
22
+ return this.extractCommonData('creditnote');
25
23
  }
26
24
 
27
25
  /**
28
- * Decodes a UBL debit note (invoice)
29
- * @returns Promise resolving to a TDebitNote object
26
+ * Decodes a UBL invoice document: an invoice, a debit note or a self-billed invoice
27
+ * @param accountingDocType The type the document type code stands for
28
+ * @returns Promise resolving to the invoice document
30
29
  */
31
- protected async decodeDebitNote(): Promise<TDebitNote> {
32
- // Extract common data
33
- const commonData = await this.extractCommonData();
34
-
35
- // Return the invoice data as a debit note
36
- return {
37
- ...commonData,
38
- accountingDocType: 'debitnote' as const
39
- } as unknown as TDebitNote;
30
+ protected async decodeInvoice(
31
+ accountingDocType: TInvoiceDocument['accountingDocType'],
32
+ ): Promise<TInvoiceDocument> {
33
+ return this.extractCommonData(accountingDocType);
40
34
  }
41
35
 
42
36
  /**
43
37
  * Extracts common invoice data from XRechnung XML
38
+ * @param accountingDocType The type the document is decoded as
44
39
  * @returns Common invoice data
45
40
  */
46
- private async extractCommonData(): Promise<Partial<TInvoice>> {
41
+ private async extractCommonData<TYPE extends TAccountingDocType>(
42
+ accountingDocType: TYPE,
43
+ ): Promise<TAccountingDocEnvelope<TYPE, {}>> {
47
44
  try {
48
45
  const root = this.doc.documentElement;
49
46
  const invoiceId = this.getRequiredText(
@@ -86,7 +83,8 @@ export class XRechnungDecoder extends UBLBaseDecoder {
86
83
  const buyerItemID = this.getText('./cac:Item/cac:BuyersItemIdentification/cbc:ID', line) || '';
87
84
  const standardItemID = this.getText('./cac:Item/cac:StandardItemIdentification/cbc:ID', line) || '';
88
85
  const commodityClassification = this.getText('./cac:Item/cac:CommodityClassification/cbc:ItemClassificationCode', line) || '';
89
- const unitType = this.getText(`./${quantityElement}/@unitCode`, line) || 'EA';
86
+ // an absent unit code (BT-130) stays absent, so that BR-23 reports it
87
+ const unitType = this.getText(`./${quantityElement}/@unitCode`, line);
90
88
 
91
89
  let unitQuantity = 1;
92
90
  const quantityText = this.getText(`./${quantityElement}`, line);
@@ -236,10 +234,12 @@ export class XRechnungDecoder extends UBLBaseDecoder {
236
234
  // Create the common invoice data with metadata only when the source XML actually contains it.
237
235
  const invoiceData: any = {
238
236
  type: 'accounting-doc' as const,
239
- accountingDocType: 'invoice' as const,
237
+ accountingDocType,
240
238
  id: invoiceId,
241
239
  accountingDocId: invoiceId,
242
240
  date: issueDate,
241
+ // a document read from XML has been issued; the envelope carries that as the document and the accounting status
242
+ status: 'issued',
243
243
  accountingDocStatus: 'issued' as const,
244
244
  versionInfo: {
245
245
  type: 'final',