@fin.cx/einvoice 8.1.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 (61) 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 +17 -2
  4. package/dist_ts/formats/base/base.encoder.d.ts +1 -1
  5. package/dist_ts/formats/cii/cii.decoder.d.ts +12 -1
  6. package/dist_ts/formats/cii/cii.decoder.js +34 -1
  7. package/dist_ts/formats/cii/cii.encoder.d.ts +14 -3
  8. package/dist_ts/formats/cii/cii.encoder.js +58 -6
  9. package/dist_ts/formats/cii/cii.types.d.ts +1 -0
  10. package/dist_ts/formats/cii/cii.types.js +3 -2
  11. package/dist_ts/formats/cii/facturx/facturx.decoder.d.ts +1 -1
  12. package/dist_ts/formats/cii/facturx/facturx.decoder.js +9 -4
  13. package/dist_ts/formats/cii/facturx/facturx.encoder.d.ts +2 -2
  14. package/dist_ts/formats/cii/facturx/facturx.encoder.js +13 -10
  15. package/dist_ts/formats/cii/zugferd/zugferd.decoder.d.ts +1 -1
  16. package/dist_ts/formats/cii/zugferd/zugferd.decoder.js +9 -4
  17. package/dist_ts/formats/cii/zugferd/zugferd.encoder.d.ts +1 -1
  18. package/dist_ts/formats/cii/zugferd/zugferd.encoder.js +16 -13
  19. package/dist_ts/formats/cii/zugferd/zugferd.v1.decoder.d.ts +7 -1
  20. package/dist_ts/formats/cii/zugferd/zugferd.v1.decoder.js +27 -4
  21. package/dist_ts/formats/ubl/generic/ubl.encoder.d.ts +23 -1
  22. package/dist_ts/formats/ubl/generic/ubl.encoder.js +67 -11
  23. package/dist_ts/formats/ubl/ubl.decoder.d.ts +1 -1
  24. package/dist_ts/formats/ubl/ubl.encoder.d.ts +16 -3
  25. package/dist_ts/formats/ubl/ubl.encoder.js +75 -13
  26. package/dist_ts/formats/ubl/xrechnung/xrechnung.decoder.d.ts +6 -1
  27. package/dist_ts/formats/ubl/xrechnung/xrechnung.decoder.js +37 -7
  28. package/dist_ts/formats/ubl/xrechnung/xrechnung.encoder.d.ts +1 -1
  29. package/dist_ts/formats/ubl/xrechnung/xrechnung.encoder.js +4 -2
  30. package/dist_ts/formats/utils/date.value.d.ts +10 -0
  31. package/dist_ts/formats/utils/date.value.js +21 -0
  32. package/dist_ts/formats/utils/document.typecode.d.ts +3 -3
  33. package/dist_ts/formats/utils/document.typecode.js +8 -4
  34. package/dist_ts/formats/utils/preceding.invoice.d.ts +26 -0
  35. package/dist_ts/formats/utils/preceding.invoice.js +52 -0
  36. package/dist_ts/index.d.ts +1 -1
  37. package/dist_ts/index.js +1 -1
  38. package/dist_ts/interfaces/common.d.ts +6 -2
  39. package/package.json +3 -3
  40. package/readme.md +60 -10
  41. package/ts/00_commitinfo_data.ts +1 -1
  42. package/ts/einvoice.ts +18 -1
  43. package/ts/formats/base/base.encoder.ts +1 -1
  44. package/ts/formats/cii/cii.decoder.ts +43 -1
  45. package/ts/formats/cii/cii.encoder.ts +62 -6
  46. package/ts/formats/cii/cii.types.ts +2 -1
  47. package/ts/formats/cii/facturx/facturx.decoder.ts +9 -3
  48. package/ts/formats/cii/facturx/facturx.encoder.ts +13 -9
  49. package/ts/formats/cii/zugferd/zugferd.decoder.ts +9 -3
  50. package/ts/formats/cii/zugferd/zugferd.encoder.ts +16 -12
  51. package/ts/formats/cii/zugferd/zugferd.v1.decoder.ts +30 -3
  52. package/ts/formats/ubl/generic/ubl.encoder.ts +74 -10
  53. package/ts/formats/ubl/ubl.decoder.ts +1 -1
  54. package/ts/formats/ubl/ubl.encoder.ts +77 -13
  55. package/ts/formats/ubl/xrechnung/xrechnung.decoder.ts +45 -6
  56. package/ts/formats/ubl/xrechnung/xrechnung.encoder.ts +4 -1
  57. package/ts/formats/utils/date.value.ts +21 -0
  58. package/ts/formats/utils/document.typecode.ts +7 -3
  59. package/ts/formats/utils/preceding.invoice.ts +74 -0
  60. package/ts/index.ts +3 -0
  61. package/ts/interfaces/common.ts +6 -1
