@fin.cx/einvoice 8.0.0 → 8.2.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 (93) hide show
  1. package/dist_ts/00_commitinfo_data.js +1 -1
  2. package/dist_ts/einvoice.d.ts +14 -1
  3. package/dist_ts/einvoice.js +48 -3
  4. package/dist_ts/formats/base/base.encoder.d.ts +1 -1
  5. package/dist_ts/formats/base/base.validator.d.ts +5 -0
  6. package/dist_ts/formats/base/base.validator.js +28 -1
  7. package/dist_ts/formats/cii/cii.decoder.d.ts +12 -1
  8. package/dist_ts/formats/cii/cii.decoder.js +34 -1
  9. package/dist_ts/formats/cii/cii.encoder.d.ts +14 -3
  10. package/dist_ts/formats/cii/cii.encoder.js +58 -6
  11. package/dist_ts/formats/cii/cii.types.d.ts +1 -0
  12. package/dist_ts/formats/cii/cii.types.js +3 -2
  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 +1 -1
  16. package/dist_ts/formats/cii/facturx/facturx.decoder.js +11 -5
  17. package/dist_ts/formats/cii/facturx/facturx.encoder.d.ts +2 -2
  18. package/dist_ts/formats/cii/facturx/facturx.encoder.js +17 -11
  19. package/dist_ts/formats/cii/facturx/facturx.validator.js +8 -4
  20. package/dist_ts/formats/cii/zugferd/zugferd.decoder.d.ts +1 -1
  21. package/dist_ts/formats/cii/zugferd/zugferd.decoder.js +11 -5
  22. package/dist_ts/formats/cii/zugferd/zugferd.encoder.d.ts +1 -1
  23. package/dist_ts/formats/cii/zugferd/zugferd.encoder.js +20 -14
  24. package/dist_ts/formats/cii/zugferd/zugferd.v1.decoder.d.ts +7 -1
  25. package/dist_ts/formats/cii/zugferd/zugferd.v1.decoder.js +29 -5
  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.js +2 -2
  30. package/dist_ts/formats/ubl/en16931.ubl.validator.js +5 -1
  31. package/dist_ts/formats/ubl/generic/ubl.encoder.d.ts +23 -1
  32. package/dist_ts/formats/ubl/generic/ubl.encoder.js +71 -12
  33. package/dist_ts/formats/ubl/ubl.decoder.d.ts +1 -1
  34. package/dist_ts/formats/ubl/ubl.encoder.d.ts +16 -3
  35. package/dist_ts/formats/ubl/ubl.encoder.js +75 -13
  36. package/dist_ts/formats/ubl/ubl.validator.d.ts +11 -0
  37. package/dist_ts/formats/ubl/ubl.validator.js +35 -1
  38. package/dist_ts/formats/ubl/xrechnung/xrechnung.decoder.d.ts +6 -1
  39. package/dist_ts/formats/ubl/xrechnung/xrechnung.decoder.js +39 -8
  40. package/dist_ts/formats/ubl/xrechnung/xrechnung.encoder.d.ts +1 -1
  41. package/dist_ts/formats/ubl/xrechnung/xrechnung.encoder.js +4 -2
  42. package/dist_ts/formats/utils/date.value.d.ts +10 -0
  43. package/dist_ts/formats/utils/date.value.js +21 -0
  44. package/dist_ts/formats/utils/document.typecode.d.ts +3 -3
  45. package/dist_ts/formats/utils/document.typecode.js +8 -4
  46. package/dist_ts/formats/utils/preceding.invoice.d.ts +26 -0
  47. package/dist_ts/formats/utils/preceding.invoice.js +52 -0
  48. package/dist_ts/formats/utils/unit.codes.d.ts +40 -0
  49. package/dist_ts/formats/utils/unit.codes.js +193 -0
  50. package/dist_ts/formats/validation/codelist.validator.d.ts +1 -1
  51. package/dist_ts/formats/validation/codelist.validator.js +7 -9
  52. package/dist_ts/formats/validation/en16931.business-rules.validator.js +13 -13
  53. package/dist_ts/formats/validation/validation.types.d.ts +0 -4
  54. package/dist_ts/formats/validation/validation.types.js +1 -28
  55. package/dist_ts/index.d.ts +5 -1
  56. package/dist_ts/index.js +6 -1
  57. package/dist_ts/interfaces/common.d.ts +6 -2
  58. package/dist_ts_install/download-schematron.js +3 -5
  59. package/package.json +8 -8
  60. package/readme.md +103 -10
  61. package/ts/00_commitinfo_data.ts +1 -1
  62. package/ts/einvoice.ts +51 -2
  63. package/ts/formats/base/base.encoder.ts +1 -1
  64. package/ts/formats/base/base.validator.ts +28 -0
  65. package/ts/formats/cii/cii.decoder.ts +43 -1
  66. package/ts/formats/cii/cii.encoder.ts +62 -6
  67. package/ts/formats/cii/cii.types.ts +2 -1
  68. package/ts/formats/cii/cii.validator.ts +47 -0
  69. package/ts/formats/cii/facturx/facturx.decoder.ts +11 -4
  70. package/ts/formats/cii/facturx/facturx.encoder.ts +17 -10
  71. package/ts/formats/cii/facturx/facturx.validator.ts +9 -3
  72. package/ts/formats/cii/zugferd/zugferd.decoder.ts +11 -4
  73. package/ts/formats/cii/zugferd/zugferd.encoder.ts +20 -13
  74. package/ts/formats/cii/zugferd/zugferd.v1.decoder.ts +32 -4
  75. package/ts/formats/cii/zugferd/zugferd.validator.ts +4 -3
  76. package/ts/formats/converters/xml-to-einvoice.converter.ts +11 -1
  77. package/ts/formats/semantic/semantic.adapter.ts +1 -1
  78. package/ts/formats/ubl/en16931.ubl.validator.ts +6 -0
  79. package/ts/formats/ubl/generic/ubl.encoder.ts +78 -11
  80. package/ts/formats/ubl/ubl.decoder.ts +1 -1
  81. package/ts/formats/ubl/ubl.encoder.ts +77 -13
  82. package/ts/formats/ubl/ubl.validator.ts +47 -0
  83. package/ts/formats/ubl/xrechnung/xrechnung.decoder.ts +47 -7
  84. package/ts/formats/ubl/xrechnung/xrechnung.encoder.ts +4 -1
  85. package/ts/formats/utils/date.value.ts +21 -0
  86. package/ts/formats/utils/document.typecode.ts +7 -3
  87. package/ts/formats/utils/preceding.invoice.ts +74 -0
  88. package/ts/formats/utils/unit.codes.ts +215 -0
  89. package/ts/formats/validation/codelist.validator.ts +8 -19
  90. package/ts/formats/validation/en16931.business-rules.validator.ts +13 -13
  91. package/ts/formats/validation/validation.types.ts +0 -28
  92. package/ts/index.ts +12 -0
  93. package/ts/interfaces/common.ts +6 -1
