@fin.cx/einvoice 8.1.0 → 8.2.1

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 (75) 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.decoder.js +4 -3
  5. package/dist_ts/formats/base/base.encoder.d.ts +1 -1
  6. package/dist_ts/formats/cii/cii.decoder.d.ts +27 -1
  7. package/dist_ts/formats/cii/cii.decoder.js +61 -1
  8. package/dist_ts/formats/cii/cii.encoder.d.ts +14 -3
  9. package/dist_ts/formats/cii/cii.encoder.js +58 -6
  10. package/dist_ts/formats/cii/cii.types.d.ts +1 -0
  11. package/dist_ts/formats/cii/cii.types.js +3 -2
  12. package/dist_ts/formats/cii/facturx/facturx.decoder.d.ts +1 -1
  13. package/dist_ts/formats/cii/facturx/facturx.decoder.js +11 -8
  14. package/dist_ts/formats/cii/facturx/facturx.encoder.d.ts +2 -2
  15. package/dist_ts/formats/cii/facturx/facturx.encoder.js +18 -15
  16. package/dist_ts/formats/cii/zugferd/zugferd.decoder.d.ts +1 -1
  17. package/dist_ts/formats/cii/zugferd/zugferd.decoder.js +12 -10
  18. package/dist_ts/formats/cii/zugferd/zugferd.encoder.d.ts +1 -1
  19. package/dist_ts/formats/cii/zugferd/zugferd.encoder.js +26 -19
  20. package/dist_ts/formats/cii/zugferd/zugferd.v1.decoder.d.ts +19 -1
  21. package/dist_ts/formats/cii/zugferd/zugferd.v1.decoder.js +51 -10
  22. package/dist_ts/formats/ubl/generic/ubl.encoder.d.ts +23 -1
  23. package/dist_ts/formats/ubl/generic/ubl.encoder.js +69 -14
  24. package/dist_ts/formats/ubl/ubl.decoder.d.ts +1 -1
  25. package/dist_ts/formats/ubl/ubl.encoder.d.ts +16 -3
  26. package/dist_ts/formats/ubl/ubl.encoder.js +75 -13
  27. package/dist_ts/formats/ubl/ubl.types.d.ts +6 -0
  28. package/dist_ts/formats/ubl/ubl.types.js +9 -3
  29. package/dist_ts/formats/ubl/xrechnung/xrechnung.decoder.d.ts +6 -1
  30. package/dist_ts/formats/ubl/xrechnung/xrechnung.decoder.js +46 -14
  31. package/dist_ts/formats/ubl/xrechnung/xrechnung.encoder.d.ts +1 -1
  32. package/dist_ts/formats/ubl/xrechnung/xrechnung.encoder.js +7 -12
  33. package/dist_ts/formats/utils/date.value.d.ts +28 -0
  34. package/dist_ts/formats/utils/date.value.js +50 -0
  35. package/dist_ts/formats/utils/document.typecode.d.ts +3 -3
  36. package/dist_ts/formats/utils/document.typecode.js +8 -4
  37. package/dist_ts/formats/utils/format.detector.js +6 -4
  38. package/dist_ts/formats/utils/payment.terms.d.ts +18 -0
  39. package/dist_ts/formats/utils/payment.terms.js +32 -0
  40. package/dist_ts/formats/utils/preceding.invoice.d.ts +26 -0
  41. package/dist_ts/formats/utils/preceding.invoice.js +52 -0
  42. package/dist_ts/formats/validation/integrated.validator.js +6 -3
  43. package/dist_ts/formats/validation/xrechnung.validator.js +6 -4
  44. package/dist_ts/index.d.ts +1 -1
  45. package/dist_ts/index.js +1 -1
  46. package/dist_ts/interfaces/common.d.ts +6 -2
  47. package/package.json +3 -3
  48. package/readme.md +77 -10
  49. package/ts/00_commitinfo_data.ts +1 -1
  50. package/ts/einvoice.ts +18 -1
  51. package/ts/formats/base/base.decoder.ts +3 -2
  52. package/ts/formats/base/base.encoder.ts +1 -1
  53. package/ts/formats/cii/cii.decoder.ts +74 -1
  54. package/ts/formats/cii/cii.encoder.ts +62 -6
  55. package/ts/formats/cii/cii.types.ts +2 -1
  56. package/ts/formats/cii/facturx/facturx.decoder.ts +11 -7
  57. package/ts/formats/cii/facturx/facturx.encoder.ts +18 -14
  58. package/ts/formats/cii/zugferd/zugferd.decoder.ts +12 -9
  59. package/ts/formats/cii/zugferd/zugferd.encoder.ts +26 -18
  60. package/ts/formats/cii/zugferd/zugferd.v1.decoder.ts +58 -9
  61. package/ts/formats/ubl/generic/ubl.encoder.ts +76 -13
  62. package/ts/formats/ubl/ubl.decoder.ts +1 -1
  63. package/ts/formats/ubl/ubl.encoder.ts +77 -13
  64. package/ts/formats/ubl/ubl.types.ts +8 -2
  65. package/ts/formats/ubl/xrechnung/xrechnung.decoder.ts +56 -13
  66. package/ts/formats/ubl/xrechnung/xrechnung.encoder.ts +7 -12
  67. package/ts/formats/utils/date.value.ts +52 -0
  68. package/ts/formats/utils/document.typecode.ts +7 -3
  69. package/ts/formats/utils/format.detector.ts +5 -5
  70. package/ts/formats/utils/payment.terms.ts +37 -0
  71. package/ts/formats/utils/preceding.invoice.ts +74 -0
  72. package/ts/formats/validation/integrated.validator.ts +5 -2
  73. package/ts/formats/validation/xrechnung.validator.ts +5 -3
  74. package/ts/index.ts +3 -0
  75. package/ts/interfaces/common.ts +6 -1
