@fin.cx/einvoice 8.0.0 → 8.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (93) hide show
  1. package/dist_ts/00_commitinfo_data.js +1 -1
  2. package/dist_ts/einvoice.d.ts +14 -1
  3. package/dist_ts/einvoice.js +48 -3
  4. package/dist_ts/formats/base/base.encoder.d.ts +1 -1
  5. package/dist_ts/formats/base/base.validator.d.ts +5 -0
  6. package/dist_ts/formats/base/base.validator.js +28 -1
  7. package/dist_ts/formats/cii/cii.decoder.d.ts +12 -1
  8. package/dist_ts/formats/cii/cii.decoder.js +34 -1
  9. package/dist_ts/formats/cii/cii.encoder.d.ts +14 -3
  10. package/dist_ts/formats/cii/cii.encoder.js +58 -6
  11. package/dist_ts/formats/cii/cii.types.d.ts +1 -0
  12. package/dist_ts/formats/cii/cii.types.js +3 -2
  13. package/dist_ts/formats/cii/cii.validator.d.ts +11 -0
  14. package/dist_ts/formats/cii/cii.validator.js +35 -1
  15. package/dist_ts/formats/cii/facturx/facturx.decoder.d.ts +1 -1
  16. package/dist_ts/formats/cii/facturx/facturx.decoder.js +11 -5
  17. package/dist_ts/formats/cii/facturx/facturx.encoder.d.ts +2 -2
  18. package/dist_ts/formats/cii/facturx/facturx.encoder.js +17 -11
  19. package/dist_ts/formats/cii/facturx/facturx.validator.js +8 -4
  20. package/dist_ts/formats/cii/zugferd/zugferd.decoder.d.ts +1 -1
  21. package/dist_ts/formats/cii/zugferd/zugferd.decoder.js +11 -5
  22. package/dist_ts/formats/cii/zugferd/zugferd.encoder.d.ts +1 -1
  23. package/dist_ts/formats/cii/zugferd/zugferd.encoder.js +20 -14
  24. package/dist_ts/formats/cii/zugferd/zugferd.v1.decoder.d.ts +7 -1
  25. package/dist_ts/formats/cii/zugferd/zugferd.v1.decoder.js +29 -5
  26. package/dist_ts/formats/cii/zugferd/zugferd.validator.js +5 -4
  27. package/dist_ts/formats/converters/xml-to-einvoice.converter.d.ts +5 -0
  28. package/dist_ts/formats/converters/xml-to-einvoice.converter.js +11 -2
  29. package/dist_ts/formats/semantic/semantic.adapter.js +2 -2
  30. package/dist_ts/formats/ubl/en16931.ubl.validator.js +5 -1
  31. package/dist_ts/formats/ubl/generic/ubl.encoder.d.ts +23 -1
  32. package/dist_ts/formats/ubl/generic/ubl.encoder.js +71 -12
  33. package/dist_ts/formats/ubl/ubl.decoder.d.ts +1 -1
  34. package/dist_ts/formats/ubl/ubl.encoder.d.ts +16 -3
  35. package/dist_ts/formats/ubl/ubl.encoder.js +75 -13
  36. package/dist_ts/formats/ubl/ubl.validator.d.ts +11 -0
  37. package/dist_ts/formats/ubl/ubl.validator.js +35 -1
  38. package/dist_ts/formats/ubl/xrechnung/xrechnung.decoder.d.ts +6 -1
  39. package/dist_ts/formats/ubl/xrechnung/xrechnung.decoder.js +39 -8
  40. package/dist_ts/formats/ubl/xrechnung/xrechnung.encoder.d.ts +1 -1
  41. package/dist_ts/formats/ubl/xrechnung/xrechnung.encoder.js +4 -2
  42. package/dist_ts/formats/utils/date.value.d.ts +10 -0
  43. package/dist_ts/formats/utils/date.value.js +21 -0
  44. package/dist_ts/formats/utils/document.typecode.d.ts +3 -3
  45. package/dist_ts/formats/utils/document.typecode.js +8 -4
  46. package/dist_ts/formats/utils/preceding.invoice.d.ts +26 -0
  47. package/dist_ts/formats/utils/preceding.invoice.js +52 -0
  48. package/dist_ts/formats/utils/unit.codes.d.ts +40 -0
  49. package/dist_ts/formats/utils/unit.codes.js +193 -0
  50. package/dist_ts/formats/validation/codelist.validator.d.ts +1 -1
  51. package/dist_ts/formats/validation/codelist.validator.js +7 -9
  52. package/dist_ts/formats/validation/en16931.business-rules.validator.js +13 -13
  53. package/dist_ts/formats/validation/validation.types.d.ts +0 -4
  54. package/dist_ts/formats/validation/validation.types.js +1 -28
  55. package/dist_ts/index.d.ts +5 -1
  56. package/dist_ts/index.js +6 -1
  57. package/dist_ts/interfaces/common.d.ts +6 -2
  58. package/dist_ts_install/download-schematron.js +3 -5
  59. package/package.json +8 -8
  60. package/readme.md +103 -10
  61. package/ts/00_commitinfo_data.ts +1 -1
  62. package/ts/einvoice.ts +51 -2
  63. package/ts/formats/base/base.encoder.ts +1 -1
  64. package/ts/formats/base/base.validator.ts +28 -0
  65. package/ts/formats/cii/cii.decoder.ts +43 -1
  66. package/ts/formats/cii/cii.encoder.ts +62 -6
  67. package/ts/formats/cii/cii.types.ts +2 -1
  68. package/ts/formats/cii/cii.validator.ts +47 -0
  69. package/ts/formats/cii/facturx/facturx.decoder.ts +11 -4
  70. package/ts/formats/cii/facturx/facturx.encoder.ts +17 -10
  71. package/ts/formats/cii/facturx/facturx.validator.ts +9 -3
  72. package/ts/formats/cii/zugferd/zugferd.decoder.ts +11 -4
  73. package/ts/formats/cii/zugferd/zugferd.encoder.ts +20 -13
  74. package/ts/formats/cii/zugferd/zugferd.v1.decoder.ts +32 -4
  75. package/ts/formats/cii/zugferd/zugferd.validator.ts +4 -3
  76. package/ts/formats/converters/xml-to-einvoice.converter.ts +11 -1
  77. package/ts/formats/semantic/semantic.adapter.ts +1 -1
  78. package/ts/formats/ubl/en16931.ubl.validator.ts +6 -0
  79. package/ts/formats/ubl/generic/ubl.encoder.ts +78 -11
  80. package/ts/formats/ubl/ubl.decoder.ts +1 -1
  81. package/ts/formats/ubl/ubl.encoder.ts +77 -13
  82. package/ts/formats/ubl/ubl.validator.ts +47 -0
  83. package/ts/formats/ubl/xrechnung/xrechnung.decoder.ts +47 -7
  84. package/ts/formats/ubl/xrechnung/xrechnung.encoder.ts +4 -1
  85. package/ts/formats/utils/date.value.ts +21 -0
  86. package/ts/formats/utils/document.typecode.ts +7 -3
  87. package/ts/formats/utils/preceding.invoice.ts +74 -0
  88. package/ts/formats/utils/unit.codes.ts +215 -0
  89. package/ts/formats/validation/codelist.validator.ts +8 -19
  90. package/ts/formats/validation/en16931.business-rules.validator.ts +13 -13
  91. package/ts/formats/validation/validation.types.ts +0 -28
  92. package/ts/index.ts +12 -0
  93. package/ts/interfaces/common.ts +6 -1