@@ -4,6 +4,7 @@ import { ZUGFERD_PROFILE_IDS } from './zugferd.types.js';
4
4
  import { CIIProfile } from '../cii.types.js';
5
5
  import { DOMParser, XMLSerializer } from '../../../plugins.js';
6
6
  import { getDocumentTypeCode } from '../../utils/document.typecode.js';
7
+ import { getWritableDate } from '../../utils/date.value.js';
7
8
 
8
9
  /**
9
10
  * Encoder for ZUGFeRD invoice format
@@ -35,7 +36,7 @@ export class ZUGFeRDEncoder extends CIIBaseEncoder {
35
36
  }
36
37
 
37
38
  /**
38
- * Encodes an invoice document (invoice, debit note or self-billed invoice) into ZUGFeRD XML
39
+ * Encodes an invoice document (invoice, corrected invoice, debit note or self-billed invoice) into ZUGFeRD XML
39
40
  * @param invoice Invoice document to encode
40
41
  * @returns ZUGFeRD XML string
41
42
  */
@@ -43,7 +44,7 @@ export class ZUGFeRDEncoder extends CIIBaseEncoder {
43
44
  // Create base XML
44
45
  const xmlDoc = this.createBaseXml();
45
46
 
46
- // Set document type code (380 invoice, 383 debit note, 389 self-billed invoice)
47
+ // Set document type code (380 invoice, 383 debit note, 384 corrected invoice, 389 self-billed invoice)
47
48
  this.setDocumentTypeCode(xmlDoc, getDocumentTypeCode(invoice.accountingDocType));
48
49
 
49
50
  // Add common invoice data
@@ -149,7 +150,7 @@ export class ZUGFeRDEncoder extends CIIBaseEncoder {
149
150
  const issueDateElement = doc.createElement('ram:IssueDateTime');
150
151
  const dateStringElement = doc.createElement('udt:DateTimeString');
151
152
  dateStringElement.setAttribute('format', '102'); // YYYYMMDD format
152
- dateStringElement.textContent = this.formatDateYYYYMMDD(invoice.date);
153
+ dateStringElement.textContent = this.formatDateYYYYMMDD(invoice.date, 'BT-2 invoice issue date');
153
154
  issueDateElement.appendChild(dateStringElement);
154
155
  documentElement.appendChild(issueDateElement);
155
156
 
@@ -187,6 +188,9 @@ export class ZUGFeRDEncoder extends CIIBaseEncoder {
187
188
  // Add settlement section with payment terms and totals
188
189
  this.addSettlementSection(doc, transactionElement, invoice);
189
190
 
191
+ // Add the preceding invoice references (BG-3) the document states
192
+ this.addPrecedingInvoiceReferences(doc, invoice);
193
+
190
194
  // Add line items
191
195
  this.addLineItems(doc, transactionElement, invoice);
192
196
  }
@@ -316,12 +320,12 @@ export class ZUGFeRDEncoder extends CIIBaseEncoder {
316
320
  transactionElement.appendChild(deliveryElement);
317
321
 
318
322
  // Add delivery date if available
319
- if (invoice.deliveryDate) {
323
+ if (invoice.deliveryDate !== undefined) {
320
324
  const deliveryDateElement = doc.createElement('ram:ActualDeliverySupplyChainEvent');
321
325
  const occurrenceDateElement = doc.createElement('ram:OccurrenceDateTime');
322
326
  const dateStringElement = doc.createElement('udt:DateTimeString');
323
327
  dateStringElement.setAttribute('format', '102'); // YYYYMMDD format
324
- dateStringElement.textContent = this.formatDateYYYYMMDD(invoice.deliveryDate);
328
+ dateStringElement.textContent = this.formatDateYYYYMMDD(invoice.deliveryDate, 'BT-72 actual delivery date');
325
329
  occurrenceDateElement.appendChild(dateStringElement);
326
330
  deliveryDateElement.appendChild(occurrenceDateElement);
327
331
  deliveryElement.appendChild(deliveryDateElement);
@@ -332,21 +336,21 @@ export class ZUGFeRDEncoder extends CIIBaseEncoder {
332
336
  const periodElement = doc.createElement('ram:BillingSpecifiedPeriod');
333
337
 
334
338
  // Start date
335
- if (invoice.periodOfPerformance.from) {
339
+ if (invoice.periodOfPerformance.from !== undefined) {
336
340
  const startDateElement = doc.createElement('ram:StartDateTime');
337
341
  const startDateStringElement = doc.createElement('udt:DateTimeString');
338
342
  startDateStringElement.setAttribute('format', '102'); // YYYYMMDD format
339
- startDateStringElement.textContent = this.formatDateYYYYMMDD(invoice.periodOfPerformance.from);
343
+ startDateStringElement.textContent = this.formatDateYYYYMMDD(invoice.periodOfPerformance.from, 'BT-73 invoicing period start date');
340
344
  startDateElement.appendChild(startDateStringElement);
341
345
  periodElement.appendChild(startDateElement);
342
346
  }
343
347
 
344
348
  // End date
345
- if (invoice.periodOfPerformance.to) {
349
+ if (invoice.periodOfPerformance.to !== undefined) {
346
350
  const endDateElement = doc.createElement('ram:EndDateTime');
347
351
  const endDateStringElement = doc.createElement('udt:DateTimeString');
348
352
  endDateStringElement.setAttribute('format', '102'); // YYYYMMDD format
349
- endDateStringElement.textContent = this.formatDateYYYYMMDD(invoice.periodOfPerformance.to);
353
+ endDateStringElement.textContent = this.formatDateYYYYMMDD(invoice.periodOfPerformance.to, 'BT-74 invoicing period end date');
350
354
  endDateElement.appendChild(endDateStringElement);
351
355
  periodElement.appendChild(endDateElement);
352
356
  }
@@ -391,7 +395,7 @@ export class ZUGFeRDEncoder extends CIIBaseEncoder {
391
395
  const dueDate = new Date(invoice.date);
392
396
  dueDate.setDate(dueDate.getDate() + invoice.dueInDays);
393
397
 
394
- dateStringElement.textContent = this.formatDateYYYYMMDD(dueDate.getTime());
398
+ dateStringElement.textContent = this.formatDateYYYYMMDD(dueDate.getTime(), 'BT-9 payment due date');
395
399
  dueDateElement.appendChild(dateStringElement);
396
400
  paymentTermsElement.appendChild(dueDateElement);
397
401
 
@@ -599,7 +603,10 @@ export class ZUGFeRDEncoder extends CIIBaseEncoder {
599
603
  const deliveryElement = doc.createElement('ram:SpecifiedLineTradeDelivery');
600
604
  const quantityElement = doc.createElement('ram:BilledQuantity');
601
605
  quantityElement.textContent = item.unitQuantity.toString();
602
- quantityElement.setAttribute('unitCode', item.unitType);
606
+ // a line without a unit is written without one, so that BR-23 reports it
607
+ if (item.unitType) {
608
+ quantityElement.setAttribute('unitCode', item.unitType);
609
+ }
603
610
  deliveryElement.appendChild(quantityElement);
604
611
  lineItemElement.appendChild(deliveryElement);
605
612
 
@@ -652,8 +659,8 @@ export class ZUGFeRDEncoder extends CIIBaseEncoder {
652
659
  * @param timestamp Timestamp to format
653
660
  * @returns Formatted date string
654
661
  */
655
- private formatDateYYYYMMDD(timestamp: number): string {
656
- const date = new Date(timestamp);
662
+ private formatDateYYYYMMDD(timestamp: number, field: string): string {
663
+ const date = getWritableDate(timestamp, field, 'cii');
657
664
  const year = date.getFullYear();
658
665
  const month = (date.getMonth() + 1).toString().padStart(2, '0');
659
666
  const day = date.getDate().toString().padStart(2, '0');
@@ -6,9 +6,16 @@ import type {
6
6
  TInvoiceDocument,
7
7
  } from '../../../interfaces/common.js';
8
8
  import { ZUGFERD_V1_NAMESPACES } from '../cii.types.js';
9
- import { business, finance } from '../../../plugins.js';
9
+ import { business, finance, xpath } from '../../../plugins.js';
10
10
  import { EN16931Validator } from '../../validation/en16931.validator.js';
11
11
 
12
+ /** XPath selection with the ZUGFeRD v1 namespaces */
13
+ const zugferdV1Select = xpath.useNamespaces({
14
+ rsm: ZUGFERD_V1_NAMESPACES.RSM,
15
+ ram: ZUGFERD_V1_NAMESPACES.RAM,
16
+ udt: ZUGFERD_V1_NAMESPACES.UDT,
17
+ });
18
+
12
19
  /**
13
20
  * Decoder for ZUGFeRD v1 invoice format
14
21
  */
@@ -28,6 +35,22 @@ export class ZUGFeRDV1Decoder extends CIIBaseDecoder {
28
35
  };
29
36
  }
30
37
 
38
+ /**
39
+ * Reads the actual delivery date of the v1 header delivery
40
+ * (`ram:ApplicableSupplyChainTradeDelivery`); undefined when the document
41
+ * does not state one.
42
+ */
43
+ protected override extractDeliveryDate(): number | undefined {
44
+ const deliveryDatePath =
45
+ '/rsm:CrossIndustryDocument/rsm:SpecifiedSupplyChainTradeTransaction/ram:ApplicableSupplyChainTradeDelivery/ram:ActualDeliverySupplyChainEvent/ram:OccurrenceDateTime/udt:DateTimeString';
46
+ const deliveryDate = String(zugferdV1Select(`string(${deliveryDatePath})`, this.doc)).trim();
47
+ if (!deliveryDate) {
48
+ return undefined;
49
+ }
50
+ const format = String(zugferdV1Select(`string(${deliveryDatePath}/@format)`, this.doc)).trim();
51
+ return this.parseRequiredCIIDate(deliveryDate, format);
52
+ }
53
+
31
54
  /**
32
55
  * Decodes a ZUGFeRD v1 credit note
33
56
  * @returns Promise resolving to a TCreditNote object
@@ -37,7 +60,7 @@ export class ZUGFeRDV1Decoder extends CIIBaseDecoder {
37
60
  }
38
61
 
39
62
  /**
40
- * Decodes a ZUGFeRD v1 invoice document: an invoice, a debit note or a self-billed invoice
63
+ * Decodes a ZUGFeRD v1 invoice document: an invoice, a corrected invoice, a debit note or a self-billed invoice
41
64
  * @param accountingDocType The type the document type code stands for
42
65
  * @returns Promise resolving to the invoice document
43
66
  */
@@ -99,6 +122,9 @@ export class ZUGFeRDV1Decoder extends CIIBaseDecoder {
99
122
  notes = allNotes.slice(1); // Remove subject from notes
100
123
  }
101
124
 
125
+ // Extract the actual delivery date, if stated
126
+ const deliveryDate = this.extractDeliveryDate();
127
+
102
128
  // Check for reverse charge
103
129
  const reverseCharge = this.exists('//ram:SpecifiedTradeAllowanceCharge/ram:ReasonCode[text()="62"]');
104
130
 
@@ -126,7 +152,8 @@ export class ZUGFeRDV1Decoder extends CIIBaseDecoder {
126
152
  reverseCharge: reverseCharge,
127
153
  currency: currencyCode as finance.TCurrency,
128
154
  notes: notes,
129
- deliveryDate: issueDate,
155
+ // the day of supply as the document states it (BT-72), never the issue date in its place
156
+ ...(deliveryDate === undefined ? {} : { deliveryDate }),
130
157
  objectActions: []
131
158
  };
132
159
 
@@ -206,7 +233,8 @@ export class ZUGFeRDV1Decoder extends CIIBaseDecoder {
206
233
  const name = this.getText('ram:SpecifiedTradeProduct/ram:Name', itemNode);
207
234
  const articleNumber = this.getText('ram:SpecifiedTradeProduct/ram:SellerAssignedID', itemNode);
208
235
  const unitQuantity = this.getNumber('ram:SpecifiedLineTradeDelivery/ram:BilledQuantity', itemNode);
209
- const unitType = this.getText('ram:SpecifiedLineTradeDelivery/ram:BilledQuantity/@unitCode', itemNode) || 'EA';
236
+ // an absent unit code (BT-130) stays absent, so that BR-23 reports it
237
+ const unitType = this.getText('ram:SpecifiedLineTradeDelivery/ram:BilledQuantity/@unitCode', itemNode);
210
238
  const unitNetPrice = this.getNumber('ram:SpecifiedLineTradeAgreement/ram:NetPriceProductTradePrice/ram:ChargeAmount', itemNode);
211
239
  const vatPercentage = this.getNumber('ram:SpecifiedLineTradeSettlement/ram:ApplicableTradeTax/ram:RateApplicablePercent', itemNode);
212
240
 
@@ -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
  */
@@ -442,7 +442,7 @@ export class SemanticModelAdapter {
442
442
  identifier: (index + 1).toString(),
443
443
  note: (item as any).description || (item as any).text || '',
444
444
  invoicedQuantity: item.unitQuantity,
445
- invoicedQuantityUnitOfMeasureCode: item.unitType || 'C62',
445
+ invoicedQuantityUnitOfMeasureCode: item.unitType,
446
446
  lineExtensionAmount: item.unitNetPrice * item.unitQuantity,
447
447
  purchaseOrderLineReference: (item as any).purchaseOrderLineRef,
448
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;
@@ -3,6 +3,8 @@ import type { TAccountingDoc, TCreditNote, TInvoiceDocument } from '../../../int
3
3
  import { UBLDocumentType } from '../ubl.types.js';
4
4
  import { DOMParser, XMLSerializer } from '../../../plugins.js';
5
5
  import { getDocumentTypeCode } from '../../utils/document.typecode.js';
6
+ import { getPrecedingInvoiceReferences } from '../../utils/preceding.invoice.js';
7
+ import { getWritableDate } from '../../utils/date.value.js';
6
8
 
7
9
  /**
8
10
  * UBL Encoder implementation
@@ -24,13 +26,15 @@ export class UBLEncoder extends UBLBaseEncoder {
24
26
 
25
27
  // Add credit note specific data
26
28
  this.addCreditNoteSpecificData(doc, creditNote);
29
+
30
+ this.orderRootElements(doc);
27
31
 
28
32
  // Serialize to string
29
33
  return new XMLSerializer().serializeToString(doc);
30
34
  }
31
35
 
32
36
  /**
33
- * Encodes an invoice document (invoice, debit note or self-billed invoice) into UBL XML
37
+ * Encodes an invoice document (invoice, corrected invoice, debit note or self-billed invoice) into UBL XML
34
38
  * @param invoice Invoice document to encode
35
39
  * @returns UBL XML string
36
40
  */
@@ -44,6 +48,8 @@ export class UBLEncoder extends UBLBaseEncoder {
44
48
 
45
49
  // Add invoice specific data
46
50
  this.addInvoiceSpecificData(doc, invoice);
51
+
52
+ this.orderRootElements(doc);
47
53
 
48
54
  // Serialize to string
49
55
  return new XMLSerializer().serializeToString(doc);
@@ -71,16 +77,15 @@ export class UBLEncoder extends UBLBaseEncoder {
71
77
  this.appendElement(doc, root, 'cbc:ID', invoice.id);
72
78
 
73
79
  // Issue Date
74
- this.appendElement(doc, root, 'cbc:IssueDate', this.formatDate(invoice.date));
80
+ this.appendElement(doc, root, 'cbc:IssueDate', this.formatDate(invoice.date, 'BT-2 invoice issue date'));
75
81
 
76
82
  // Due Date - the CreditNote schema has no DueDate element; its payment due date lives in cac:PaymentMeans
77
83
  if (documentType === UBLDocumentType.INVOICE) {
78
- const issueTimestamp = typeof invoice.date === 'number' ? invoice.date : Date.now();
79
- const dueDate = new Date(issueTimestamp);
84
+ const dueDate = getWritableDate(invoice.date, 'BT-2 invoice issue date', 'ubl');
80
85
  dueDate.setDate(dueDate.getDate() + (invoice.dueInDays || 30));
81
- this.appendElement(doc, root, 'cbc:DueDate', this.formatDate(dueDate.getTime()));
86
+ this.appendElement(doc, root, 'cbc:DueDate', this.formatDate(dueDate.getTime(), 'BT-9 payment due date'));
82
87
  }
83
- // Document Type Code (380 invoice, 381 credit note, 383 debit note, 389 self-billed invoice; the element differs per schema)
88
+ // Document Type Code (380 invoice, 381 credit note, 383 debit note, 384 corrected invoice, 389 self-billed invoice; the element differs per schema)
84
89
  this.appendElement(
85
90
  doc,
86
91
  root,
@@ -104,12 +109,21 @@ export class UBLEncoder extends UBLBaseEncoder {
104
109
 
105
110
  // Document Currency Code
106
111
  this.appendElement(doc, root, 'cbc:DocumentCurrencyCode', invoice.currency);
112
+
113
+ // Invoicing period (BG-14), in schema order before the document references
114
+ this.addInvoicePeriod(doc, root, invoice);
115
+
116
+ // Preceding invoice references (BG-3)
117
+ this.addBillingReferences(doc, root, invoice);
107
118
 
108
119
  // Add accounting supplier party (seller)
109
120
  this.addParty(doc, root, 'cac:AccountingSupplierParty', invoice.from);
110
121
 
111
122
  // Add accounting customer party (buyer)
112
123
  this.addParty(doc, root, 'cac:AccountingCustomerParty', invoice.to);
124
+
125
+ // Actual delivery date (BT-72), in schema order after the parties
126
+ this.addDelivery(doc, root, invoice);
113
127
 
114
128
  // Add payment terms
115
129
  this.addPaymentTerms(doc, root, invoice);
@@ -127,6 +141,57 @@ export class UBLEncoder extends UBLBaseEncoder {
127
141
  this.preserveMetadata(doc, root, invoice);
128
142
  }
129
143
 
144
+ /**
145
+ * Writes the invoicing period (BG-14: BT-73 and BT-74) the document states.
146
+ * @param doc XML document
147
+ * @param root Document root
148
+ * @param invoice Accounting document
149
+ */
150
+ private addInvoicePeriod(doc: Document, root: Element, invoice: TAccountingDoc): void {
151
+ if (!invoice.periodOfPerformance) {
152
+ return;
153
+ }
154
+ const invoicePeriod = doc.createElement('cac:InvoicePeriod');
155
+ this.appendElement(doc, invoicePeriod, 'cbc:StartDate', this.formatDate(invoice.periodOfPerformance.from, 'BT-73 invoicing period start date'));
156
+ this.appendElement(doc, invoicePeriod, 'cbc:EndDate', this.formatDate(invoice.periodOfPerformance.to, 'BT-74 invoicing period end date'));
157
+ root.appendChild(invoicePeriod);
158
+ }
159
+
160
+ /**
161
+ * Writes one preceding invoice reference (BG-3) per invoice the document
162
+ * states it corrects: the number (BT-25) and, when stated, the issue date (BT-26).
163
+ * @param doc XML document
164
+ * @param root Document root
165
+ * @param invoice Accounting document
166
+ */
167
+ private addBillingReferences(doc: Document, root: Element, invoice: TAccountingDoc): void {
168
+ for (const reference of getPrecedingInvoiceReferences(invoice)) {
169
+ const billingReference = doc.createElement('cac:BillingReference');
170
+ const invoiceDocumentReference = doc.createElement('cac:InvoiceDocumentReference');
171
+ this.appendElement(doc, invoiceDocumentReference, 'cbc:ID', reference.documentId);
172
+ if (reference.issueDate !== undefined) {
173
+ this.appendElement(doc, invoiceDocumentReference, 'cbc:IssueDate', this.formatDate(reference.issueDate, 'BT-26 preceding invoice issue date'));
174
+ }
175
+ billingReference.appendChild(invoiceDocumentReference);
176
+ root.appendChild(billingReference);
177
+ }
178
+ }
179
+
180
+ /**
181
+ * Writes the actual delivery date (BT-72) the document states.
182
+ * @param doc XML document
183
+ * @param root Document root
184
+ * @param invoice Accounting document
185
+ */
186
+ private addDelivery(doc: Document, root: Element, invoice: TAccountingDoc): void {
187
+ if (invoice.deliveryDate === undefined) {
188
+ return;
189
+ }
190
+ const delivery = doc.createElement('cac:Delivery');
191
+ this.appendElement(doc, delivery, 'cbc:ActualDeliveryDate', this.formatDate(invoice.deliveryDate, 'BT-72 actual delivery date'));
192
+ root.appendChild(delivery);
193
+ }
194
+
130
195
  /**
131
196
  * Adds credit note specific data to the document
132
197
  * @param doc XML document
@@ -280,11 +345,10 @@ export class UBLEncoder extends UBLBaseEncoder {
280
345
  // Payment means code - default to credit transfer
281
346
  this.appendElement(doc, paymentMeansNode, 'cbc:PaymentMeansCode', '30');
282
347
 
283
- // Payment due date - ensure invoice.date is a valid timestamp
284
- const issueTimestamp = typeof invoice.date === 'number' ? invoice.date : Date.now();
285
- const dueDate = new Date(issueTimestamp);
348
+ // Payment due date, counted from the issue date the document states
349
+ const dueDate = getWritableDate(invoice.date, 'BT-2 invoice issue date', 'ubl');
286
350
  dueDate.setDate(dueDate.getDate() + (invoice.dueInDays || 30));
287
- this.appendElement(doc, paymentMeansNode, 'cbc:PaymentDueDate', this.formatDate(dueDate.getTime()));
351
+ this.appendElement(doc, paymentMeansNode, 'cbc:PaymentDueDate', this.formatDate(dueDate.getTime(), 'BT-9 payment due date'));
288
352
 
289
353
  // Add payment channel code if available
290
354
  if (paymentOptions.description) {
@@ -459,7 +523,10 @@ export class UBLEncoder extends UBLBaseEncoder {
459
523
 
460
524
  // Invoiced quantity
461
525
  const quantityElement = doc.createElement(creditNote ? 'cbc:CreditedQuantity' : 'cbc:InvoicedQuantity');
462
- quantityElement.setAttribute('unitCode', item.unitType);
526
+ // a line without a unit is written without one, so that BR-23 reports it
527
+ if (item.unitType) {
528
+ quantityElement.setAttribute('unitCode', item.unitType);
529
+ }
463
530
  quantityElement.textContent = item.unitQuantity.toString();
464
531
  invoiceLineNode.appendChild(quantityElement);
465
532
 
@@ -89,7 +89,7 @@ export abstract class UBLBaseDecoder extends BaseDecoder {
89
89
  protected abstract decodeCreditNote(): Promise<TCreditNote>;
90
90
 
91
91
  /**
92
- * Decodes a UBL invoice document: an invoice, a debit note or a self-billed invoice
92
+ * Decodes a UBL invoice document: an invoice, a corrected invoice, a debit note or a self-billed invoice
93
93
  * @param accountingDocType The type the document type code stands for
94
94
  * @returns Promise resolving to the invoice document
95
95
  */
@@ -1,6 +1,42 @@
1
1
  import { BaseEncoder } from '../base/base.encoder.js';
2
2
  import type { TAccountingDoc, TCreditNote, TInvoiceDocument } from '../../interfaces/common.js';
3
3
  import { UBLDocumentType, UBL_NAMESPACES } from './ubl.types.js';
4
+ import { getWritableDate } from '../utils/date.value.js';
5
+
6
+ /**
7
+ * The children of the document root in the order the UBL 2.1 schema requires
8
+ * them (UBL-Invoice-2.1.xsd and UBL-CreditNote-2.1.xsd, local names).
9
+ */
10
+ const UBL_ROOT_SEQUENCES: Record<UBLDocumentType, readonly string[]> = {
11
+ [UBLDocumentType.INVOICE]: [
12
+ 'UBLExtensions', 'UBLVersionID', 'CustomizationID', 'ProfileID', 'ProfileExecutionID', 'ID',
13
+ 'CopyIndicator', 'UUID', 'IssueDate', 'IssueTime', 'DueDate', 'InvoiceTypeCode', 'Note',
14
+ 'TaxPointDate', 'DocumentCurrencyCode', 'TaxCurrencyCode', 'PricingCurrencyCode',
15
+ 'PaymentCurrencyCode', 'PaymentAlternativeCurrencyCode', 'AccountingCostCode', 'AccountingCost',
16
+ 'LineCountNumeric', 'BuyerReference', 'InvoicePeriod', 'OrderReference', 'BillingReference',
17
+ 'DespatchDocumentReference', 'ReceiptDocumentReference', 'StatementDocumentReference',
18
+ 'OriginatorDocumentReference', 'ContractDocumentReference', 'AdditionalDocumentReference',
19
+ 'ProjectReference', 'Signature', 'AccountingSupplierParty', 'AccountingCustomerParty',
20
+ 'PayeeParty', 'BuyerCustomerParty', 'SellerSupplierParty', 'TaxRepresentativeParty', 'Delivery',
21
+ 'DeliveryTerms', 'PaymentMeans', 'PaymentTerms', 'PrepaidPayment', 'AllowanceCharge',
22
+ 'TaxExchangeRate', 'PricingExchangeRate', 'PaymentExchangeRate', 'PaymentAlternativeExchangeRate',
23
+ 'TaxTotal', 'WithholdingTaxTotal', 'LegalMonetaryTotal', 'InvoiceLine',
24
+ ],
25
+ [UBLDocumentType.CREDIT_NOTE]: [
26
+ 'UBLExtensions', 'UBLVersionID', 'CustomizationID', 'ProfileID', 'ProfileExecutionID', 'ID',
27
+ 'CopyIndicator', 'UUID', 'IssueDate', 'IssueTime', 'TaxPointDate', 'CreditNoteTypeCode', 'Note',
28
+ 'DocumentCurrencyCode', 'TaxCurrencyCode', 'PricingCurrencyCode', 'PaymentCurrencyCode',
29
+ 'PaymentAlternativeCurrencyCode', 'AccountingCostCode', 'AccountingCost', 'LineCountNumeric',
30
+ 'BuyerReference', 'InvoicePeriod', 'DiscrepancyResponse', 'OrderReference', 'BillingReference',
31
+ 'DespatchDocumentReference', 'ReceiptDocumentReference', 'ContractDocumentReference',
32
+ 'AdditionalDocumentReference', 'StatementDocumentReference', 'OriginatorDocumentReference',
33
+ 'Signature', 'AccountingSupplierParty', 'AccountingCustomerParty', 'PayeeParty',
34
+ 'BuyerCustomerParty', 'SellerSupplierParty', 'TaxRepresentativeParty', 'Delivery',
35
+ 'DeliveryTerms', 'PaymentMeans', 'PaymentTerms', 'TaxExchangeRate', 'PricingExchangeRate',
36
+ 'PaymentExchangeRate', 'PaymentAlternativeExchangeRate', 'AllowanceCharge', 'TaxTotal',
37
+ 'LegalMonetaryTotal', 'CreditNoteLine',
38
+ ],
39
+ };
4
40
 
5
41
  /**
6
42
  * Base encoder for UBL-based invoice formats
@@ -27,7 +63,7 @@ export abstract class UBLBaseEncoder extends BaseEncoder {
27
63
  protected abstract encodeCreditNote(creditNote: TCreditNote): Promise<string>;
28
64
 
29
65
  /**
30
- * Encodes an invoice document (invoice, debit note or self-billed invoice) into UBL XML
66
+ * Encodes an invoice document (invoice, corrected invoice, debit note or self-billed invoice) into UBL XML
31
67
  * @param invoice Invoice document to encode
32
68
  * @returns UBL XML string
33
69
  */
@@ -47,20 +83,48 @@ export abstract class UBLBaseEncoder extends BaseEncoder {
47
83
  }
48
84
 
49
85
  /**
50
- * Formats a date as an ISO string (YYYY-MM-DD)
51
- * @param timestamp Timestamp to format
52
- * @returns Formatted date string
86
+ * Puts the children of the document root into the order the schema requires.
87
+ * Elements are added in several passes (the generic encoder, a customization,
88
+ * preserved metadata), each inserting where it can; this final pass makes the
89
+ * result follow the schema sequence. The sort is stable, so repeated elements
90
+ * (notes, references, lines) keep their order, and an element the sequence
91
+ * does not name stays behind the element it followed.
92
+ * @param doc XML document
53
93
  */
54
- protected formatDate(timestamp: number): string {
55
- // Ensure timestamp is valid
56
- if (!timestamp || isNaN(timestamp)) {
57
- timestamp = Date.now();
94
+ protected orderRootElements(doc: Document): void {
95
+ const root = doc.documentElement;
96
+ const sequence = UBL_ROOT_SEQUENCES[root.localName as UBLDocumentType];
97
+ if (!sequence) {
98
+ return;
99
+ }
100
+ const children: Element[] = [];
101
+ for (let node = root.firstChild; node; node = node.nextSibling) {
102
+ if (node.nodeType === 1) {
103
+ children.push(node as Element);
104
+ }
58
105
  }
59
- const date = new Date(timestamp);
60
- // Check if date is valid
61
- if (isNaN(date.getTime())) {
62
- return new Date().toISOString().split('T')[0];
106
+ let lastRank = -1;
107
+ const ranked = children.map((element, position) => {
108
+ // an element made with createElement('cac:X') carries its prefix in localName as well
109
+ const rank = sequence.indexOf(element.nodeName.slice(element.nodeName.indexOf(':') + 1));
110
+ lastRank = rank === -1 ? lastRank : rank;
111
+ return { element, rank: lastRank, position };
112
+ });
113
+ ranked.sort((a, b) => a.rank - b.rank || a.position - b.position);
114
+ for (const { element } of ranked) {
115
+ root.appendChild(element);
63
116
  }
64
- return date.toISOString().split('T')[0];
117
+ }
118
+
119
+ /**
120
+ * Formats a date as an ISO string (YYYY-MM-DD, UTC). A value that is no
121
+ * valid timestamp is refused with an `EInvoiceFormatError` naming the field;
122
+ * no date is put in its place.
123
+ * @param timestamp Timestamp to format
124
+ * @param field The business term the date is written as
125
+ * @returns Formatted date string
126
+ */
127
+ protected formatDate(timestamp: number, field: string): string {
128
+ return getWritableDate(timestamp, field, 'ubl').toISOString().split('T')[0];
65
129
  }
66
130
  }
@@ -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