@@ -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,47 @@ export class ZUGFeRDV1Decoder extends CIIBaseDecoder {
28
35
  };
29
36
  }
30
37
 
38
+ /**
39
+ * Reads the issue date of the v1 header (`rsm:HeaderExchangedDocument`); a
40
+ * missing or invalid one is refused with an `EInvoiceParsingError`.
41
+ */
42
+ protected override extractIssueDate(): number {
43
+ const issueDatePath = '/rsm:CrossIndustryDocument/rsm:HeaderExchangedDocument/ram:IssueDateTime/udt:DateTimeString';
44
+ return this.parseRequiredCIIDate(this.v1Text(issueDatePath), this.v1Text(`${issueDatePath}/@format`));
45
+ }
46
+
47
+ /**
48
+ * Reads the payment due date of the v1 header payment terms; undefined when
49
+ * the document states none.
50
+ */
51
+ protected override extractDueDate(): number | undefined {
52
+ const dueDatePath =
53
+ '/rsm:CrossIndustryDocument/rsm:SpecifiedSupplyChainTradeTransaction/ram:ApplicableSupplyChainTradeSettlement/ram:SpecifiedTradePaymentTerms/ram:DueDateDateTime/udt:DateTimeString';
54
+ const dueDate = this.v1Text(dueDatePath);
55
+ return dueDate ? this.parseRequiredCIIDate(dueDate, this.v1Text(`${dueDatePath}/@format`)) : undefined;
56
+ }
57
+
58
+ /** The trimmed text of the first node a v1 path selects, or '' */
59
+ private v1Text(path: string): string {
60
+ return String(zugferdV1Select(`string(${path})`, this.doc)).trim();
61
+ }
62
+
63
+ /**
64
+ * Reads the actual delivery date of the v1 header delivery
65
+ * (`ram:ApplicableSupplyChainTradeDelivery`); undefined when the document
66
+ * does not state one.
67
+ */
68
+ protected override extractDeliveryDate(): number | undefined {
69
+ const deliveryDatePath =
70
+ '/rsm:CrossIndustryDocument/rsm:SpecifiedSupplyChainTradeTransaction/ram:ApplicableSupplyChainTradeDelivery/ram:ActualDeliverySupplyChainEvent/ram:OccurrenceDateTime/udt:DateTimeString';
71
+ const deliveryDate = String(zugferdV1Select(`string(${deliveryDatePath})`, this.doc)).trim();
72
+ if (!deliveryDate) {
73
+ return undefined;
74
+ }
75
+ const format = String(zugferdV1Select(`string(${deliveryDatePath}/@format)`, this.doc)).trim();
76
+ return this.parseRequiredCIIDate(deliveryDate, format);
77
+ }
78
+
31
79
  /**
32
80
  * Decodes a ZUGFeRD v1 credit note
33
81
  * @returns Promise resolving to a TCreditNote object
@@ -37,7 +85,7 @@ export class ZUGFeRDV1Decoder extends CIIBaseDecoder {
37
85
  }
38
86
 
39
87
  /**
40
- * Decodes a ZUGFeRD v1 invoice document: an invoice, a debit note or a self-billed invoice
88
+ * Decodes a ZUGFeRD v1 invoice document: an invoice, a corrected invoice, a debit note or a self-billed invoice
41
89
  * @param accountingDocType The type the document type code stands for
42
90
  * @returns Promise resolving to the invoice document
43
91
  */