@@ -7,6 +7,7 @@ import type {
7
7
  } from '../../../interfaces/common.js';
8
8
  import { business, finance } from '../../../plugins.js';
9
9
  import { EN16931Validator } from '../../validation/en16931.validator.js';
10
+ import { getPrecedingInvoiceFields } from '../../utils/preceding.invoice.js';
10
11
 
11
12
  /**
12
13
  * Decoder for ZUGFeRD invoice format
@@ -21,7 +22,7 @@ export class ZUGFeRDDecoder extends CIIBaseDecoder {
21
22
  }
22
23
 
23
24
  /**
24
- * Decodes a ZUGFeRD invoice document: an invoice, a debit note or a self-billed invoice
25
+ * Decodes a ZUGFeRD invoice document: an invoice, a corrected invoice, a debit note or a self-billed invoice
25
26
  * @param accountingDocType The type the document type code stands for
26
27
  * @returns Promise resolving to the invoice document
27
28
  */
@@ -83,6 +84,9 @@ export class ZUGFeRDDecoder extends CIIBaseDecoder {
83
84
  notes = allNotes.slice(1); // Remove subject from notes
84
85
  }
85
86
 
87
+ // Extract the actual delivery date, if stated
88
+ const deliveryDate = this.extractDeliveryDate();
89
+
86
90
  // Check for reverse charge
87
91
  const reverseCharge = this.exists('//ram:SpecifiedTradeAllowanceCharge/ram:ReasonCode[text()="62"]');
88
92
 
@@ -110,8 +114,10 @@ export class ZUGFeRDDecoder extends CIIBaseDecoder {
110
114
  reverseCharge: reverseCharge,
111
115
  currency: currencyCode as finance.TCurrency,
112
116
  notes: notes,
113
- deliveryDate: issueDate,
114
- objectActions: []
117
+ // the day of supply as the document states it (BT-72)
118
+ ...(deliveryDate === undefined ? {} : { deliveryDate }),
119
+ objectActions: [],
120
+ ...getPrecedingInvoiceFields(accountingDocType, this.extractPrecedingInvoiceReferences()),
115
121
  };
116
122
 
117
123
  // Validate mandatory EN16931 fields unless validation is skipped
@@ -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
 