package/ts/einvoice.ts CHANGED
@@ -32,6 +32,7 @@ import { ValidatorFactory } from './formats/factories/validator.factory.js';
32
32
  // Import format detector
33
33
  import { FormatDetector } from './formats/utils/format.detector.js';
34
34
  import { isAccountingDocType } from './formats/utils/document.typecode.js';
35
+ import { meetsUnitCodeRule } from './formats/utils/unit.codes.js';
35
36
 
36
37
  import {
37
38
  FACTURX_ATTACHMENT_DESCRIPTION,
@@ -55,7 +56,8 @@ import type { IEInvoiceMetadata } from './interfaces/en16931-metadata.js';
55
56
  * Main class for working with electronic invoices.
56
57
  * Supports various invoice formats including Factur-X, ZUGFeRD, UBL, and XRechnung
57
58
  * Implements the tsclass accounting document envelope: one class holds an invoice,
58
- * a credit note, a debit note or a self-billed invoice, told apart by `accountingDocType`
59
+ * a corrected invoice, a credit note, a debit note or a self-billed invoice, told
60
+ * apart by `accountingDocType`
59
61
  */
60
62
  export class EInvoice implements TAccountingDocEnvelope<TAccountingDocType, {}> {
61
63
  private static sharedPdfEmbedder?: import('./formats/pdf/pdf.embedder.js').PDFEmbedder;
@@ -151,6 +153,20 @@ export class EInvoice implements TAccountingDocEnvelope<TAccountingDocType, {}>
151
153
  documentId: string;
152
154
  issueDate?: number;
153
155
  }>;
156
+
157
+ /**
158
+ * On a corrected invoice (type code 384): the invoice it corrects, written
159
+ * as the first preceding invoice reference (BG-3). A received corrected
160
+ * invoice may lack it.
161
+ */
162
+ public correctedInvoice?: finance.TCorrectedInvoiceReference;
163
+
164
+ /**
165
+ * On a corrected invoice: the details it supplies or corrects, as the
166
+ * issuer states them. EN 16931 has no element for them, so they are not
167
+ * written to the XML; an issuer states them as a note in `notes`.
168
+ */
169
+ public corrections?: finance.TCorrectedInvoice['corrections'];
154
170
  public printResult?: {
155
171
  pdfBufferString: string;
156
172
  totalNet: number;
@@ -446,6 +462,8 @@ export class EInvoice implements TAccountingDocEnvelope<TAccountingDocType, {}>
446
462
  electronicAddress: this.electronicAddress,
447
463
  paymentOptions: this.paymentOptions,
448
464
  relatedDocuments: this.relatedDocuments,
465
+ correctedInvoice: this.correctedInvoice,
466
+ corrections: this.corrections,
449
467
  printResult: this.printResult
450
468
  };
451
469
 
@@ -517,6 +535,14 @@ export class EInvoice implements TAccountingDocEnvelope<TAccountingDocType, {}>
517
535
  result = this.validateDecodedInvoice(level);
518
536
  }
