@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
@@ -4,12 +4,14 @@ import { CII_NAMESPACES, CIIProfile } from './cii.types.js';
4
4
  import { DOMParser, xpath } from '../../plugins.js';
5
5
  import { EInvoiceParsingError } from '../../errors.js';
6
6
  import { getAccountingDocType } from '../utils/document.typecode.js';
7
+ import type { TPrecedingInvoiceReference } from '../utils/preceding.invoice.js';
7
8
 
8
9
  const ciiParser = new DOMParser();
9
10
  const ciiNamespaces = {
10
11
  rsm: CII_NAMESPACES.RSM,
11
12
  ram: CII_NAMESPACES.RAM,
12
13
  udt: CII_NAMESPACES.UDT,
14
+ qdt: CII_NAMESPACES.QDT,
13
15
  };
14
16
  const ciiSelect = xpath.useNamespaces(ciiNamespaces);
15
17
 
@@ -91,7 +93,7 @@ export abstract class CIIBaseDecoder extends BaseDecoder {
91
93
  protected abstract decodeCreditNote(): Promise<TCreditNote>;
92
94
 
93
95
  /**
94
- * Decodes a CII invoice document: an invoice, a debit note or a self-billed invoice
96
+ * Decodes a CII invoice document: an invoice, a corrected invoice, a debit note or a self-billed invoice
95
97
  * @param accountingDocType The type the document type code stands for
96
98
  * @returns Promise resolving to the invoice document
97
99
  */
@@ -99,6 +101,77 @@ export abstract class CIIBaseDecoder extends BaseDecoder {
99
101
  accountingDocType: TInvoiceDocument['accountingDocType'],
100
102
  ): Promise<TInvoiceDocument>;
101
103
 
104
+ /**
105
+ * Reads the preceding invoice references (BG-3) of the header settlement:
106
+ * the number (BT-25) and, when stated, the issue date (BT-26) of each.
107
+ */
108
+ protected extractPrecedingInvoiceReferences(): TPrecedingInvoiceReference[] {
109
+ const referenceNodes = this.select(
110
+ '/rsm:CrossIndustryInvoice/rsm:SupplyChainTradeTransaction/ram:ApplicableHeaderTradeSettlement/ram:InvoiceReferencedDocument',
111
+ this.doc,
112
+ );
113
+ const references: TPrecedingInvoiceReference[] = [];
114
+ for (const referenceNode of Array.isArray(referenceNodes) ? referenceNodes : []) {
115
+ const documentId = this.getText('./ram:IssuerAssignedID', referenceNode as Node).trim();
116
+ if (!documentId) {
117
+ continue;
118
+ }
119
+ const issueDate = this.getText('./ram:FormattedIssueDateTime/qdt:DateTimeString', referenceNode as Node).trim();
120
+ const issueDateFormat = this.getText('./ram:FormattedIssueDateTime/qdt:DateTimeString/@format', referenceNode as Node).trim();
121
+ references.push(
122
+ issueDate
123
+ ? { documentId, issueDate: this.parseRequiredCIIDate(issueDate, issueDateFormat) }
124
+ : { documentId },
125
+ );
126
+ }
127
+ return references;
128
+ }
129
+
130
+ /**
131
+ * Reads the invoice issue date (BT-2); a missing or invalid one is refused
132
+ * with an `EInvoiceParsingError`, as no other date can stand in for it.
133
+ */
134
+ protected extractIssueDate(): number {
135
+ const issueDatePath = '/rsm:CrossIndustryInvoice/rsm:ExchangedDocument/ram:IssueDateTime/udt:DateTimeString';
136
+ return this.parseRequiredCIIDate(this.getText(issueDatePath), this.getText(`${issueDatePath}/@format`).trim());
137
+ }
138
+
139
+ /**
140
+ * Reads the payment due date (BT-9) of the header payment terms; undefined
141
+ * when the document states none.
142
+ */
143
+ protected extractDueDate(): number | undefined {
144
+ const dueDatePath =
145
+ '/rsm:CrossIndustryInvoice/rsm:SupplyChainTradeTransaction/ram:ApplicableHeaderTradeSettlement/ram:SpecifiedTradePaymentTerms/ram:DueDateDateTime/udt:DateTimeString';
146
+ const dueDate = this.getText(dueDatePath).trim();
147
+ if (!dueDate) {
148
+ return undefined;
149
+ }
150
+ return this.parseRequiredCIIDate(dueDate, this.getText(`${dueDatePath}/@format`).trim());
151
+ }
152
+
153
+ /**
154
+ * The payment term in days from the issue date to the stated due date, or
155
+ * `absentDueInDays` when the document states no due date.
156
+ */
157
+ protected dueInDaysOf(issueDate: number, dueDate: number | undefined, absentDueInDays: number): number {
158
+ return dueDate === undefined ? absentDueInDays : Math.round((dueDate - issueDate) / (1000 * 60 * 60 * 24));
159
+ }
160
+
161
+ /**
162
+ * Reads the actual delivery date (BT-72) of the header delivery; undefined
163
+ * when the document does not state one.
164
+ */
165
+ protected extractDeliveryDate(): number | undefined {
166
+ const deliveryDatePath =
167
+ '/rsm:CrossIndustryInvoice/rsm:SupplyChainTradeTransaction/ram:ApplicableHeaderTradeDelivery/ram:ActualDeliverySupplyChainEvent/ram:OccurrenceDateTime/udt:DateTimeString';
168
+ const deliveryDate = this.getText(deliveryDatePath).trim();
169
+ if (!deliveryDate) {
170
+ return undefined;
171
+ }
172
+ return this.parseRequiredCIIDate(deliveryDate, this.getText(`${deliveryDatePath}/@format`).trim());
173
+ }
174
+
102
175
  /**
103
176
  * Gets a text value from an XPath expression
104
177
  * @param xpath XPath expression
@@ -1,5 +1,8 @@
1
1
  import { BaseEncoder } from '../base/base.encoder.js';
2
2
  import type { TAccountingDoc, TCreditNote, TInvoiceDocument } from '../../interfaces/common.js';
3
+ import { EInvoiceFormatError } from '../../errors.js';
4
+ import { getPrecedingInvoiceReferences } from '../utils/preceding.invoice.js';
5
+ import { getWritableDate } from '../utils/date.value.js';
3
6
  import { CII_NAMESPACES, CIIProfile } from './cii.types.js';
4
7
 
5
8
  /**
@@ -22,6 +25,13 @@ export abstract class CIIBaseEncoder extends BaseEncoder {
22
25
  * @returns CII XML string
23
26
  */
24
27
  public async encode(invoice: TAccountingDoc): Promise<string> {
28
+ // MINIMUM has no preceding invoice reference (BG-3): a reference the document states cannot be written there
29
+ if (this.profile === CIIProfile.MINIMUM && getPrecedingInvoiceReferences(invoice).length > 0) {
30
+ throw new EInvoiceFormatError(
31
+ 'The MINIMUM profile has no preceding invoice reference (BG-3); a document that refers to a preceding invoice needs a profile that carries it',
32
+ { targetFormat: 'cii', unsupportedFeatures: ['BG-3 in profile MINIMUM'] },
33
+ );
34
+ }
25
35
  // CII has one root for every document; the two paths differ in the type code they write
26
36
  if (invoice.accountingDocType === 'creditnote') {
27
37
  return this.encodeCreditNote(invoice);
@@ -37,7 +47,7 @@ export abstract class CIIBaseEncoder extends BaseEncoder {
37
47
  protected abstract encodeCreditNote(creditNote: TCreditNote): Promise<string>;
38
48
 
39
49
  /**
40
- * Encodes an invoice document (invoice, debit note or self-billed invoice) into CII XML
50
+ * Encodes an invoice document (invoice, corrected invoice, debit note or self-billed invoice) into CII XML
41
51
  * @param invoice Invoice document to encode
42
52
  * @returns CII XML string
43
53
  */
@@ -51,17 +61,63 @@ export abstract class CIIBaseEncoder extends BaseEncoder {
51
61
  return `<?xml version="1.0" encoding="UTF-8"?>
52
62
  <rsm:CrossIndustryInvoice xmlns:rsm="${CII_NAMESPACES.RSM}"
53
63
  xmlns:ram="${CII_NAMESPACES.RAM}"
54
- xmlns:udt="${CII_NAMESPACES.UDT}">
64
+ xmlns:udt="${CII_NAMESPACES.UDT}"
65
+ xmlns:qdt="${CII_NAMESPACES.QDT}">
55
66
  </rsm:CrossIndustryInvoice>`;
56
67
  }
57
68
 
58
69
  /**
59
- * Formats a date as an ISO string (YYYY-MM-DD)
70
+ * Writes one preceding invoice reference (BG-3) per invoice the document
71
+ * states it corrects: the number (BT-25) and, when stated, the issue date
72
+ * (BT-26). In the header settlement they follow the monetary summation, as
73
+ * the CII schema orders them.
74
+ * @param doc XML document
75
+ * @param invoice Accounting document
76
+ */
77
+ protected addPrecedingInvoiceReferences(doc: Document, invoice: TAccountingDoc): void {
78
+ const references = getPrecedingInvoiceReferences(invoice);
79
+ if (references.length === 0) {
80
+ return;
81
+ }
82
+ const settlement = doc.getElementsByTagName('ram:ApplicableHeaderTradeSettlement')[0];
83
+ if (!settlement) {
84
+ throw new EInvoiceFormatError('The document has no header trade settlement to carry its preceding invoice reference', {
85
+ targetFormat: 'cii',
86
+ unsupportedFeatures: ['BG-3'],
87
+ });
88
+ }
89
+ let summation: Element | undefined;
90
+ for (let node = settlement.firstChild; node; node = node.nextSibling) {
91
+ if (node.nodeType === 1 && node.nodeName === 'ram:SpecifiedTradeSettlementHeaderMonetarySummation') {
92
+ summation = node as Element;
93
+ }
94
+ }
95
+ const anchor = summation ? summation.nextSibling : null;
96
+ for (const reference of references) {
97
+ const referencedDocument = doc.createElement('ram:InvoiceReferencedDocument');
98
+ const issuerAssignedId = doc.createElement('ram:IssuerAssignedID');
99
+ issuerAssignedId.textContent = reference.documentId;
100
+ referencedDocument.appendChild(issuerAssignedId);
101
+ if (reference.issueDate !== undefined) {
102
+ const formattedIssueDateTime = doc.createElement('ram:FormattedIssueDateTime');
103
+ const dateTimeString = doc.createElement('qdt:DateTimeString');
104
+ dateTimeString.setAttribute('format', '102');
105
+ dateTimeString.textContent = this.formatDate(reference.issueDate, 'BT-26 preceding invoice issue date').replace(/-/g, '');
106
+ formattedIssueDateTime.appendChild(dateTimeString);
107
+ referencedDocument.appendChild(formattedIssueDateTime);
108
+ }
109
+ settlement.insertBefore(referencedDocument, anchor);
110
+ }
111
+ }
112
+
113
+ /**
114
+ * Formats a date as an ISO string (YYYY-MM-DD, UTC). A value that is no
115
+ * valid timestamp is refused with an `EInvoiceFormatError` naming the field.
60
116
  * @param timestamp Timestamp to format
117
+ * @param field The business term the date is written as
61
118
  * @returns Formatted date string
62
119
  */
63
- protected formatDate(timestamp: number): string {
64
- const date = new Date(timestamp);
65
- return date.toISOString().split('T')[0];
120
+ protected formatDate(timestamp: number, field: string): string {
121
+ return getWritableDate(timestamp, field, 'cii').toISOString().split('T')[0];
66
122
  }
67
123
  }
@@ -6,7 +6,8 @@
6
6
  export const CII_NAMESPACES = {
7
7
  RSM: 'urn:un:unece:uncefact:data:standard:CrossIndustryInvoice:100',
8
8
  RAM: 'urn:un:unece:uncefact:data:standard:ReusableAggregateBusinessInformationEntity:100',
9
- UDT: 'urn:un:unece:uncefact:data:standard:UnqualifiedDataType:100'
9
+ UDT: 'urn:un:unece:uncefact:data:standard:UnqualifiedDataType:100',
10
+ QDT: 'urn:un:unece:uncefact:data:standard:QualifiedDataType:100'
10
11
  };
11
12
 
12
13
  // ZUGFeRD v1 namespaces
@@ -8,6 +8,7 @@ import type {
8
8
  import { FACTURX_PROFILE_IDS } from './facturx.types.js';
9
9
  import { business, finance, general } from '../../../plugins.js';
10
10
  import { EN16931Validator } from '../../validation/en16931.validator.js';
11
+ import { getPrecedingInvoiceFields } from '../../utils/preceding.invoice.js';
11
12
 
12
13
  /**
13
14
  * Decoder for Factur-X invoice format
@@ -22,7 +23,7 @@ export class FacturXDecoder extends CIIBaseDecoder {
22
23
  }
23
24
 
24
25
  /**
25
- * Decodes a Factur-X invoice document: an invoice, a debit note or a self-billed invoice
26
+ * Decodes a Factur-X invoice document: an invoice, a corrected invoice, a debit note or a self-billed invoice
26
27
  * @param accountingDocType The type the document type code stands for
27
28
  * @returns Promise resolving to the invoice document
28
29
  */
@@ -66,10 +67,8 @@ export class FacturXDecoder extends CIIBaseDecoder {
66
67
  const items = this.extractItems();
67
68
 
68
69
  // Extract due date
69
- const dueDateStr = this.getText('//ram:SpecifiedTradePaymentTerms/ram:DueDateDateTime/udt:DateTimeString');
70
- const dueDateFormat = this.getText('//ram:SpecifiedTradePaymentTerms/ram:DueDateDateTime/udt:DateTimeString/@format');
71
- const dueDate = dueDateStr ? this.parseCIIDate(dueDateStr, dueDateFormat) : issueDate;
72
- const dueInDays = Math.round((dueDate - issueDate) / (1000 * 60 * 60 * 24));
70
+ // no due date stated: `dueInDays` cannot express it yet (required field in @tsclass/tsclass); value unchanged: 0 days
71
+ const dueInDays = this.dueInDaysOf(issueDate, this.extractDueDate(), 0);
73
72
 
74
73
  // Extract currency
75
74
  const currencyCode = this.getText('//ram:InvoiceCurrencyCode') || 'EUR';
@@ -92,6 +91,9 @@ export class FacturXDecoder extends CIIBaseDecoder {
92
91
  notes = allNotes.slice(1); // Remove subject from notes
93
92
  }
94
93
 
94
+ // Extract the actual delivery date, if stated
95
+ const deliveryDate = this.extractDeliveryDate();
96
+
95
97
  // Check for reverse charge
96
98
  const reverseCharge = this.exists('//ram:SpecifiedTradeAllowanceCharge/ram:ReasonCode[text()="62"]');
97
99
 
@@ -119,8 +121,10 @@ export class FacturXDecoder extends CIIBaseDecoder {
119
121
  reverseCharge: reverseCharge,
120
122
  currency: currencyCode as finance.TCurrency,
121
123
  notes: notes,
122
- deliveryDate: issueDate,
123
- objectActions: []
124
+ // the day of supply as the document states it (BT-72)
125
+ ...(deliveryDate === undefined ? {} : { deliveryDate }),
126
+ objectActions: [],
127
+ ...getPrecedingInvoiceFields(accountingDocType, this.extractPrecedingInvoiceReferences()),
124
128
  };
125
129
 
126
130
  // Validate mandatory EN16931 fields unless validation is skipped
@@ -6,6 +6,7 @@ import { Decimal } from '../../utils/decimal.js';
6
6
  import { DecimalCurrencyCalculator } from '../../utils/currency.calculator.decimal.js';
7
7
  import { getCurrencyMinorUnits } from '../../utils/currency.utils.js';
8
8
  import { getDocumentTypeCode } from '../../utils/document.typecode.js';
9
+ import { getWritableDate, getWritableDueDate } from '../../utils/date.value.js';
9
10
 
10
11
  /**
11
12
  * Encoder for Factur-X invoice format
@@ -31,7 +32,7 @@ export class FacturXEncoder extends CIIBaseEncoder {
31
32
  }
32
33
 
33
34
  /**
34
- * Encodes an invoice document (invoice, debit note or self-billed invoice) into Factur-X XML
35
+ * Encodes an invoice document (invoice, corrected invoice, debit note or self-billed invoice) into Factur-X XML
35
36
  * @param invoice Invoice document to encode
36
37
  * @returns Factur-X XML string
37
38
  */
@@ -39,7 +40,7 @@ export class FacturXEncoder extends CIIBaseEncoder {
39
40
  // Create base XML
40
41
  const xmlDoc = this.createBaseXml();
41
42
 
42
- // Set document type code (380 invoice, 383 debit note, 389 self-billed invoice)
43
+ // Set document type code (380 invoice, 383 debit note, 384 corrected invoice, 389 self-billed invoice)
43
44
  this.setDocumentTypeCode(xmlDoc, getDocumentTypeCode(invoice.accountingDocType));
44
45
 
45
46
  // Add common invoice data
@@ -101,7 +102,7 @@ export class FacturXEncoder extends CIIBaseEncoder {
101
102
  /**
102
103
  * Sets the document type code in the XML document
103
104
  * @param doc XML document
104
- * @param typeCode Document type code (380 invoice, 381 credit note, 383 debit note, 389 self-billed invoice)
105
+ * @param typeCode Document type code (380 invoice, 381 credit note, 383 debit note, 384 corrected invoice, 389 self-billed invoice)
105
106
  */
106
107
  private setDocumentTypeCode(doc: Document, typeCode: string): void {
107
108
  // Get root element
@@ -145,7 +146,7 @@ export class FacturXEncoder extends CIIBaseEncoder {
145
146
  const issueDateElement = doc.createElement('ram:IssueDateTime');
146
147
  const dateStringElement = doc.createElement('udt:DateTimeString');
147
148
  dateStringElement.setAttribute('format', '102'); // YYYYMMDD format
148
- dateStringElement.textContent = this.formatDateYYYYMMDD(invoice.date);
149
+ dateStringElement.textContent = this.formatDateYYYYMMDD(invoice.date, 'BT-2 invoice issue date');
149
150
  issueDateElement.appendChild(dateStringElement);
150
151
  documentElement.appendChild(issueDateElement);
151
152
 
@@ -183,6 +184,9 @@ export class FacturXEncoder extends CIIBaseEncoder {
183
184
  // Add settlement section with payment terms and totals
184
185
  this.addSettlementSection(doc, transactionElement, invoice);
185
186
 
187
+ // Add the preceding invoice references (BG-3) the document states
188
+ this.addPrecedingInvoiceReferences(doc, invoice);
189
+
186
190
  // Add line items
187
191
  this.addLineItems(doc, transactionElement, invoice);
188
192
  }
@@ -284,12 +288,12 @@ export class FacturXEncoder extends CIIBaseEncoder {
284
288
  transactionElement.appendChild(deliveryElement);
285
289
 
286
290
  // Add delivery date if available
287
- if (invoice.deliveryDate) {
291
+ if (invoice.deliveryDate !== undefined) {
288
292
  const deliveryDateElement = doc.createElement('ram:ActualDeliverySupplyChainEvent');
289
293
  const occurrenceDateElement = doc.createElement('ram:OccurrenceDateTime');
290
294
  const dateStringElement = doc.createElement('udt:DateTimeString');
291
295
  dateStringElement.setAttribute('format', '102'); // YYYYMMDD format
292
- dateStringElement.textContent = this.formatDateYYYYMMDD(invoice.deliveryDate);
296
+ dateStringElement.textContent = this.formatDateYYYYMMDD(invoice.deliveryDate, 'BT-72 actual delivery date');
293
297
  occurrenceDateElement.appendChild(dateStringElement);
294
298
  deliveryDateElement.appendChild(occurrenceDateElement);
295
299
  deliveryElement.appendChild(deliveryDateElement);
@@ -321,10 +325,9 @@ export class FacturXEncoder extends CIIBaseEncoder {
321
325
  dateStringElement.setAttribute('format', '102'); // YYYYMMDD format
322
326
 
323
327
  // Calculate due date
324
- const dueDate = new Date(invoice.date);
325
- dueDate.setDate(dueDate.getDate() + invoice.dueInDays);
328
+ const dueDate = getWritableDueDate(invoice.date, invoice.dueInDays, 'cii');
326
329
 
327
- dateStringElement.textContent = this.formatDateYYYYMMDD(dueDate.getTime());
330
+ dateStringElement.textContent = this.formatDateYYYYMMDD(dueDate.getTime(), 'BT-9 payment due date');
328
331
  dueDateElement.appendChild(dateStringElement);
329
332
  paymentTermsElement.appendChild(dueDateElement);
330
333
 
@@ -487,11 +490,12 @@ export class FacturXEncoder extends CIIBaseEncoder {
487
490
  * @param timestamp Timestamp to format
488
491
  * @returns Formatted date string
489
492
  */
490
- private formatDateYYYYMMDD(timestamp: number): string {
491
- const date = new Date(timestamp);
492
- const year = date.getFullYear();
493
- const month = (date.getMonth() + 1).toString().padStart(2, '0');
494
- const day = date.getDate().toString().padStart(2, '0');
493
+ private formatDateYYYYMMDD(timestamp: number, field: string): string {
494
+ const date = getWritableDate(timestamp, field, 'cii');
495
+ // the UTC fields: a calendar day is its UTC midnight, whatever the server's zone
496
+ const year = date.getUTCFullYear();
497
+ const month = (date.getUTCMonth() + 1).toString().padStart(2, '0');
498
+ const day = date.getUTCDate().toString().padStart(2, '0');
495
499
  return `${year}${month}${day}`;
496
500
  }
497
501
  }
@@ -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
  */
@@ -43,9 +44,7 @@ export class ZUGFeRDDecoder extends CIIBaseDecoder {
43
44
  const invoiceId = this.getText('//rsm:ExchangedDocument/ram:ID');
44
45
 
45
46
  // Extract issue date
46
- const issueDateStr = this.getText('//ram:IssueDateTime/udt:DateTimeString');
47
- const issueDateFormat = this.getText('//ram:IssueDateTime/udt:DateTimeString/@format');
48
- const issueDate = this.parseCIIDate(issueDateStr, issueDateFormat);
47
+ const issueDate = this.extractIssueDate();
49
48
 
50
49
  // Extract seller information
51
50
  const seller = this.extractParty('//ram:SellerTradeParty');
@@ -57,9 +56,8 @@ export class ZUGFeRDDecoder extends CIIBaseDecoder {
57
56
  const items = this.extractItems();
58
57
 
59
58
  // Extract due date
60
- const dueDateStr = this.getText('//ram:SpecifiedTradePaymentTerms/ram:DueDateDateTime/udt:DateTimeString');
61
- const dueDate = dueDateStr ? new Date(dueDateStr).getTime() : Date.now();
62
- const dueInDays = Math.round((dueDate - issueDate) / (1000 * 60 * 60 * 24));
59
+ // no due date stated: `dueInDays` cannot express it yet (required field in @tsclass/tsclass); value unchanged: the days from the issue date to the moment of decoding
60
+ const dueInDays = this.dueInDaysOf(issueDate, this.extractDueDate(), Math.round((Date.now() - issueDate) / (1000 * 60 * 60 * 24)));
63
61
 
64
62
  // Extract currency
65
63
  const currencyCode = this.getText('//ram:InvoiceCurrencyCode') || 'EUR';
@@ -83,6 +81,9 @@ export class ZUGFeRDDecoder extends CIIBaseDecoder {
83
81
  notes = allNotes.slice(1); // Remove subject from notes
84
82
  }
85
83
 
84
+ // Extract the actual delivery date, if stated
85
+ const deliveryDate = this.extractDeliveryDate();
86
+
86
87
  // Check for reverse charge
87
88
  const reverseCharge = this.exists('//ram:SpecifiedTradeAllowanceCharge/ram:ReasonCode[text()="62"]');
88
89
 
@@ -110,8 +111,10 @@ export class ZUGFeRDDecoder extends CIIBaseDecoder {
110
111
  reverseCharge: reverseCharge,
111
112
  currency: currencyCode as finance.TCurrency,
112
113
  notes: notes,
113
- deliveryDate: issueDate,
114
- objectActions: []
114
+ // the day of supply as the document states it (BT-72)
115
+ ...(deliveryDate === undefined ? {} : { deliveryDate }),
116
+ objectActions: [],
117
+ ...getPrecedingInvoiceFields(accountingDocType, this.extractPrecedingInvoiceReferences()),
115
118
  };
116
119
 
117
120
  // Validate mandatory EN16931 fields unless validation is skipped
@@ -4,6 +4,8 @@ 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, getWritableDueDate } from '../../utils/date.value.js';
8
+ import { getPaymentTermsNote } from '../../utils/payment.terms.js';
7
9
 
8
10
  /**
9
11
  * Encoder for ZUGFeRD invoice format
@@ -35,7 +37,7 @@ export class ZUGFeRDEncoder extends CIIBaseEncoder {
35
37
  }
36
38
 
37
39
  /**
38
- * Encodes an invoice document (invoice, debit note or self-billed invoice) into ZUGFeRD XML
40
+ * Encodes an invoice document (invoice, corrected invoice, debit note or self-billed invoice) into ZUGFeRD XML
39
41
  * @param invoice Invoice document to encode
40
42
  * @returns ZUGFeRD XML string
41
43
  */
@@ -43,7 +45,7 @@ export class ZUGFeRDEncoder extends CIIBaseEncoder {
43
45
  // Create base XML
44
46
  const xmlDoc = this.createBaseXml();
45
47
 
46
- // Set document type code (380 invoice, 383 debit note, 389 self-billed invoice)
48
+ // Set document type code (380 invoice, 383 debit note, 384 corrected invoice, 389 self-billed invoice)
47
49
  this.setDocumentTypeCode(xmlDoc, getDocumentTypeCode(invoice.accountingDocType));
48
50
 
49
51
  // Add common invoice data
@@ -149,7 +151,7 @@ export class ZUGFeRDEncoder extends CIIBaseEncoder {
149
151
  const issueDateElement = doc.createElement('ram:IssueDateTime');
150
152
  const dateStringElement = doc.createElement('udt:DateTimeString');
151
153
  dateStringElement.setAttribute('format', '102'); // YYYYMMDD format
152
- dateStringElement.textContent = this.formatDateYYYYMMDD(invoice.date);
154
+ dateStringElement.textContent = this.formatDateYYYYMMDD(invoice.date, 'BT-2 invoice issue date');
153
155
  issueDateElement.appendChild(dateStringElement);
154
156
  documentElement.appendChild(issueDateElement);
155
157
 
@@ -187,6 +189,9 @@ export class ZUGFeRDEncoder extends CIIBaseEncoder {
187
189
  // Add settlement section with payment terms and totals
188
190
  this.addSettlementSection(doc, transactionElement, invoice);
189
191
 
192
+ // Add the preceding invoice references (BG-3) the document states
193
+ this.addPrecedingInvoiceReferences(doc, invoice);
194
+
190
195
  // Add line items
191
196
  this.addLineItems(doc, transactionElement, invoice);
192
197
  }
@@ -316,12 +321,12 @@ export class ZUGFeRDEncoder extends CIIBaseEncoder {
316
321
  transactionElement.appendChild(deliveryElement);
317
322
 
318
323
  // Add delivery date if available
319
- if (invoice.deliveryDate) {
324
+ if (invoice.deliveryDate !== undefined) {
320
325
  const deliveryDateElement = doc.createElement('ram:ActualDeliverySupplyChainEvent');
321
326
  const occurrenceDateElement = doc.createElement('ram:OccurrenceDateTime');
322
327
  const dateStringElement = doc.createElement('udt:DateTimeString');
323
328
  dateStringElement.setAttribute('format', '102'); // YYYYMMDD format
324
- dateStringElement.textContent = this.formatDateYYYYMMDD(invoice.deliveryDate);
329
+ dateStringElement.textContent = this.formatDateYYYYMMDD(invoice.deliveryDate, 'BT-72 actual delivery date');
325
330
  occurrenceDateElement.appendChild(dateStringElement);
326
331
  deliveryDateElement.appendChild(occurrenceDateElement);
327
332
  deliveryElement.appendChild(deliveryDateElement);
@@ -332,21 +337,21 @@ export class ZUGFeRDEncoder extends CIIBaseEncoder {
332
337
  const periodElement = doc.createElement('ram:BillingSpecifiedPeriod');
333
338
 
334
339
  // Start date
335
- if (invoice.periodOfPerformance.from) {
340
+ if (invoice.periodOfPerformance.from !== undefined) {
336
341
  const startDateElement = doc.createElement('ram:StartDateTime');
337
342
  const startDateStringElement = doc.createElement('udt:DateTimeString');
338
343
  startDateStringElement.setAttribute('format', '102'); // YYYYMMDD format
339
- startDateStringElement.textContent = this.formatDateYYYYMMDD(invoice.periodOfPerformance.from);
344
+ startDateStringElement.textContent = this.formatDateYYYYMMDD(invoice.periodOfPerformance.from, 'BT-73 invoicing period start date');
340
345
  startDateElement.appendChild(startDateStringElement);
341
346
  periodElement.appendChild(startDateElement);
342
347
  }
343
348
 
344
349
  // End date
345
- if (invoice.periodOfPerformance.to) {
350
+ if (invoice.periodOfPerformance.to !== undefined) {
346
351
  const endDateElement = doc.createElement('ram:EndDateTime');
347
352
  const endDateStringElement = doc.createElement('udt:DateTimeString');
348
353
  endDateStringElement.setAttribute('format', '102'); // YYYYMMDD format
349
- endDateStringElement.textContent = this.formatDateYYYYMMDD(invoice.periodOfPerformance.to);
354
+ endDateStringElement.textContent = this.formatDateYYYYMMDD(invoice.periodOfPerformance.to, 'BT-74 invoicing period end date');
350
355
  endDateElement.appendChild(endDateStringElement);
351
356
  periodElement.appendChild(endDateElement);
352
357
  }
@@ -378,7 +383,10 @@ export class ZUGFeRDEncoder extends CIIBaseEncoder {
378
383
  if (invoice.paymentOptions) {
379
384
  // Add payment instructions as description - this is generic enough to work with any payment type
380
385
  const descriptionElement = doc.createElement('ram:Description');
381
- descriptionElement.textContent = `Due in ${invoice.dueInDays} days. ${invoice.paymentOptions.description || ''}`;
386
+ descriptionElement.textContent = [
387
+ getPaymentTermsNote(invoice.date, invoice.dueInDays, invoice.language, 'cii'),
388
+ invoice.paymentOptions.description,
389
+ ].filter(Boolean).join('. ');
382
390
  paymentTermsElement.appendChild(descriptionElement);
383
391
  }
384
392
 
@@ -388,10 +396,9 @@ export class ZUGFeRDEncoder extends CIIBaseEncoder {
388
396
  dateStringElement.setAttribute('format', '102'); // YYYYMMDD format
389
397
 
390
398
  // Calculate due date
391
- const dueDate = new Date(invoice.date);
392
- dueDate.setDate(dueDate.getDate() + invoice.dueInDays);
399
+ const dueDate = getWritableDueDate(invoice.date, invoice.dueInDays, 'cii');
393
400
 
394
- dateStringElement.textContent = this.formatDateYYYYMMDD(dueDate.getTime());
401
+ dateStringElement.textContent = this.formatDateYYYYMMDD(dueDate.getTime(), 'BT-9 payment due date');
395
402
  dueDateElement.appendChild(dateStringElement);
396
403
  paymentTermsElement.appendChild(dueDateElement);
397
404
 
@@ -655,11 +662,12 @@ export class ZUGFeRDEncoder extends CIIBaseEncoder {
655
662
  * @param timestamp Timestamp to format
656
663
  * @returns Formatted date string
657
664
  */
658
- private formatDateYYYYMMDD(timestamp: number): string {
659
- const date = new Date(timestamp);
660
- const year = date.getFullYear();
661
- const month = (date.getMonth() + 1).toString().padStart(2, '0');
662
- const day = date.getDate().toString().padStart(2, '0');
665
+ private formatDateYYYYMMDD(timestamp: number, field: string): string {
666
+ const date = getWritableDate(timestamp, field, 'cii');
667
+ // the UTC fields: a calendar day is its UTC midnight, whatever the server's zone
668
+ const year = date.getUTCFullYear();
669
+ const month = (date.getUTCMonth() + 1).toString().padStart(2, '0');
670
+ const day = date.getUTCDate().toString().padStart(2, '0');
663
671
  return `${year}${month}${day}`;
664
672
  }
665
673
  }