@@ -655,8 +659,8 @@ export class ZUGFeRDEncoder extends CIIBaseEncoder {
655
659
  * @param timestamp Timestamp to format
656
660
  * @returns Formatted date string
657
661
  */
658
- private formatDateYYYYMMDD(timestamp: number): string {
659
- const date = new Date(timestamp);
662
+ private formatDateYYYYMMDD(timestamp: number, field: string): string {
663
+ const date = getWritableDate(timestamp, field, 'cii');
660
664
  const year = date.getFullYear();
661
665
  const month = (date.getMonth() + 1).toString().padStart(2, '0');
662
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
 
@@ -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) {
@@ -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
  }
@@ -8,6 +8,10 @@ import type {
8
8
  import { business, finance } from '../../../plugins.js';
9
9
  import { UBLDocumentType } from '../ubl.types.js';
10
10
  import { EN16931Validator } from '../../validation/en16931.validator.js';
11
+ import {
12
+ getPrecedingInvoiceFields,
13
+ type TPrecedingInvoiceReference,
14
+ } from '../../utils/preceding.invoice.js';
11
15
 
12
16
  /**
13
17
  * Decoder for XRechnung (UBL) format
@@ -23,7 +27,7 @@ export class XRechnungDecoder extends UBLBaseDecoder {
23
27
  }
24
28
 
25
29
  /**
26
- * Decodes a UBL invoice document: an invoice, a debit note or a self-billed invoice
30
+ * Decodes a UBL invoice document: an invoice, a corrected invoice, a debit note or a self-billed invoice
27
31
  * @param accountingDocType The type the document type code stands for
28
32
  * @returns Promise resolving to the invoice document
29
33
  */
@@ -168,10 +172,11 @@ export class XRechnungDecoder extends UBLBaseDecoder {
168
172
  const paymentTermsNote = this.getText('//cac:PaymentTerms/cbc:Note', this.doc);
169
173
  const discountPercent = this.getText('//cac:PaymentTerms/cbc:SettlementDiscountPercent', this.doc);
170
174
 
171
- // Extract period information
172
- const periodStart = this.getText('//cac:InvoicePeriod/cbc:StartDate', this.doc);
173
- const periodEnd = this.getText('//cac:InvoicePeriod/cbc:EndDate', this.doc);
174
- const deliveryDate = this.getText('//cac:Delivery/cbc:ActualDeliveryDate', this.doc);
175
+ // Extract the document level period (BG-14) and delivery date (BT-72); a line has
176
+ // an invoice period (BG-26) and a delivery of its own, which are not the document's
177
+ const periodStart = this.getText('/*/cac:InvoicePeriod/cbc:StartDate', this.doc);
178
+ const periodEnd = this.getText('/*/cac:InvoicePeriod/cbc:EndDate', this.doc);
179
+ const deliveryDate = this.getText('/*/cac:Delivery/cbc:ActualDeliveryDate', this.doc);
175
180
 
176
181
  // Extract notes (excluding PaymentTerms notes)
177
182
  const allNotes: string[] = [];
@@ -255,7 +260,18 @@ export class XRechnungDecoder extends UBLBaseDecoder {
255
260
  reverseCharge: false,
256
261
  currency: currencyCode as finance.TCurrency,
257
262
  notes: notes,
258
- objectActions: []
263
+ objectActions: [],
264
+ // the day and the period of supply as the document states them (BT-72, BG-14)
265
+ ...(deliveryDate ? { deliveryDate: this.parseRequiredUblDate(deliveryDate) } : {}),
266
+ ...(periodStart && periodEnd
267
+ ? {
268
+ periodOfPerformance: {
269
+ from: this.parseRequiredUblDate(periodStart),
270
+ to: this.parseRequiredUblDate(periodEnd),
271
+ },
272
+ }
273
+ : {}),
274
+ ...getPrecedingInvoiceFields(accountingDocType, this.extractPrecedingInvoiceReferences()),
259
275
  };
260
276
 
261
277
  if (hasBusinessReferences || hasPaymentInformation || hasDateInformation) {
@@ -281,6 +297,29 @@ export class XRechnungDecoder extends UBLBaseDecoder {
281
297
  }
282
298
  }
283
299
 
300
+ /**
301
+ * Reads the preceding invoice references (BG-3): the number (BT-25) and,
302
+ * when stated, the issue date (BT-26) of each.
303
+ */
304
+ private extractPrecedingInvoiceReferences(): TPrecedingInvoiceReference[] {
305
+ const referenceNodes = this.select(
306
+ './cac:BillingReference/cac:InvoiceDocumentReference',
307
+ this.doc.documentElement,
308
+ );
309
+ const references: TPrecedingInvoiceReference[] = [];
310
+ for (const referenceNode of Array.isArray(referenceNodes) ? referenceNodes : []) {
311
+ const documentId = this.getText('./cbc:ID', referenceNode).trim();
312
+ if (!documentId) {
313
+ continue;
314
+ }
315
+ const issueDate = this.getText('./cbc:IssueDate', referenceNode).trim();
316
+ references.push(
317
+ issueDate ? { documentId, issueDate: this.parseRequiredUblDate(issueDate) } : { documentId },
318
+ );
319
+ }
320
+ return references;
321
+ }
322
+
284
323
  /**
285
324
  * Extracts party information from XML
286
325
  * @param partyPath XPath to the party element
@@ -25,7 +25,7 @@ export class XRechnungEncoder extends UBLEncoder {
25
25
  }
26
26
 
27
27
  /**
28
- * Encodes an invoice document (invoice, debit note or self-billed invoice) into XRechnung XML
28
+ * Encodes an invoice document (invoice, corrected invoice, debit note or self-billed invoice) into XRechnung XML
29
29
  * @param invoice Invoice document to encode
30
30
  * @returns XRechnung XML string
31
31
  */
@@ -114,6 +114,9 @@ export class XRechnungEncoder extends UBLEncoder {
114
114
 
115
115
  // Enhance line items with metadata
116
116
  this.enhanceLineItems(doc, invoice);
117
+
118
+ // the customizations above insert where they can; the schema order is restored last
119
+ this.orderRootElements(doc);
117
120
  }
118
121
 
119
122
  /**
@@ -0,0 +1,21 @@
1
+ import { EInvoiceFormatError } from '../../errors.js';
2
+
3
+ /**
4
+ * The date an encoder is about to write, as a `Date`. A value that is no
5
+ * finite timestamp, or lies outside the range of a JavaScript date, is refused
6
+ * with an `EInvoiceFormatError` naming the field: the encoders never write a
7
+ * date the document does not state, such as today in place of a missing one.
8
+ * @param timestamp The timestamp (milliseconds since the epoch) to write
9
+ * @param field The business term it is written as, for example `BT-2 invoice issue date`
10
+ * @param targetFormat The format being written
11
+ */
12
+ export const getWritableDate = (timestamp: unknown, field: string, targetFormat: string): Date => {
13
+ const date = typeof timestamp === 'number' && Number.isFinite(timestamp) ? new Date(timestamp) : undefined;
14
+ if (!date || Number.isNaN(date.getTime())) {
15
+ throw new EInvoiceFormatError(`${field} is no valid date: ${String(timestamp)}`, {
16
+ targetFormat,
17
+ unsupportedFeatures: [field],
18
+ });
19
+ }
20
+ return date;
21
+ };
@@ -7,6 +7,7 @@ import { EInvoiceFormatError } from '../../errors.js';
7
7
  */
8
8
  const DOCUMENT_TYPE_CODES: Record<TAccountingDocType, string> = {
9
9
  'invoice': '380',
10
+ 'corrected-invoice': '384',
10
11
  'creditnote': '381',
11
12
  'debitnote': '383',
12
13
  'self-billed-invoice': '389',
@@ -42,9 +43,9 @@ export const getDocumentTypeCode = (accountingDocType: TAccountingDocType): stri
42
43
  };
43
44
 
44
45
  /**
45
- * The accounting document type of a decoded type code. 383 and 389 have a
46
- * type of their own; the remaining credit note related codes are credit
47
- * notes; everything else is an invoice, which covers 380, 384 and the other
46
+ * The accounting document type of a decoded type code. 383, 384 and 389 have
47
+ * a type of their own; the remaining credit note related codes are credit
48
+ * notes; everything else is an invoice, which covers 380 and the other
48
49
  * invoice related codes (326, 386, 875, ...) the envelope has no name for.
49
50
  * A missing or unlisted code stays an invoice as well: the document is an
50
51
  * invoice document, and reporting the code is the validators' job (BR-04,
@@ -55,6 +56,9 @@ export const getAccountingDocType = (typeCode: string): TAccountingDocType => {
55
56
  if (code === DOCUMENT_TYPE_CODES['debitnote']) {
56
57
  return 'debitnote';
57
58
  }
59
+ if (code === DOCUMENT_TYPE_CODES['corrected-invoice']) {
60
+ return 'corrected-invoice';
61
+ }
58
62
  if (code === DOCUMENT_TYPE_CODES['self-billed-invoice']) {
59
63
  return 'self-billed-invoice';
60
64
  }