519
537
 
538
+ // At the business level the document check covers the unit rules (BR-23, BR-CL-23) on every
539
+ // line. The feature flag validators below check the lines again on the decoded model, which
540
+ // cannot tell an absent unit from an empty unitCode="" attribute, so there they would report
541
+ // a second, possibly different rule for the same line. Below the business level the flags
542
+ // are the only unit checks and report them.
543
+ const isDocumentUnitRule = (ruleId: string): boolean =>
544
+ level === ValidationLevel.BUSINESS && (ruleId === 'BR-23' || ruleId === 'BR-CL-23');
545
+
520
546
  // Enhanced validation with feature flags
521
547
  if (options?.featureFlags?.includes('EN16931_BUSINESS_RULES')) {
522
548
  const businessRulesValidator = new EN16931BusinessRulesValidator();
@@ -526,6 +552,7 @@ export class EInvoice implements TAccountingDocEnvelope<TAccountingDocType, {}>
526
552
  result.errors = result.errors.concat(
527
553
  businessResults
528
554
  .filter(r => r.severity === 'error')
555
+ .filter(r => !isDocumentUnitRule(r.ruleId))
529
556
  .map(r => ({ code: r.ruleId, message: r.message, field: r.field }))
530
557
  );
531
558
 
@@ -548,6 +575,7 @@ export class EInvoice implements TAccountingDocEnvelope<TAccountingDocType, {}>
548
575
  result.errors = result.errors.concat(
549
576
  codeListResults
550
577
  .filter(r => r.severity === 'error')
578
+ .filter(r => !isDocumentUnitRule(r.ruleId))
551
579
  .map(r => ({ code: r.ruleId, message: r.message, field: r.field }))
552
580
  );
553
581
  }