@@ -59,9 +107,7 @@ export class ZUGFeRDV1Decoder extends CIIBaseDecoder {
59
107
  const invoiceId = this.getText('//ram:ID');
60
108
 
61
109
  // Extract issue date
62
- const issueDateStr = this.getText('//ram:IssueDateTime/udt:DateTimeString');
63
- const issueDateFormat = this.getText('//ram:IssueDateTime/udt:DateTimeString/@format');
64
- const issueDate = this.parseCIIDate(issueDateStr, issueDateFormat);
110
+ const issueDate = this.extractIssueDate();
65
111
 
66
112
  // Extract seller information
67
113
  const seller = this.extractParty('//ram:SellerTradeParty');
@@ -73,9 +119,8 @@ export class ZUGFeRDV1Decoder extends CIIBaseDecoder {
73
119
  const items = this.extractItems();
74
120
 
75
121
  // Extract due date
76
- const dueDateStr = this.getText('//ram:SpecifiedTradePaymentTerms/ram:DueDateDateTime/udt:DateTimeString');
77
- const dueDate = dueDateStr ? new Date(dueDateStr).getTime() : Date.now();
78
- const dueInDays = Math.round((dueDate - issueDate) / (1000 * 60 * 60 * 24));
122
+ // no due date stated: `dueInDays` cannot express it yet (required field in @tsclass/tsclass); value unchanged: 0 days, what this decoder returned before it read the v1 dates
123
+ const dueInDays = this.dueInDaysOf(issueDate, this.extractDueDate(), 0);
79
124
 
80
125
  // Extract currency
81
126
  const currencyCode = this.getText('//ram:InvoiceCurrencyCode') || 'EUR';
@@ -99,6 +144,9 @@ export class ZUGFeRDV1Decoder extends CIIBaseDecoder {
99
144
  notes = allNotes.slice(1); // Remove subject from notes
100
145
  }
101
146
 
147
+ // Extract the actual delivery date, if stated
148
+ const deliveryDate = this.extractDeliveryDate();
149
+
102
150
  // Check for reverse charge
103
151
  const reverseCharge = this.exists('//ram:SpecifiedTradeAllowanceCharge/ram:ReasonCode[text()="62"]');
104
152
 
@@ -126,7 +174,8 @@ export class ZUGFeRDV1Decoder extends CIIBaseDecoder {
126
174
  reverseCharge: reverseCharge,
127
175
  currency: currencyCode as finance.TCurrency,
128
176
  notes: notes,
129
- deliveryDate: issueDate,
177
+ // the day of supply as the document states it (BT-72), never the issue date in its place
178
+ ...(deliveryDate === undefined ? {} : { deliveryDate }),
130
179
  objectActions: []
131
180
  };
132
181
 
@@ -3,6 +3,9 @@ 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 { getWritableDueDate } from '../../utils/date.value.js';
8
+ import { getPaymentTermsNote } from '../../utils/payment.terms.js';
6
9
 
7
10
  /**
8
11
  * UBL Encoder implementation
@@ -24,13 +27,15 @@ export class UBLEncoder extends UBLBaseEncoder {
24
27
 
25
28
  // Add credit note specific data
26
29
  this.addCreditNoteSpecificData(doc, creditNote);
30
+
31
+ this.orderRootElements(doc);
27
32
 
28
33
  // Serialize to string
29
34
  return new XMLSerializer().serializeToString(doc);
30
35
  }
31
36
 
32
37
  /**
33
- * Encodes an invoice document (invoice, debit note or self-billed invoice) into UBL XML
38
+ * Encodes an invoice document (invoice, corrected invoice, debit note or self-billed invoice) into UBL XML
34
39
  * @param invoice Invoice document to encode
35
40
  * @returns UBL XML string
36
41
  */
@@ -44,6 +49,8 @@ export class UBLEncoder extends UBLBaseEncoder {
44
49
 
45
50
  // Add invoice specific data
46
51
  this.addInvoiceSpecificData(doc, invoice);
52
+
53
+ this.orderRootElements(doc);
47
54
 
48
55
  // Serialize to string
49
56
  return new XMLSerializer().serializeToString(doc);
@@ -71,16 +78,14 @@ export class UBLEncoder extends UBLBaseEncoder {
71
78
  this.appendElement(doc, root, 'cbc:ID', invoice.id);
72
79
 
73
80
  // Issue Date
74
- this.appendElement(doc, root, 'cbc:IssueDate', this.formatDate(invoice.date));
81
+ this.appendElement(doc, root, 'cbc:IssueDate', this.formatDate(invoice.date, 'BT-2 invoice issue date'));
75
82
 
76
83
  // Due Date - the CreditNote schema has no DueDate element; its payment due date lives in cac:PaymentMeans
77
84
  if (documentType === UBLDocumentType.INVOICE) {
78
- const issueTimestamp = typeof invoice.date === 'number' ? invoice.date : Date.now();
79
- const dueDate = new Date(issueTimestamp);
80
- dueDate.setDate(dueDate.getDate() + (invoice.dueInDays || 30));
81
- this.appendElement(doc, root, 'cbc:DueDate', this.formatDate(dueDate.getTime()));
85
+ const dueDate = getWritableDueDate(invoice.date, invoice.dueInDays, 'ubl');
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
@@ -254,7 +319,7 @@ export class UBLEncoder extends UBLBaseEncoder {
254
319
  parentElement.appendChild(paymentTermsNode);
255
320
 
256
321
  // Payment terms note
257
- this.appendElement(doc, paymentTermsNode, 'cbc:Note', `Due in ${invoice.dueInDays} days`);
322
+ this.appendElement(doc, paymentTermsNode, 'cbc:Note', getPaymentTermsNote(invoice.date, invoice.dueInDays, invoice.language, 'ubl'));
258
323
 
259
324
  // Add payment means if available
260
325
  if (invoice.paymentOptions) {
@@ -280,11 +345,9 @@ 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);
286
- dueDate.setDate(dueDate.getDate() + (invoice.dueInDays || 30));
287
- this.appendElement(doc, paymentMeansNode, 'cbc:PaymentDueDate', this.formatDate(dueDate.getTime()));
348
+ // Payment due date, counted from the issue date the document states
349
+ const dueDate = getWritableDueDate(invoice.date, invoice.dueInDays, 'ubl');
350
+ this.appendElement(doc, paymentMeansNode, 'cbc:PaymentDueDate', this.formatDate(dueDate.getTime(), 'BT-9 payment due date'));
288
351
 
289
352
  // Add payment channel code if available
290
353
  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
  }
@@ -17,8 +17,14 @@ export enum UBLDocumentType {
17
17
  CREDIT_NOTE = 'CreditNote'
18
18
  }
19
19
 
20
- // UBL customization IDs for different formats
20
+ // UBL customization IDs (specification identifier, BT-24) for different formats
21
21
  export const UBL_CUSTOMIZATION_IDS = {
22
- XRECHNUNG: 'urn:cen.eu:en16931:2017#compliant#urn:xoev-de:kosit:standard:xrechnung_2.0',
22
+ /**
23
+ * XRechnung 3.0 (CIUS), as the KoSIT XRechnung Schematron defines it (`XR-CIUS-ID` in
24
+ * common.sch: 'urn:cen.eu:en16931:2017#compliant#urn:xeinkauf.de:kosit:xrechnung_' and
25
+ * the version '3.0'). XRechnung 1.x and 2.x used the prefix
26
+ * 'urn:cen.eu:en16931:2017#compliant#urn:xoev-de:kosit:standard:xrechnung_'.
27
+ */
28
+ XRECHNUNG: 'urn:cen.eu:en16931:2017#compliant#urn:xeinkauf.de:kosit:xrechnung_3.0',
23
29
  PEPPOL_BIS: 'urn:cen.eu:en16931:2017#compliant#urn:fdc:peppol.eu:2017:poacc:billing:3.0'
24
30
  };
@@ -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
  */
@@ -56,14 +60,18 @@ export class XRechnungDecoder extends UBLBaseDecoder {
56
60
  const issueDate = this.parseRequiredUblDate(issueDateText);
57
61
  const currencyCode = this.getText('//cbc:DocumentCurrencyCode', this.doc) || 'EUR';
58
62
 
59
- // Extract payment terms
60
- let dueInDays = 30; // Default
61
- const dueDateText = this.getText('//cac:PaymentTerms/cbc:PaymentDueDate', this.doc);
63
+ // The payment due date (BT-9): cbc:DueDate on an Invoice, cac:PaymentMeans/cbc:PaymentDueDate
64
+ // on a CreditNote, cac:PaymentTerms/cbc:PaymentDueDate as a last resort; the payment term in
65
+ // days is the calendar days from the issue date to it.
66
+ // no due date stated: `dueInDays` cannot express it yet (required field in @tsclass/tsclass); value unchanged: 30 days
67
+ let dueInDays = 30;
68
+ const dueDateText = (
69
+ this.getText('/*/cbc:DueDate', this.doc) ||
70
+ this.getText('/*/cac:PaymentMeans/cbc:PaymentDueDate', this.doc) ||
71
+ this.getText('/*/cac:PaymentTerms/cbc:PaymentDueDate', this.doc)
72
+ ).trim();
62
73
  if (dueDateText) {
63
- const dueDateObj = new Date(dueDateText);
64
- const issueDateObj = new Date(issueDate);
65
- const diffTime = Math.abs(dueDateObj.getTime() - issueDateObj.getTime());
66
- dueInDays = Math.ceil(diffTime / (1000 * 60 * 60 * 24));
74
+ dueInDays = Math.round((this.parseRequiredUblDate(dueDateText) - issueDate) / (1000 * 60 * 60 * 24));
67
75
  }
68
76
 
69
77
  // Extract items
@@ -168,10 +176,11 @@ export class XRechnungDecoder extends UBLBaseDecoder {
168
176
  const paymentTermsNote = this.getText('//cac:PaymentTerms/cbc:Note', this.doc);
169
177
  const discountPercent = this.getText('//cac:PaymentTerms/cbc:SettlementDiscountPercent', this.doc);
170
178
 
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);
179
+ // Extract the document level period (BG-14) and delivery date (BT-72); a line has
180
+ // an invoice period (BG-26) and a delivery of its own, which are not the document's
181
+ const periodStart = this.getText('/*/cac:InvoicePeriod/cbc:StartDate', this.doc);
182
+ const periodEnd = this.getText('/*/cac:InvoicePeriod/cbc:EndDate', this.doc);
183
+ const deliveryDate = this.getText('/*/cac:Delivery/cbc:ActualDeliveryDate', this.doc);
175
184
 
176
185
  // Extract notes (excluding PaymentTerms notes)
177
186
  const allNotes: string[] = [];
@@ -255,7 +264,18 @@ export class XRechnungDecoder extends UBLBaseDecoder {
255
264
  reverseCharge: false,
256
265
  currency: currencyCode as finance.TCurrency,
257
266
  notes: notes,
258
- objectActions: []
267
+ objectActions: [],
268
+ // the day and the period of supply as the document states them (BT-72, BG-14)
269
+ ...(deliveryDate ? { deliveryDate: this.parseRequiredUblDate(deliveryDate) } : {}),
270
+ ...(periodStart && periodEnd
271
+ ? {
272
+ periodOfPerformance: {
273
+ from: this.parseRequiredUblDate(periodStart),
274
+ to: this.parseRequiredUblDate(periodEnd),
275
+ },
276
+ }
277
+ : {}),
278
+ ...getPrecedingInvoiceFields(accountingDocType, this.extractPrecedingInvoiceReferences()),
259
279
  };
260
280
 
261
281
  if (hasBusinessReferences || hasPaymentInformation || hasDateInformation) {
@@ -281,6 +301,29 @@ export class XRechnungDecoder extends UBLBaseDecoder {
281
301
  }
282
302
  }
283
303
 
304
+ /**
305
+ * Reads the preceding invoice references (BG-3): the number (BT-25) and,
306
+ * when stated, the issue date (BT-26) of each.
307
+ */
308
+ private extractPrecedingInvoiceReferences(): TPrecedingInvoiceReference[] {
309
+ const referenceNodes = this.select(
310
+ './cac:BillingReference/cac:InvoiceDocumentReference',
311
+ this.doc.documentElement,
312
+ );
313
+ const references: TPrecedingInvoiceReference[] = [];
314
+ for (const referenceNode of Array.isArray(referenceNodes) ? referenceNodes : []) {
315
+ const documentId = this.getText('./cbc:ID', referenceNode).trim();
316
+ if (!documentId) {
317
+ continue;
318
+ }
319
+ const issueDate = this.getText('./cbc:IssueDate', referenceNode).trim();
320
+ references.push(
321
+ issueDate ? { documentId, issueDate: this.parseRequiredUblDate(issueDate) } : { documentId },
322
+ );
323
+ }
324
+ return references;
325
+ }
326
+
284
327
  /**
285
328
  * Extracts party information from XML
286
329
  * @param partyPath XPath to the party element
@@ -1,6 +1,7 @@
1
1
  import { UBLEncoder } from '../generic/ubl.encoder.js';
2
2
  import type { TAccountingDoc, TCreditNote, TInvoiceDocument } from '../../../interfaces/common.js';
3
3
  import { DOMParser, XMLSerializer } from '../../../plugins.js';
4
+ import { UBL_CUSTOMIZATION_IDS } from '../ubl.types.js';
4
5
 
5
6
  /**
6
7
  * Encoder for XRechnung (UBL) format
@@ -25,7 +26,7 @@ export class XRechnungEncoder extends UBLEncoder {
25
26
  }
26
27
 
27
28
  /**
28
- * Encodes an invoice document (invoice, debit note or self-billed invoice) into XRechnung XML
29
+ * Encodes an invoice document (invoice, corrected invoice, debit note or self-billed invoice) into XRechnung XML
29
30
  * @param invoice Invoice document to encode
30
31
  * @returns XRechnung XML string
31
32
  */
@@ -52,10 +53,10 @@ export class XRechnungEncoder extends UBLEncoder {
52
53
  // Extract metadata if available
53
54
  const metadata = (invoice as any).metadata?.extensions;
54
55
 
55
- // Update Customization ID to XRechnung 2.0
56
+ // Specification identifier (BT-24): XRechnung 3.0
56
57
  const customizationId = root.getElementsByTagName('cbc:CustomizationID')[0];
57
58
  if (customizationId) {
58
- customizationId.textContent = 'urn:cen.eu:en16931:2017#compliant#urn:xoev-de:kosit:standard:xrechnung_2.0';
59
+ customizationId.textContent = UBL_CUSTOMIZATION_IDS.XRECHNUNG;
59
60
  }
60
61
 
61
62
  // Add or update Buyer Reference (required for XRechnung)
@@ -73,15 +74,6 @@ export class XRechnungEncoder extends UBLEncoder {
73
74
  buyerRef.textContent = buyerReferenceValue;
74
75
  }
75
76
 
76
- // Update payment terms to German
77
- const paymentTermsNotes = root.getElementsByTagName('cac:PaymentTerms');
78
- if (paymentTermsNotes.length > 0) {
79
- const noteElement = paymentTermsNotes[0].getElementsByTagName('cbc:Note')[0];
80
- if (noteElement && noteElement.textContent) {
81
- noteElement.textContent = `Zahlung innerhalb von ${invoice.dueInDays || 30} Tagen`;
82
- }
83
- }
84
-
85
77
  // Add electronic address for parties if available
86
78
  this.addElectronicAddressToParty(doc, 'cac:AccountingSupplierParty', invoice.from);
87
79
  this.addElectronicAddressToParty(doc, 'cac:AccountingCustomerParty', invoice.to);
@@ -114,6 +106,9 @@ export class XRechnungEncoder extends UBLEncoder {
114
106
 
115
107
  // Enhance line items with metadata
116
108
  this.enhanceLineItems(doc, invoice);
109
+
110
+ // the customizations above insert where they can; the schema order is restored last
111
+ this.orderRootElements(doc);
117
112
  }
118
113
 
119
114
  /**
@@ -0,0 +1,52 @@
1
+ import { EInvoiceFormatError } from '../../errors.js';
2
+
3
+ /**
4
+ * Calendar dates (BT-2, BT-7, BT-9, BT-26, BT-72, BT-73, BT-74) carry no time
5
+ * of day and no time zone: UBL writes them as `xsd:date`, CII in format 102
6
+ * (`YYYYMMDD`). In the accounting document envelope such a day is the UTC
7
+ * midnight of that day, which is what the decoders return. The encoders write
8
+ * it with the UTC fields of the date and count days in UTC, so the day
9
+ * written does not depend on the time zone of the machine that writes it.
10
+ */
11
+
12
+ /**
13
+ * The date an encoder is about to write, as a `Date`. A value that is no
14
+ * finite timestamp, or lies outside the range of a JavaScript date, is refused
15
+ * with an `EInvoiceFormatError` naming the field: the encoders never write a
16
+ * date the document does not state, such as today in place of a missing one.
17
+ * @param timestamp The timestamp (milliseconds since the epoch) to write
18
+ * @param field The business term it is written as, for example `BT-2 invoice issue date`
19
+ * @param targetFormat The format being written
20
+ */
21
+ export const getWritableDate = (timestamp: unknown, field: string, targetFormat: string): Date => {
22
+ const date = typeof timestamp === 'number' && Number.isFinite(timestamp) ? new Date(timestamp) : undefined;
23
+ if (!date || Number.isNaN(date.getTime())) {
24
+ throw new EInvoiceFormatError(`${field} is no valid date: ${String(timestamp)}`, {
25
+ targetFormat,
26
+ unsupportedFeatures: [field],
27
+ });
28
+ }
29
+ return date;
30
+ };
31
+
32
+ /**
33
+ * The payment due date (BT-9) a document states: its issue date plus
34
+ * `dueInDays` calendar days. `0` is due on the issue date. A `dueInDays` that
35
+ * is no whole number of days is refused with an `EInvoiceFormatError`; no other
36
+ * term is put in its place.
37
+ * @param issueTimestamp The issue date (BT-2)
38
+ * @param dueInDays The payment term in days, as the document states it
39
+ * @param targetFormat The format being written
40
+ */
41
+ export const getWritableDueDate = (issueTimestamp: unknown, dueInDays: unknown, targetFormat: string): Date => {
42
+ const dueDate = getWritableDate(issueTimestamp, 'BT-2 invoice issue date', targetFormat);
43
+ if (typeof dueInDays !== 'number' || !Number.isInteger(dueInDays)) {
44
+ throw new EInvoiceFormatError(`BT-9 payment due date: dueInDays is no whole number of days: ${String(dueInDays)}`, {
45
+ targetFormat,
46
+ unsupportedFeatures: ['BT-9 payment due date'],
47
+ });
48
+ }
49
+ // calendar days counted in UTC: a calendar day is its UTC midnight, whatever the server's zone
50
+ dueDate.setUTCDate(dueDate.getUTCDate() + dueInDays);
51
+ return dueDate;
52
+ };