@@ -576,6 +604,27 @@ export class EInvoice implements TAccountingDocEnvelope<TAccountingDocType, {}>
576
604
  this.createValidationError(message)
577
605
  );
578
606
 
607
+ if (level === ValidationLevel.BUSINESS) {
608
+ // BR-23 and BR-CL-23: every line has a unit code, and it is a code of UN/ECE Recommendation 20
609
+ // with the Recommendation 21 extension, as the XML validators check it on an exported document
610
+ (invoice.items ?? []).forEach((item, index) => {
611
+ if (!item.unitType) {
612
+ // BR-23: the line has no unit code at all
613
+ errors.push({
614
+ code: 'BR-23',
615
+ message: 'An Invoice line (BG-25) shall have an Invoiced quantity unit of measure code (BT-130)',
616
+ location: `items[${index}].unitType`
617
+ });
618
+ } else if (!meetsUnitCodeRule(item.unitType)) {
619
+ errors.push({
620
+ code: 'BR-CL-23',
621
+ message: `Unit code MUST be coded according to the UN/ECE Recommendation 20 with Rec 21 extension; found "${item.unitType}"`,
622
+ location: `items[${index}].unitType`
623
+ });
624
+ }
625
+ });
626
+ }
627
+
579
628
  return {
580
629
  valid: errors.length === 0,
581
630
  errors,
@@ -793,7 +842,7 @@ export class EInvoice implements TAccountingDocEnvelope<TAccountingDocType, {}>
793
842
  position: data.position || this.items.length + 1,
794
843
  name: data.name || '',
795
844
  articleNumber: data.articleNumber,
796
- unitType: data.unitType || 'unit',
845
+ unitType: data.unitType || 'C62',
797
846
  unitQuantity: data.unitQuantity || 1,
798
847
  unitNetPrice: data.unitNetPrice || 0,
799
848
  vatPercentage: data.vatPercentage || 0
@@ -6,7 +6,7 @@ import type { TAccountingDoc } from '../../interfaces/common.js';
6
6
  */
7
7
  export abstract class BaseEncoder {
8
8
  /**
9
- * Encodes an accounting document (invoice, credit note, debit note or self-billed invoice) into XML
9
+ * Encodes an accounting document (invoice, corrected invoice, credit note, debit note or self-billed invoice) into XML
10
10
  * @param invoice Accounting document to encode
11
11
  * @returns XML string
12
12
  */
@@ -61,4 +61,32 @@ export abstract class BaseValidator {
61
61
  location
62
62
  });
63
63
  }
64
+
65
+ /**
66
+ * The path of an element in the document, with the position of each step among its siblings
67
+ * of the same name, for example `/Invoice/cac:InvoiceLine[2]/cbc:InvoicedQuantity`.
68
+ */
69
+ protected locationOf(element: Element): string {
70
+ const steps: string[] = [];
71
+ let current: Element | null = element;
72
+ while (current) {
73
+ const name = current.nodeName;
74
+ let position = 1;
75
+ let count = 0;
76
+ const parent: Node | null = current.parentNode;
77
+ if (parent) {
78
+ for (let child = parent.firstChild; child; child = child.nextSibling) {
79
+ if (child.nodeType === 1 && child.nodeName === name) {
80
+ count++;
81
+ if (child === current) {
82
+ position = count;
83
+ }
84
+ }
85
+ }
86
+ }
87
+ steps.unshift(count > 1 ? `${name}[${position}]` : name);
88
+ current = parent && parent.nodeType === 1 ? parent as Element : null;
89
+ }
90
+ return `/${steps.join('/')}`;
91
+ }
64
92
  }
@@ -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,46 @@ 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 actual delivery date (BT-72) of the header delivery; undefined
132
+ * when the document does not state one.
133
+ */
134
+ protected extractDeliveryDate(): number | undefined {
135
+ const deliveryDatePath =
136
+ '/rsm:CrossIndustryInvoice/rsm:SupplyChainTradeTransaction/ram:ApplicableHeaderTradeDelivery/ram:ActualDeliverySupplyChainEvent/ram:OccurrenceDateTime/udt:DateTimeString';
137
+ const deliveryDate = this.getText(deliveryDatePath).trim();
138
+ if (!deliveryDate) {
139
+ return undefined;
140
+ }
141
+ return this.parseRequiredCIIDate(deliveryDate, this.getText(`${deliveryDatePath}/@format`).trim());
142
+ }
143
+
102
144
  /**
103
145
  * Gets a text value from an XPath expression
104
146
  * @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
@@ -3,6 +3,7 @@ import { ValidationLevel } from '../../interfaces/common.js';
3
3
  import type { ValidationResult } from '../../interfaces/common.js';
4
4
  import { CII_NAMESPACES, CIIProfile } from './cii.types.js';
5
5
  import { DOMParser, xpath } from '../../plugins.js';
6
+ import { meetsUnitCodeRule } from '../utils/unit.codes.js';
6
7
 
7
8
  const ciiValidatorParser = new DOMParser();
8
9
  const ciiValidatorNamespaces = {
@@ -113,6 +114,52 @@ export abstract class CIIBaseValidator extends BaseValidator {
113
114
  */
114
115
  protected abstract validateStructure(): boolean;
115
116
 
117
+ /**
118
+ * BR-23: every invoice line has a unit code on its billed quantity.
119
+ * @returns True if every line has one
120
+ */
121
+ protected validateLineUnitCodePresence(): boolean {
122
+ // BR-23: every invoice line (BG-25) has an invoiced quantity unit of measure code (BT-130)
123
+ const lines = this.select('//ram:IncludedSupplyChainTradeLineItem', this.doc) as Element[];
124
+ let valid = true;
125
+ for (const line of lines) {
126
+ if (!this.exists('./ram:SpecifiedLineTradeDelivery/ram:BilledQuantity/@unitCode', line)) {
127
+ this.addError(
128
+ 'BR-23',
129
+ 'An Invoice line (BG-25) shall have an Invoiced quantity unit of measure code (BT-130)',
130
+ this.locationOf(line)
131
+ );
132
+ valid = false;
133
+ }
134
+ }
135
+ return valid;
136
+ }
137
+
138
+ /**
139
+ * BR-CL-23: every unit code of a billed or basis quantity (BT-130, BT-150) is a code of
140
+ * UN/ECE Recommendation 20 with the Recommendation 21 extension.
141
+ * @returns True if every unit code passed
142
+ */
143
+ protected validateUnitCodes(): boolean {
144
+ const quantities = this.select(
145
+ '//ram:BilledQuantity[@unitCode] | //ram:BasisQuantity[@unitCode]',
146
+ this.doc
147
+ ) as Element[];
148
+ let valid = true;
149
+ for (const quantity of quantities) {
150
+ const unitCode = quantity.getAttribute('unitCode') ?? '';
151
+ if (!meetsUnitCodeRule(unitCode)) {
152
+ this.addError(
153
+ 'BR-CL-23',
154
+ `Unit code MUST be coded according to the UN/ECE Recommendation 20 with Rec 21 extension; found "${unitCode}"`,
155
+ `${this.locationOf(quantity)}/@unitCode`
156
+ );
157
+ valid = false;
158
+ }
159
+ }
160
+ return valid;
161
+ }
162
+
116
163
  /**
117
164
  * Detects the CII profile from the XML
118
165
  */
@@ -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
  */
@@ -92,6 +93,9 @@ export class FacturXDecoder extends CIIBaseDecoder {
92
93
  notes = allNotes.slice(1); // Remove subject from notes
93
94
  }
94
95
 
96
+ // Extract the actual delivery date, if stated
97
+ const deliveryDate = this.extractDeliveryDate();
98
+
95
99
  // Check for reverse charge
96
100
  const reverseCharge = this.exists('//ram:SpecifiedTradeAllowanceCharge/ram:ReasonCode[text()="62"]');
97
101
 
@@ -119,8 +123,10 @@ export class FacturXDecoder extends CIIBaseDecoder {
119
123
  reverseCharge: reverseCharge,
120
124
  currency: currencyCode as finance.TCurrency,
121
125
  notes: notes,
122
- deliveryDate: issueDate,
123
- objectActions: []
126
+ // the day of supply as the document states it (BT-72)
127
+ ...(deliveryDate === undefined ? {} : { deliveryDate }),
128
+ objectActions: [],
129
+ ...getPrecedingInvoiceFields(accountingDocType, this.extractPrecedingInvoiceReferences()),
124
130
  };
125
131
 
126
132
  // Validate mandatory EN16931 fields unless validation is skipped
@@ -191,7 +197,8 @@ export class FacturXDecoder extends CIIBaseDecoder {
191
197
  const name = this.getText('ram:SpecifiedTradeProduct/ram:Name', itemNode);
192
198
  const articleNumber = this.getText('ram:SpecifiedTradeProduct/ram:SellerAssignedID', itemNode);
193
199
  const unitQuantity = this.getNumber('ram:SpecifiedLineTradeDelivery/ram:BilledQuantity', itemNode);
194
- const unitType = this.getText('ram:SpecifiedLineTradeDelivery/ram:BilledQuantity/@unitCode', itemNode) || 'EA';
200
+ // an absent unit code (BT-130) stays absent, so that BR-23 reports it
201
+ const unitType = this.getText('ram:SpecifiedLineTradeDelivery/ram:BilledQuantity/@unitCode', itemNode);
195
202
  const unitNetPrice = this.getNumber('ram:SpecifiedLineTradeAgreement/ram:NetPriceProductTradePrice/ram:ChargeAmount', itemNode);
196
203
  const vatPercentage = this.getNumber('ram:SpecifiedLineTradeSettlement/ram:ApplicableTradeTax/ram:RateApplicablePercent', itemNode);
197
204
 
@@ -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 } 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);
@@ -324,7 +328,7 @@ export class FacturXEncoder extends CIIBaseEncoder {
324
328
  const dueDate = new Date(invoice.date);
325
329
  dueDate.setDate(dueDate.getDate() + invoice.dueInDays);
326
330
 
327
- dateStringElement.textContent = this.formatDateYYYYMMDD(dueDate.getTime());
331
+ dateStringElement.textContent = this.formatDateYYYYMMDD(dueDate.getTime(), 'BT-9 payment due date');
328
332
  dueDateElement.appendChild(dateStringElement);
329
333
  paymentTermsElement.appendChild(dueDateElement);
330
334
 
@@ -431,7 +435,10 @@ export class FacturXEncoder extends CIIBaseEncoder {
431
435
  const deliveryElement = doc.createElement('ram:SpecifiedLineTradeDelivery');
432
436
  const quantityElement = doc.createElement('ram:BilledQuantity');
433
437
  quantityElement.textContent = item.unitQuantity.toString();
434
- quantityElement.setAttribute('unitCode', item.unitType);
438
+ // a line without a unit is written without one, so that BR-23 reports it
439
+ if (item.unitType) {
440
+ quantityElement.setAttribute('unitCode', item.unitType);
441
+ }
435
442
  deliveryElement.appendChild(quantityElement);
436
443
  lineItemElement.appendChild(deliveryElement);
437
444
 
@@ -484,8 +491,8 @@ export class FacturXEncoder extends CIIBaseEncoder {
484
491
  * @param timestamp Timestamp to format
485
492
  * @returns Formatted date string
486
493
  */
487
- private formatDateYYYYMMDD(timestamp: number): string {
488
- const date = new Date(timestamp);
494
+ private formatDateYYYYMMDD(timestamp: number, field: string): string {
495
+ const date = getWritableDate(timestamp, field, 'cii');
489
496
  const year = date.getFullYear();
490
497
  const month = (date.getMonth() + 1).toString().padStart(2, '0');
491
498
  const day = date.getDate().toString().padStart(2, '0');
@@ -24,14 +24,14 @@ export class FacturXValidator extends CIIBaseValidator {
24
24
  ];
25
25
 
26
26
  for (const section of sections) {
27
- if (!this.exists(section)) {
27
+ if (!this.exists(`/rsm:CrossIndustryInvoice/${section}`)) {
28
28
  this.addError('FX-STRUCT-1', `Required section ${section} is missing`, '/rsm:CrossIndustryInvoice');
29
29
  valid = false;
30
30
  }
31
31
  }
32
32
 
33
33
  // Check for SupplyChainTradeTransaction sections
34
- if (this.exists('rsm:SupplyChainTradeTransaction')) {
34
+ if (this.exists('/rsm:CrossIndustryInvoice/rsm:SupplyChainTradeTransaction')) {
35
35
  const tradeSubsections = [
36
36
  'ram:ApplicableHeaderTradeAgreement',
37
37
  'ram:ApplicableHeaderTradeDelivery',
@@ -39,7 +39,7 @@ export class FacturXValidator extends CIIBaseValidator {
39
39
  ];
40
40
 
41
41
  for (const subsection of tradeSubsections) {
42
- if (!this.exists(`rsm:SupplyChainTradeTransaction/${subsection}`)) {
42
+ if (!this.exists(`/rsm:CrossIndustryInvoice/rsm:SupplyChainTradeTransaction/${subsection}`)) {
43
43
  this.addError('FX-STRUCT-2', `Required subsection ${subsection} is missing`,
44
44
  '/rsm:CrossIndustryInvoice/rsm:SupplyChainTradeTransaction');
45
45
  valid = false;
@@ -70,6 +70,12 @@ export class FacturXValidator extends CIIBaseValidator {
70
70
  // and/or the Seller tax representative VAT identifier (BT-63).
71
71
  valid = this.validateSellerVatIdentifier() && valid;
72
72
 
73
+ // BR-23: every line has a unit code
74
+ valid = this.validateLineUnitCodePresence() && valid;
75
+
76
+ // BR-CL-23: unit codes of UN/ECE Recommendation 20 with the Recommendation 21 extension
77
+ valid = this.validateUnitCodes() && valid;
78
+
73
79
  return valid;
74
80
  }
75
81
 
@@ -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
@@ -195,7 +201,8 @@ export class ZUGFeRDDecoder extends CIIBaseDecoder {
195
201
  const name = this.getText('ram:SpecifiedTradeProduct/ram:Name', itemNode);
196
202
  const articleNumber = this.getText('ram:SpecifiedTradeProduct/ram:SellerAssignedID', itemNode);
197
203
  const unitQuantity = this.getNumber('ram:SpecifiedLineTradeDelivery/ram:BilledQuantity', itemNode);
198
- const unitType = this.getText('ram:SpecifiedLineTradeDelivery/ram:BilledQuantity/@unitCode', itemNode) || 'EA';
204
+ // an absent unit code (BT-130) stays absent, so that BR-23 reports it
205
+ const unitType = this.getText('ram:SpecifiedLineTradeDelivery/ram:BilledQuantity/@unitCode', itemNode);
199
206
  const unitNetPrice = this.getNumber('ram:SpecifiedLineTradeAgreement/ram:NetPriceProductTradePrice/ram:ChargeAmount', itemNode);
200
207
  const vatPercentage = this.getNumber('ram:SpecifiedLineTradeSettlement/ram:ApplicableTradeTax/ram:RateApplicablePercent', itemNode);
201
208