@fin.cx/einvoice 8.2.2 → 8.2.4

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 (40) hide show
  1. package/dist_ts/00_commitinfo_data.js +1 -1
  2. package/dist_ts/formats/cii/cii.decoder.d.ts +6 -0
  3. package/dist_ts/formats/cii/cii.decoder.js +11 -1
  4. package/dist_ts/formats/cii/cii.encoder.d.ts +10 -0
  5. package/dist_ts/formats/cii/cii.encoder.js +42 -2
  6. package/dist_ts/formats/cii/cii.types.d.ts +11 -0
  7. package/dist_ts/formats/cii/cii.types.js +41 -1
  8. package/dist_ts/formats/cii/facturx/facturx.decoder.js +8 -4
  9. package/dist_ts/formats/cii/facturx/facturx.encoder.js +30 -17
  10. package/dist_ts/formats/cii/zugferd/zugferd.decoder.js +8 -4
  11. package/dist_ts/formats/cii/zugferd/zugferd.encoder.js +63 -43
  12. package/dist_ts/formats/cii/zugferd/zugferd.v1.decoder.d.ts +5 -0
  13. package/dist_ts/formats/cii/zugferd/zugferd.v1.decoder.js +18 -5
  14. package/dist_ts/formats/ubl/generic/ubl.encoder.js +14 -15
  15. package/dist_ts/formats/ubl/ubl.encoder.js +4 -1
  16. package/dist_ts/formats/ubl/xrechnung/xrechnung.decoder.d.ts +4 -0
  17. package/dist_ts/formats/ubl/xrechnung/xrechnung.decoder.js +12 -2
  18. package/dist_ts/formats/utils/document.totals.d.ts +16 -6
  19. package/dist_ts/formats/utils/document.totals.js +29 -11
  20. package/dist_ts/formats/utils/number.text.d.ts +9 -0
  21. package/dist_ts/formats/utils/number.text.js +26 -0
  22. package/dist_ts/formats/utils/vat.category.d.ts +50 -0
  23. package/dist_ts/formats/utils/vat.category.js +64 -0
  24. package/package.json +2 -2
  25. package/readme.md +25 -0
  26. package/ts/00_commitinfo_data.ts +1 -1
  27. package/ts/formats/cii/cii.decoder.ts +14 -0
  28. package/ts/formats/cii/cii.encoder.ts +42 -1
  29. package/ts/formats/cii/cii.types.ts +41 -0
  30. package/ts/formats/cii/facturx/facturx.decoder.ts +8 -3
  31. package/ts/formats/cii/facturx/facturx.encoder.ts +32 -16
  32. package/ts/formats/cii/zugferd/zugferd.decoder.ts +8 -3
  33. package/ts/formats/cii/zugferd/zugferd.encoder.ts +71 -46
  34. package/ts/formats/cii/zugferd/zugferd.v1.decoder.ts +22 -4
  35. package/ts/formats/ubl/generic/ubl.encoder.ts +13 -15
  36. package/ts/formats/ubl/ubl.encoder.ts +3 -0
  37. package/ts/formats/ubl/xrechnung/xrechnung.decoder.ts +15 -1
  38. package/ts/formats/utils/document.totals.ts +43 -14
  39. package/ts/formats/utils/number.text.ts +25 -0
  40. package/ts/formats/utils/vat.category.ts +89 -0
@@ -1,6 +1,8 @@
1
1
  import { Decimal } from './decimal.js';
2
2
  import { DecimalCurrencyCalculator } from './currency.calculator.decimal.js';
3
+ import { toPlainDecimalString } from './number.text.js';
3
4
  import { EInvoiceFormatError } from '../../errors.js';
5
+ import { getVatCategory, getVatExemption } from './vat.category.js';
4
6
  /**
5
7
  * The item amounts of a document that are no finite number. The totals cannot
6
8
  * be computed while there is one: the encoders and the total getters refuse
@@ -51,37 +53,53 @@ export const getDocumentCalculator = (currency) => new DecimalCurrencyCalculator
51
53
  * decimals (BR-DEC-*): each line net amount (BT-131) is quantity × net price
52
54
  * rounded; the sum of line net amounts (BT-106) is their sum (BR-CO-10);
53
55
  * each VAT category taxable amount (BT-116) is the sum of the line net amounts
54
- * at that rate, and its tax amount (BT-117) is BT-116 × rate / 100 rounded
55
- * (BR-CO-17); the total VAT (BT-110) is the sum of the category tax amounts
56
+ * of that category and rate, and its tax amount (BT-117) is BT-116 × rate / 100
57
+ * rounded (BR-CO-17), 0 for reverse charge (BR-AE-09); the total VAT (BT-110) is the sum of the category tax amounts
56
58
  * (BR-CO-14); the total with VAT (BT-112) is BT-109 + BT-110 (BR-CO-15).
57
59
  * @param accountingDoc The document
58
60
  */
59
61
  export const computeDocumentTotals = (accountingDoc) => {
60
62
  const calculator = getDocumentCalculator(accountingDoc.currency);
61
63
  const minorUnits = calculator.getCurrencyInfo().minorUnits;
64
+ const category = getVatCategory(accountingDoc);
62
65
  const lineNetAmounts = [];
63
- const taxableByRate = new Map();
66
+ const lineVatCategories = [];
67
+ const taxableByGroup = new Map();
64
68
  // an amount that is no number cannot be computed with; it is refused, not treated as 0
65
69
  const [firstInvalid] = findInvalidItemAmounts(accountingDoc.items);
66
70
  if (firstInvalid) {
67
71
  throw new EInvoiceFormatError(`items[${firstInvalid.index}].${firstInvalid.field} is no number: ${String(firstInvalid.value)}`, { unsupportedFeatures: [`items[${firstInvalid.index}].${firstInvalid.field}`] });
68
72
  }
69
73
  for (const item of accountingDoc.items ?? []) {
70
- const lineNet = calculator.calculateLineNet(item.unitQuantity, item.unitNetPrice);
74
+ // quantity × net price from the numerals the encoders write, so the line net amount is the
75
+ // product of the written values, at their full precision
76
+ const lineNet = calculator.calculateLineNet(toPlainDecimalString(item.unitQuantity), toPlainDecimalString(item.unitNetPrice));
71
77
  lineNetAmounts.push(lineNet);
72
- taxableByRate.set(item.vatPercentage, (taxableByRate.get(item.vatPercentage) ?? Decimal.ZERO).add(lineNet));
78
+ lineVatCategories.push(category);
79
+ // grouped by VAT category and rate: the same rate under two categories is two groups
80
+ const key = `${category}|${item.vatPercentage}`;
81
+ const group = taxableByGroup.get(key) ?? { category, rate: item.vatPercentage, taxable: Decimal.ZERO };
82
+ group.taxable = group.taxable.add(lineNet);
83
+ taxableByGroup.set(key, group);
73
84
  }
74
- const vatGroups = [...taxableByRate.entries()].map(([rate, taxableAmount]) => ({
75
- rate,
76
- taxableAmount: calculator.round(taxableAmount),
77
- taxAmount: calculator.calculateVAT(taxableAmount, rate),
78
- }));
85
+ const vatGroups = [...taxableByGroup.values()].map((group) => {
86
+ const exemption = getVatExemption(group.category, accountingDoc.language);
87
+ return {
88
+ category: group.category,
89
+ rate: group.rate,
90
+ taxableAmount: calculator.round(group.taxable),
91
+ // reverse charge states no tax: the recipient owes it (BR-AE-09)
92
+ taxAmount: group.category === 'AE' ? calculator.round(Decimal.ZERO) : calculator.calculateVAT(group.taxable, group.rate),
93
+ ...(exemption ? { exemption } : {}),
94
+ };
95
+ });
79
96
  const lineTotal = calculator.round(Decimal.sum(lineNetAmounts));
80
97
  const taxTotal = calculator.round(Decimal.sum(vatGroups.map((group) => group.taxAmount)));
81
98
  const grandTotal = calculator.round(lineTotal.add(taxTotal));
82
99
  return {
83
100
  minorUnits,
84
101
  lineNetAmounts,
102
+ lineVatCategories,
85
103
  lineTotal,
86
104
  taxBasisTotal: lineTotal,
87
105
  vatGroups,
@@ -90,4 +108,4 @@ export const computeDocumentTotals = (accountingDoc) => {
90
108
  duePayable: grandTotal,
91
109
  };
92
110
  };
93
- //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiZG9jdW1lbnQudG90YWxzLmpzIiwic291cmNlUm9vdCI6IiIsInNvdXJjZXMiOlsiLi4vLi4vLi4vdHMvZm9ybWF0cy91dGlscy9kb2N1bWVudC50b3RhbHMudHMiXSwibmFtZXMiOltdLCJtYXBwaW5ncyI6IkFBQ0EsT0FBTyxFQUFFLE9BQU8sRUFBRSxNQUFNLGNBQWMsQ0FBQztBQUN2QyxPQUFPLEVBQUUseUJBQXlCLEVBQUUsTUFBTSxrQ0FBa0MsQ0FBQztBQUM3RSxPQUFPLEVBQUUsbUJBQW1CLEVBQUUsTUFBTSxpQkFBaUIsQ0FBQztBQVV0RDs7Ozs7O0dBTUc7QUFDSCxNQUFNLENBQUMsTUFBTSxzQkFBc0IsR0FBRyxDQUFDLEtBQTBDLEVBQXdCLEVBQUU7SUFDekcsTUFBTSxPQUFPLEdBQXlCLEVBQUUsQ0FBQztJQUN6QyxLQUFLLE1BQU0sQ0FBQyxLQUFLLEVBQUUsSUFBSSxDQUFDLElBQUksQ0FBQyxLQUFLLElBQUksRUFBRSxDQUFDLENBQUMsT0FBTyxFQUFFLEVBQUUsQ0FBQztRQUNwRCxLQUFLLE1BQU0sS0FBSyxJQUFJLENBQUMsY0FBYyxFQUFFLGNBQWMsRUFBRSxlQUFlLENBQVUsRUFBRSxDQUFDO1lBQy9FLE1BQU0sS0FBSyxHQUFZLElBQUksQ0FBQyxLQUFLLENBQUMsQ0FBQztZQUNuQyxJQUFJLE9BQU8sS0FBSyxLQUFLLFFBQVEsSUFBSSxDQUFDLE1BQU0sQ0FBQyxRQUFRLENBQUMsS0FBSyxDQUFDLEVBQUUsQ0FBQztnQkFDekQsT0FBTyxDQUFDLElBQUksQ0FBQyxFQUFFLEtBQUssRUFBRSxLQUFLLEVBQUUsS0FBSyxFQUFFLENBQUMsQ0FBQztZQUN4QyxDQUFDO1FBQ0gsQ0FBQztJQUNILENBQUM7SUFDRCxPQUFPLE9BQU8sQ0FBQztBQUNqQixDQUFDLENBQUM7QUFFRjs7Ozs7R0FLRztBQUNILE1BQU0sQ0FBQyxNQUFNLHNCQUFzQixHQUFHLENBQUMsT0FBNkIsRUFBRSxNQUFjLEVBQW9CLEVBQUUsQ0FBQyxDQUFDO0lBQzFHLE1B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
111
+ //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiZG9jdW1lbnQudG90YWxzLmpzIiwic291cmNlUm9vdCI6IiIsInNvdXJjZXMiOlsiLi4vLi4vLi4vdHMvZm9ybWF0cy91dGlscy9kb2N1bWVudC50b3RhbHMudHMiXSwibmFtZXMiOltdLCJtYXBwaW5ncyI6IkFBQ0EsT0FBTyxFQUFFLE9BQU8sRUFBRSxNQUFNLGNBQWMsQ0FBQztBQUN2QyxPQUFPLEVBQUUseUJBQXlCLEVBQUUsTUFBTSxrQ0FBa0MsQ0FBQztBQUM3RSxPQUFPLEVBQUUsb0JBQW9CLEVBQUUsTUFBTSxrQkFBa0IsQ0FBQztBQUN4RCxPQUFPLEVBQUUsbUJBQW1CLEVBQUUsTUFBTSxpQkFBaUIsQ0FBQztBQUV0RCxPQUFPLEVBQUUsY0FBYyxFQUFFLGVBQWUsRUFBNkMsTUFBTSxtQkFBbUIsQ0FBQztBQVMvRzs7Ozs7O0dBTUc7QUFDSCxNQUFNLENBQUMsTUFBTSxzQkFBc0IsR0FBRyxDQUFDLEtBQTBDLEVBQXdCLEVBQUU7SUFDekcsTUFBTSxPQUFPLEdBQXlCLEVBQUUsQ0FBQztJQUN6QyxLQUFLLE1BQU0sQ0FBQyxLQUFLLEVBQUUsSUFBSSxDQUFDLElBQUksQ0FBQyxLQUFLLElBQUksRUFBRSxDQUFDLENBQUMsT0FBTyxFQUFFLEVBQUUsQ0FBQztRQUNwRCxLQUFLLE1BQU0sS0FBSyxJQUFJLENBQUMsY0FBYyxFQUFFLGNBQWMsRUFBRSxlQUFlLENBQVUsRUFBRSxDQUFDO1lBQy9FLE1BQU0sS0FBSyxHQUFZLElBQUksQ0FBQyxLQUFLLENBQUMsQ0FBQztZQUNuQyxJQUFJLE9BQU8sS0FBSyxLQUFLLFFBQVEsSUFBSSxDQUFDLE1BQU0sQ0FBQyxRQUFRLENBQUMsS0FBSyxDQUFDLEVBQUUsQ0FBQztnQkFDekQsT0FBTyxDQUFDLElBQUksQ0FBQyxFQUFFLEtBQUssRUFBRSxLQUFLLEVBQUUsS0FBSyxFQUFFLENBQUMsQ0FBQztZQUN4QyxDQUFDO1FBQ0gsQ0FBQztJQUNILENBQUM7SUFDRCxPQUFPLE9BQU8sQ0FBQztBQUNqQixDQUFDLENB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
@@ -0,0 +1,9 @@
1
+ /**
2
+ * A finite number as a plain decimal numeral (`xsd:decimal`), the shortest one
3
+ * that reads back as the same number, without an exponent: `0.335`, `1234.5`,
4
+ * `0.0000001` (not `1e-7`). Quantities (BT-129) and net prices (BT-146) are
5
+ * written this way, with the precision they have; EN 16931 limits neither to
6
+ * two decimals (BR-DEC-* cover amounts only).
7
+ * @param value A finite number
8
+ */
9
+ export declare const toPlainDecimalString: (value: number) => string;
@@ -0,0 +1,26 @@
1
+ /**
2
+ * A finite number as a plain decimal numeral (`xsd:decimal`), the shortest one
3
+ * that reads back as the same number, without an exponent: `0.335`, `1234.5`,
4
+ * `0.0000001` (not `1e-7`). Quantities (BT-129) and net prices (BT-146) are
5
+ * written this way, with the precision they have; EN 16931 limits neither to
6
+ * two decimals (BR-DEC-* cover amounts only).
7
+ * @param value A finite number
8
+ */
9
+ export const toPlainDecimalString = (value) => {
10
+ const text = String(value);
11
+ const match = /^(-?)(\d+)(?:\.(\d+))?e([+-]\d+)$/i.exec(text);
12
+ if (!match) {
13
+ return text;
14
+ }
15
+ const [, sign, integerDigits, fractionDigits = '', exponentText] = match;
16
+ const digits = integerDigits + fractionDigits;
17
+ const point = integerDigits.length + Number(exponentText);
18
+ if (point <= 0) {
19
+ return `${sign}0.${'0'.repeat(-point)}${digits}`;
20
+ }
21
+ if (point >= digits.length) {
22
+ return `${sign}${digits}${'0'.repeat(point - digits.length)}`;
23
+ }
24
+ return `${sign}${digits.slice(0, point)}.${digits.slice(point)}`;
25
+ };
26
+ //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoibnVtYmVyLnRleHQuanMiLCJzb3VyY2VSb290IjoiIiwic291cmNlcyI6WyIuLi8uLi8uLi90cy9mb3JtYXRzL3V0aWxzL251bWJlci50ZXh0LnRzIl0sIm5hbWVzIjpbXSwibWFwcGluZ3MiOiJBQUFBOzs7Ozs7O0dBT0c7QUFDSCxNQUFNLENBQUMsTUFBTSxvQkFBb0IsR0FBRyxDQUFDLEtBQWEsRUFBVSxFQUFFO0lBQzVELE1BQU0sSUFBSSxHQUFHLE1BQU0sQ0FBQyxLQUFLLENBQUMsQ0FBQztJQUMzQixNQUFNLEtBQUssR0FBRyxvQ0FBb0MsQ0FBQyxJQUFJLENBQUMsSUFBSSxDQUFDLENBQUM7SUFDOUQsSUFBSSxDQUFDLEtBQUssRUFBRSxDQUFDO1FBQ1gsT0FBTyxJQUFJLENBQUM7SUFDZCxDQUFDO0lBQ0QsTUFBTSxDQUFDLEVBQUUsSUFBSSxFQUFFLGFBQWEsRUFBRSxjQUFjLEdBQUcsRUFBRSxFQUFFLFlBQVksQ0FBQyxHQUFHLEtBQUssQ0FBQztJQUN6RSxNQUFNLE1BQU0sR0FBRyxhQUFhLEdBQUcsY0FBYyxDQUFDO0lBQzlDLE1BQU0sS0FBSyxHQUFHLGFBQWEsQ0FBQyxNQUFNLEdBQUcsTUFBTSxDQUFDLFlBQVksQ0FBQyxDQUFDO0lBQzFELElBQUksS0FBSyxJQUFJLENBQUMsRUFBRSxDQUFDO1FBQ2YsT0FBTyxHQUFHLElBQUksS0FBSyxHQUFHLENBQUMsTUFBTSxDQUFDLENBQUMsS0FBSyxDQUFDLEdBQUcsTUFBTSxFQUFFLENBQUM7SUFDbkQsQ0FBQztJQUNELElBQUksS0FBSyxJQUFJLE1BQU0sQ0FBQyxNQUFNLEVBQUUsQ0FBQztRQUMzQixPQUFPLEdBQUcsSUFBSSxHQUFHLE1BQU0sR0FBRyxHQUFHLENBQUMsTUFBTSxDQUFDLEtBQUssR0FBRyxNQUFNLENBQUMsTUFBTSxDQUFDLEVBQUUsQ0FBQztJQUNoRSxDQUFDO0lBQ0QsT0FBTyxHQUFHLElBQUksR0FBRyxNQUFNLENBQUMsS0FBSyxDQUFDLENBQUMsRUFBRSxLQUFLLENBQUMsSUFBSSxNQUFNLENBQUMsS0FBSyxDQUFDLEtBQUssQ0FBQyxFQUFFLENBQUM7QUFDbkUsQ0FBQyxDQUFDIn0=
@@ -0,0 +1,50 @@
1
+ import type { TAccountingDoc } from '../../interfaces/common.js';
2
+ /**
3
+ * The VAT category codes (UNTDID 5305, EN 16931 BT-118/BT-151) the encoders
4
+ * write: standard rated, or reverse charge. The envelope states reverse charge
5
+ * for the whole document (`reverseCharge`); an item carries no category of its
6
+ * own, so a document cannot mix reverse charge lines with standard rated ones.
7
+ */
8
+ export type TVatCategoryCode = 'S' | 'AE';
9
+ /** The VAT exemption reason (BT-121 code, BT-120 text) a VAT category states */
10
+ export interface IVatExemption {
11
+ code: string;
12
+ reason: string;
13
+ }
14
+ /**
15
+ * The VAT category of every line of a document: reverse charge (`AE`) when the
16
+ * document states it, standard rated (`S`) otherwise.
17
+ * @param accountingDoc The document
18
+ */
19
+ export declare const getVatCategory: (accountingDoc: {
20
+ reverseCharge?: boolean;
21
+ }) => TVatCategoryCode;
22
+ /**
23
+ * The exemption reason a reverse charge VAT breakdown states (BR-AE-10): the
24
+ * code `VATEX-EU-AE` and the wording the law prescribes, "Steuerschuldnerschaft
25
+ * des Leistungsempfängers" (§ 14a Abs. 1 and 5 UStG). A document in another
26
+ * language may use the wording of Article 226 Nr. 11a of the VAT Directive in
27
+ * that language, "Reverse charge" in English (Abschnitt 14a.1 Abs. 6 Satz 2
28
+ * UStAE). Other categories state none.
29
+ * @param category The VAT category
30
+ * @param language The document language
31
+ */
32
+ export declare const getVatExemption: (category: TVatCategoryCode, language: string | undefined) => IVatExemption | undefined;
33
+ /**
34
+ * Refuses a reverse charge document that lacks what EN 16931 requires of one,
35
+ * naming the rule; no value is put in place of a missing one.
36
+ * - BR-AE-05: every line of a reverse charge document has the VAT rate 0; the
37
+ * recipient owes the tax, and the invoice states none (§ 14a Abs. 5 Satz 2
38
+ * UStG; a stated amount would be owed under § 14c Abs. 1 UStG).
39
+ * - BR-AE-02: the seller states a VAT identifier (BT-31) and the buyer a VAT
40
+ * identifier (BT-48) or a legal registration identifier (BT-47). The rule
41
+ * also accepts the seller's tax registration identifier (BT-32, e.g. the
42
+ * Steuernummer) or a tax representative's VAT identifier (BT-63) instead of
43
+ * BT-31; the envelope can state neither, so a seller without a VAT
44
+ * identifier is refused. A domestic § 13b issuer that states only a
45
+ * Steuernummer, which § 14 Abs. 4 Satz 1 Nr. 2 UStG allows, is therefore
46
+ * refused until the envelope carries a tax number.
47
+ * @param accountingDoc The document
48
+ * @param targetFormat The format being written
49
+ */
50
+ export declare const assertVatCategoryWritable: (accountingDoc: TAccountingDoc, targetFormat: string) => void;
@@ -0,0 +1,64 @@
1
+ import { EInvoiceFormatError } from '../../errors.js';
2
+ /**
3
+ * The VAT category of every line of a document: reverse charge (`AE`) when the
4
+ * document states it, standard rated (`S`) otherwise.
5
+ * @param accountingDoc The document
6
+ */
7
+ export const getVatCategory = (accountingDoc) => accountingDoc.reverseCharge ? 'AE' : 'S';
8
+ /**
9
+ * The exemption reason a reverse charge VAT breakdown states (BR-AE-10): the
10
+ * code `VATEX-EU-AE` and the wording the law prescribes, "Steuerschuldnerschaft
11
+ * des Leistungsempfängers" (§ 14a Abs. 1 and 5 UStG). A document in another
12
+ * language may use the wording of Article 226 Nr. 11a of the VAT Directive in
13
+ * that language, "Reverse charge" in English (Abschnitt 14a.1 Abs. 6 Satz 2
14
+ * UStAE). Other categories state none.
15
+ * @param category The VAT category
16
+ * @param language The document language
17
+ */
18
+ export const getVatExemption = (category, language) => category === 'AE'
19
+ ? {
20
+ code: 'VATEX-EU-AE',
21
+ reason: (language ?? '').toLowerCase().startsWith('de')
22
+ ? 'Steuerschuldnerschaft des Leistungsempfängers'
23
+ : 'Reverse charge',
24
+ }
25
+ : undefined;
26
+ /**
27
+ * Refuses a reverse charge document that lacks what EN 16931 requires of one,
28
+ * naming the rule; no value is put in place of a missing one.
29
+ * - BR-AE-05: every line of a reverse charge document has the VAT rate 0; the
30
+ * recipient owes the tax, and the invoice states none (§ 14a Abs. 5 Satz 2
31
+ * UStG; a stated amount would be owed under § 14c Abs. 1 UStG).
32
+ * - BR-AE-02: the seller states a VAT identifier (BT-31) and the buyer a VAT
33
+ * identifier (BT-48) or a legal registration identifier (BT-47). The rule
34
+ * also accepts the seller's tax registration identifier (BT-32, e.g. the
35
+ * Steuernummer) or a tax representative's VAT identifier (BT-63) instead of
36
+ * BT-31; the envelope can state neither, so a seller without a VAT
37
+ * identifier is refused. A domestic § 13b issuer that states only a
38
+ * Steuernummer, which § 14 Abs. 4 Satz 1 Nr. 2 UStG allows, is therefore
39
+ * refused until the envelope carries a tax number.
40
+ * @param accountingDoc The document
41
+ * @param targetFormat The format being written
42
+ */
43
+ export const assertVatCategoryWritable = (accountingDoc, targetFormat) => {
44
+ if (!accountingDoc.reverseCharge) {
45
+ return;
46
+ }
47
+ const refuse = (rule, message) => {
48
+ throw new EInvoiceFormatError(`${rule}: ${message}`, { targetFormat, unsupportedFeatures: [rule] });
49
+ };
50
+ for (const [index, item] of (accountingDoc.items ?? []).entries()) {
51
+ if (item.vatPercentage !== 0) {
52
+ refuse('BR-AE-05', `a reverse charge line has the VAT rate 0, items[${index}].vatPercentage is ${String(item.vatPercentage)}`);
53
+ }
54
+ }
55
+ const seller = accountingDoc.from?.registrationDetails;
56
+ if (!seller?.vatId) {
57
+ refuse('BR-AE-02', 'a reverse charge document states the seller VAT identifier (BT-31), from.registrationDetails.vatId is missing; the rule would also accept a tax registration identifier (BT-32) or a tax representative (BT-63), which the envelope cannot state');
58
+ }
59
+ const buyer = accountingDoc.to?.registrationDetails;
60
+ if (!buyer?.vatId && !buyer?.registrationId) {
61
+ refuse('BR-AE-02', 'a reverse charge document states the buyer VAT identifier (BT-48) or legal registration identifier (BT-47), to.registrationDetails.vatId and .registrationId are missing');
62
+ }
63
+ };
64
+ //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoidmF0LmNhdGVnb3J5LmpzIiwic291cmNlUm9vdCI6IiIsInNvdXJjZXMiOlsiLi4vLi4vLi4vdHMvZm9ybWF0cy91dGlscy92YXQuY2F0ZWdvcnkudHMiXSwibmFtZXMiOltdLCJtYXBwaW5ncyI6IkFBQ0EsT0FBTyxFQUFFLG1CQUFtQixFQUFFLE1BQU0saUJBQWlCLENBQUM7QUFnQnREOzs7O0dBSUc7QUFDSCxNQUFNLENBQUMsTUFBTSxjQUFjLEdBQUcsQ0FBQyxhQUEwQyxFQUFvQixFQUFFLENBQzdGLGFBQWEsQ0FBQyxhQUFhLENBQUMsQ0FBQyxDQUFDLElBQUksQ0FBQyxDQUFDLENBQUMsR0FBRyxDQUFDO0FBRTNDOzs7Ozs7Ozs7R0FTRztBQUNILE1BQU0sQ0FBQyxNQUFNLGVBQWUsR0FBRyxDQUFDLFFBQTBCLEVBQUUsUUFBNEIsRUFBNkIsRUFBRSxDQUNySCxRQUFRLEtBQUssSUFBSTtJQUNmLENBQUMsQ0FBQztRQUNFLElBQUksRUFBRSxhQUFhO1FBQ25CLE1BQU0sRUFBRSxDQUFDLFFBQVEsSUFBSSxFQUFFLENBQUMsQ0FBQyxXQUFXLEVBQUUsQ0FBQyxVQUFVLENBQUMsSUFBSSxDQUFDO1lBQ3JELENBQUMsQ0FBQywrQ0FBK0M7WUFDakQsQ0FBQyxDQUFDLGdCQUFnQjtLQUNyQjtJQUNILENBQUMsQ0FBQyxTQUFTLENBQUM7QUFFaEI7Ozs7Ozs7Ozs7Ozs7Ozs7R0FnQkc7QUFDSCxNQUFNLENBQUMsTUFBTSx5QkFBeUIsR0FBRyxDQUFDLGFBQTZCLEVBQUUsWUFBb0IsRUFBUSxFQUFFO0lBQ3JHLElBQUksQ0FBQyxhQUFhLENBQUMsYUFBYSxFQUFFLENBQUM7UUFDakMsT0FBTztJQUNULENBQUM7SUFDRCxNQUFNLE1BQU0sR0FBRyxDQUFDLElBQVksRUFBRSxPQUFlLEVBQVMsRUFBRTtRQUN0RCxNQUFNLElBQUksbUJBQW1CLENBQUMsR0FBRyxJQUFJLEtBQUssT0FBTyxFQUFFLEVBQUUsRUFBRSxZQUFZLEVBQUUsbUJBQW1CLEVBQUUsQ0FBQyxJQUFJLENBQUMsRUFBRSxDQUFDLENBQUM7SUFDdEcsQ0FBQyxDQUFDO0lBQ0YsS0FBSyxNQUFNLENBQUMsS0FBSyxFQUFFLElBQUksQ0FBQyxJQUFJLENBQUMsYUFBYSxDQUFDLEtBQUssSUFBSSxFQUFFLENBQUMsQ0FBQyxPQUFPLEVBQUUsRUFBRSxDQUFDO1FBQ2xFLElBQUksSUFBSSxDQUFDLGFBQWEsS0FBSyxDQUFDLEVBQUUsQ0FBQztZQUM3QixNQUFNLENBQUMsVUFBVSxFQUFFLG1EQUFtRCxLQUFLLHNCQUFzQixNQUFNLENBQUMsSUFBSSxDQUFDLGFBQWEsQ0FBQyxFQUFFLENBQUMsQ0FBQztRQUNqSSxDQUFDO0lBQ0gsQ0FBQztJQUNELE1BQU0sTUFBTSxHQUFHLGFBQWEsQ0FBQyxJQUFJLEVBQUUsbUJBQW1CLENBQUM7SUFDdkQsSUFBSSxDQUFDLE1BQU0sRUFBRSxLQUFLLEVBQUUsQ0FBQztRQUNuQixNQUFNLENBQ0osVUFBVSxFQUNWLGtQQUFrUCxDQUNuUCxDQUFDO0lBQ0osQ0FBQztJQUNELE1BQU0sS0FBSyxHQUFHLGFBQWEsQ0FBQyxFQUFFLEVBQUUsbUJBQW1CLENBQUM7SUFDcEQsSUFBSSxDQUFDLEtBQUssRUFBRSxLQUFLLElBQUksQ0FBQyxLQUFLLEVBQUUsY0FBYyxFQUFFLENBQUM7UUFDNUMsTUFBTSxDQUNKLFVBQVUsRUFDViwwS0FBMEssQ0FDM0ssQ0FBQztJQUNKLENBQUM7QUFDSCxDQUFDLENBQUMifQ==
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fin.cx/einvoice",
3
- "version": "8.2.2",
3
+ "version": "8.2.4",
4
4
  "private": false,
5
5
  "description": "A TypeScript module for creating, manipulating, and embedding XML data within PDF files specifically tailored for electronic invoice (einvoice) packages.",
6
6
  "main": "dist_ts/index.js",
@@ -67,7 +67,7 @@
67
67
  ],
68
68
  "scripts": {
69
69
  "test": "pnpm run test:unit",
70
- "test:unit": "tstest test/test.cii-xrechnung.node.ts --verbose --logfile --timeout 60 && tstest test/test.einvoice.ts --verbose --logfile --timeout 60 && tstest test/test.decimal.ts --verbose --logfile --timeout 60 && tstest test/test.decimal-currency-calculator.ts --verbose --logfile --timeout 60 && tstest test/test.currency-utils.ts --verbose --logfile --timeout 60 && tstest test/test.en16931-validators.ts --verbose --logfile --timeout 60 && tstest test/test.schematron-validator.ts --verbose --logfile --timeout 60 && tstest test/test.xrechnung-creditnote.node.ts --verbose --logfile --timeout 60 && tstest test/test.accountingdoc-types.node.ts --verbose --logfile --timeout 60 && tstest test/test.corrected-invoice.node.ts --verbose --logfile --timeout 60 && tstest test/test.xrechnung-3-due-date.node.ts --verbose --logfile --timeout 60 && tstest test/test.calendar-dates.node.ts --verbose --logfile --timeout 60 && tstest test/test.document-totals.node.ts --verbose --logfile --timeout 60 && tstest test/test.en16931-unitcodes.node.ts --verbose --logfile --timeout 60 && tstest test/test.en16931-lineunit.node.ts --verbose --logfile --timeout 60",
70
+ "test:unit": "tstest test/test.cii-xrechnung.node.ts --verbose --logfile --timeout 60 && tstest test/test.einvoice.ts --verbose --logfile --timeout 60 && tstest test/test.decimal.ts --verbose --logfile --timeout 60 && tstest test/test.decimal-currency-calculator.ts --verbose --logfile --timeout 60 && tstest test/test.currency-utils.ts --verbose --logfile --timeout 60 && tstest test/test.en16931-validators.ts --verbose --logfile --timeout 60 && tstest test/test.schematron-validator.ts --verbose --logfile --timeout 60 && tstest test/test.xrechnung-creditnote.node.ts --verbose --logfile --timeout 60 && tstest test/test.accountingdoc-types.node.ts --verbose --logfile --timeout 60 && tstest test/test.corrected-invoice.node.ts --verbose --logfile --timeout 60 && tstest test/test.xrechnung-3-due-date.node.ts --verbose --logfile --timeout 60 && tstest test/test.calendar-dates.node.ts --verbose --logfile --timeout 60 && tstest test/test.document-totals.node.ts --verbose --logfile --timeout 60 && tstest test/test.reverse-charge.node.ts --verbose --logfile --timeout 60 && tstest test/test.cii-schema-order.node.ts --verbose --logfile --timeout 60 && tstest test/test.en16931-unitcodes.node.ts --verbose --logfile --timeout 60 && tstest test/test.en16931-lineunit.node.ts --verbose --logfile --timeout 60",
71
71
  "test:format": "tstest test/test.format-detection.ts --verbose --logfile --timeout 60",
72
72
  "test:corpus": "tstest test/suite/einvoice_corpus-validation/ --verbose --logfile --timeout 60",
73
73
  "test:performance": "NODE_OPTIONS=--expose-gc tstest test/suite/einvoice_performance/ --verbose --logfile --timeout 60",
package/readme.md CHANGED
@@ -343,6 +343,31 @@ attachment name and associated-file relationship. It does not convert an
343
343
  arbitrary source PDF to PDF/A-3; the source PDF must already satisfy the
344
344
  required archival profile.
345
345
 
346
+ ## Reverse charge
347
+
348
+ An envelope with `reverseCharge: true` is a reverse charge document: the
349
+ recipient owes the tax (§ 13b UStG). Every line and the VAT breakdown are
350
+ written with the VAT category `AE` at the rate 0 and a tax amount of 0, and the
351
+ breakdown states the exemption reason code `VATEX-EU-AE` and the text
352
+ "Steuerschuldnerschaft des Leistungsempfängers" (§ 14a Abs. 5 UStG, Abschnitt
353
+ 13b.14 Abs. 1 UStAE) when the document's `language` starts with `de`, and
354
+ "Reverse charge", the wording Abschnitt 14a.1 Abs. 6 Satz 2 UStAE admits, for
355
+ any other. `EInvoice.language` defaults to `'en'`: set it to `'de'` for the
356
+ German wording.
357
+
358
+ Export refuses a reverse charge document, naming the rule, when a line states a
359
+ VAT rate other than 0 (BR-AE-05), the seller has no VAT identifier, or the
360
+ buyer has neither a VAT identifier nor a legal registration identifier
361
+ (`registrationDetails.registrationId`, BR-AE-02). On the seller side BR-AE-02
362
+ would also accept a tax registration identifier (BT-32, the Steuernummer) or a
363
+ tax representative's VAT identifier (BT-63); the envelope can state neither, so
364
+ a domestic § 13b issuer that has only a Steuernummer (which § 14 Abs. 4 Satz 1
365
+ Nr. 2 UStG allows) is refused until the envelope carries a tax number.
366
+
367
+ The envelope states reverse charge for the whole document; an item has no VAT
368
+ category of its own. A document mixing reverse charge lines with standard rated
369
+ ones cannot be expressed until `@tsclass/tsclass` gives items a category.
370
+
346
371
  ## Calendar dates
347
372
 
348
373
  A calendar date of the invoice (issue date, due date, delivery date, invoicing
@@ -3,6 +3,6 @@
3
3
  */
4
4
  export const commitinfo = {
5
5
  name: '@fin.cx/einvoice',
6
- version: '8.2.2',
6
+ version: '8.2.4',
7
7
  description: 'A TypeScript module for creating, manipulating, and embedding XML data within PDF files specifically tailored for electronic invoice (einvoice) packages.'
8
8
  }
@@ -158,6 +158,20 @@ export abstract class CIIBaseDecoder extends BaseDecoder {
158
158
  return dueDate === undefined ? absentDueInDays : Math.round((dueDate - issueDate) / (1000 * 60 * 60 * 24));
159
159
  }
160
160
 
161
+ /**
162
+ * Whether every line of the document has the VAT category reverse charge (AE, BT-151); the
163
+ * envelope states it for the whole document, so a document mixing AE lines with others
164
+ * cannot say it
165
+ */
166
+ protected isReverseChargeDocument(): boolean {
167
+ const categories = this.select(
168
+ '/rsm:CrossIndustryInvoice/rsm:SupplyChainTradeTransaction/ram:IncludedSupplyChainTradeLineItem/ram:SpecifiedLineTradeSettlement/ram:ApplicableTradeTax/ram:CategoryCode',
169
+ this.doc,
170
+ );
171
+ const codes = (Array.isArray(categories) ? categories : []).map((node) => ((node as Node).textContent ?? '').trim());
172
+ return codes.length > 0 && codes.every((code) => code === 'AE');
173
+ }
174
+
161
175
  /**
162
176
  * Reads the actual delivery date (BT-72) of the header delivery; undefined
163
177
  * when the document does not state one.
@@ -3,7 +3,8 @@ import type { TAccountingDoc, TCreditNote, TInvoiceDocument } from '../../interf
3
3
  import { EInvoiceFormatError } from '../../errors.js';
4
4
  import { getPrecedingInvoiceReferences } from '../utils/preceding.invoice.js';
5
5
  import { getWritableDate } from '../utils/date.value.js';
6
- import { CII_NAMESPACES, CIIProfile } from './cii.types.js';
6
+ import { assertVatCategoryWritable } from '../utils/vat.category.js';
7
+ import { CII_CHILD_SEQUENCES, CII_NAMESPACES, CIIProfile } from './cii.types.js';
7
8
 
8
9
  /**
9
10
  * Base encoder for CII-based invoice formats
@@ -25,6 +26,8 @@ export abstract class CIIBaseEncoder extends BaseEncoder {
25
26
  * @returns CII XML string
26
27
  */
27
28
  public async encode(invoice: TAccountingDoc): Promise<string> {
29
+ // a reverse charge document that lacks what EN 16931 requires of one is refused (BR-AE-02, BR-AE-05)
30
+ assertVatCategoryWritable(invoice, 'cii');
28
31
  // MINIMUM has no preceding invoice reference (BG-3): a reference the document states cannot be written there
29
32
  if (this.profile === CIIProfile.MINIMUM && getPrecedingInvoiceReferences(invoice).length > 0) {
30
33
  throw new EInvoiceFormatError(
@@ -110,6 +113,44 @@ export abstract class CIIBaseEncoder extends BaseEncoder {
110
113
  }
111
114
  }
112
115
 
116
+ /**
117
+ * Puts the children of every CII element into the order the schema requires
118
+ * (`CII_CHILD_SEQUENCES`). The encoders add elements in several passes; this
119
+ * final pass makes the document follow the schema sequences. The sort is
120
+ * stable, so repeated elements (notes, lines, VAT breakdowns) keep their
121
+ * order, and an element a sequence does not name stays behind the element it
122
+ * followed.
123
+ * @param doc XML document
124
+ */
125
+ protected orderCiiElements(doc: Document): void {
126
+ const localName = (element: Element) => element.nodeName.slice(element.nodeName.indexOf(':') + 1);
127
+ const order = (element: Element): void => {
128
+ const children: Element[] = [];
129
+ for (let node = element.firstChild; node; node = node.nextSibling) {
130
+ if (node.nodeType === 1) {
131
+ children.push(node as Element);
132
+ }
133
+ }
134
+ const sequence = CII_CHILD_SEQUENCES[localName(element)];
135
+ if (sequence) {
136
+ let lastRank = -1;
137
+ const ranked = children.map((child, position) => {
138
+ const rank = sequence.indexOf(localName(child));
139
+ lastRank = rank === -1 ? lastRank : rank;
140
+ return { child, rank: lastRank, position };
141
+ });
142
+ ranked.sort((a, b) => a.rank - b.rank || a.position - b.position);
143
+ for (const { child } of ranked) {
144
+ element.appendChild(child);
145
+ }
146
+ }
147
+ for (const child of children) {
148
+ order(child);
149
+ }
150
+ };
151
+ order(doc.documentElement);
152
+ }
153
+
113
154
  /**
114
155
  * Formats a date as an ISO string (YYYY-MM-DD, UTC). A value that is no
115
156
  * valid timestamp is refused with an `EInvoiceFormatError` naming the field.
@@ -43,3 +43,44 @@ export const CII_PROFILE_IDS = {
43
43
  ZUGFERD_V1_COMFORT: 'urn:ferd:CrossIndustryDocument:invoice:1p0:comfort',
44
44
  ZUGFERD_V1_EXTENDED: 'urn:ferd:CrossIndustryDocument:invoice:1p0:extended'
45
45
  };
46
+
47
+ /**
48
+ * The children of the CII elements the encoders write, in the order the schema
49
+ * requires them: the sequences of the UN/CEFACT Cross Industry Invoice D16B
50
+ * schema as the EN 16931 validation artefacts ship it
51
+ * (`test/assets/eInvoicing-EN16931/cii/schema/D16B SCRDM (Subset)/uncoupled clm/CII/uncefact/data/standard/`,
52
+ * `CrossIndustryInvoice_100pD16B.xsd` and
53
+ * `CrossIndustryInvoice_ReusableAggregateBusinessInformationEntity_100pD16B.xsd`),
54
+ * keyed by the local name of the parent element. The Factur-X 1.07.3 and
55
+ * ZUGFeRD 2 schemas are subsets of it with the same order.
56
+ */
57
+ export const CII_CHILD_SEQUENCES: Readonly<Record<string, readonly string[]>> = {
58
+ CrossIndustryInvoice: ['ExchangedDocumentContext', 'ExchangedDocument', 'SupplyChainTradeTransaction', 'ValuationBreakdownStatement'],
59
+ ExchangedDocument: ['ID', 'Name', 'TypeCode', 'IssueDateTime', 'CopyIndicator', 'Purpose', 'ControlRequirementIndicator', 'LanguageID', 'PurposeCode', 'RevisionDateTime', 'VersionID', 'GlobalID', 'RevisionID', 'PreviousRevisionID', 'CategoryCode', 'IncludedNote', 'EffectiveSpecifiedPeriod', 'IssuerTradeParty'],
60
+ SupplyChainTradeTransaction: ['IncludedSupplyChainTradeLineItem', 'ApplicableHeaderTradeAgreement', 'ApplicableHeaderTradeDelivery', 'ApplicableHeaderTradeSettlement'],
61
+ ApplicableHeaderTradeAgreement: ['Reference', 'BuyerReference', 'SellerTradeParty', 'BuyerTradeParty', 'SalesAgentTradeParty', 'BuyerRequisitionerTradeParty', 'BuyerAssignedAccountantTradeParty', 'SellerAssignedAccountantTradeParty', 'BuyerTaxRepresentativeTradeParty', 'SellerTaxRepresentativeTradeParty', 'ProductEndUserTradeParty', 'ApplicableTradeDeliveryTerms', 'SellerOrderReferencedDocument', 'BuyerOrderReferencedDocument', 'QuotationReferencedDocument', 'OrderResponseReferencedDocument', 'ContractReferencedDocument', 'DemandForecastReferencedDocument', 'SupplyInstructionReferencedDocument', 'PromotionalDealReferencedDocument', 'PriceListReferencedDocument', 'AdditionalReferencedDocument', 'RequisitionerReferencedDocument', 'BuyerAgentTradeParty', 'PurchaseConditionsReferencedDocument', 'SpecifiedProcuringProject', 'UltimateCustomerOrderReferencedDocument'],
62
+ SellerTradeParty: ['ID', 'GlobalID', 'Name', 'RoleCode', 'Description', 'SpecifiedLegalOrganization', 'DefinedTradeContact', 'PostalTradeAddress', 'URIUniversalCommunication', 'SpecifiedTaxRegistration', 'EndPointURIUniversalCommunication', 'LogoAssociatedSpecifiedBinaryFile'],
63
+ BuyerTradeParty: ['ID', 'GlobalID', 'Name', 'RoleCode', 'Description', 'SpecifiedLegalOrganization', 'DefinedTradeContact', 'PostalTradeAddress', 'URIUniversalCommunication', 'SpecifiedTaxRegistration', 'EndPointURIUniversalCommunication', 'LogoAssociatedSpecifiedBinaryFile'],
64
+ PostalTradeAddress: ['ID', 'PostcodeCode', 'PostOfficeBox', 'BuildingName', 'LineOne', 'LineTwo', 'LineThree', 'LineFour', 'LineFive', 'StreetName', 'CityName', 'CitySubDivisionName', 'CountryID', 'CountryName', 'CountrySubDivisionID', 'CountrySubDivisionName', 'AttentionOf', 'CareOf', 'BuildingNumber', 'DepartmentName', 'AdditionalStreetName'],
65
+ DefinedTradeContact: ['ID', 'PersonName', 'DepartmentName', 'TypeCode', 'JobTitle', 'Responsibility', 'PersonID', 'TelephoneUniversalCommunication', 'DirectTelephoneUniversalCommunication', 'MobileTelephoneUniversalCommunication', 'FaxUniversalCommunication', 'EmailURIUniversalCommunication', 'TelexUniversalCommunication', 'VOIPUniversalCommunication', 'InstantMessagingUniversalCommunication', 'SpecifiedNote', 'SpecifiedContactPerson'],
66
+ SpecifiedLegalOrganization: ['LegalClassificationCode', 'Name', 'ID', 'TradingBusinessName', 'PostalTradeAddress', 'AuthorizedLegalRegistration'],
67
+ ApplicableHeaderTradeDelivery: ['RelatedSupplyChainConsignment', 'ShipToTradeParty', 'UltimateShipToTradeParty', 'ShipFromTradeParty', 'ActualDespatchSupplyChainEvent', 'ActualPickUpSupplyChainEvent', 'ActualDeliverySupplyChainEvent', 'ActualReceiptSupplyChainEvent', 'AdditionalReferencedDocument', 'DespatchAdviceReferencedDocument', 'ReceivingAdviceReferencedDocument', 'DeliveryNoteReferencedDocument', 'ConsumptionReportReferencedDocument', 'PreviousDeliverySupplyChainEvent', 'PackingListReferencedDocument'],
68
+ ActualDeliverySupplyChainEvent: ['ID', 'OccurrenceDateTime', 'TypeCode', 'Description', 'DescriptionBinaryObject', 'UnitQuantity', 'LatestOccurrenceDateTime', 'EarliestOccurrenceDateTime', 'OccurrenceSpecifiedPeriod', 'OccurrenceLogisticsLocation'],
69
+ ApplicableHeaderTradeSettlement: ['DuePayableAmount', 'CreditorReferenceTypeCode', 'CreditorReferenceType', 'CreditorReferenceIssuerID', 'CreditorReferenceID', 'PaymentReference', 'TaxCurrencyCode', 'InvoiceCurrencyCode', 'PaymentCurrencyCode', 'InvoiceIssuerReference', 'InvoiceDateTime', 'NextInvoiceDateTime', 'CreditReasonCode', 'CreditReason', 'InvoicerTradeParty', 'InvoiceeTradeParty', 'PayeeTradeParty', 'PayerTradeParty', 'TaxApplicableTradeCurrencyExchange', 'InvoiceApplicableTradeCurrencyExchange', 'PaymentApplicableTradeCurrencyExchange', 'SpecifiedTradeSettlementPaymentMeans', 'ApplicableTradeTax', 'BillingSpecifiedPeriod', 'SpecifiedTradeAllowanceCharge', 'SubtotalCalculatedTradeTax', 'SpecifiedLogisticsServiceCharge', 'SpecifiedTradePaymentTerms', 'SpecifiedTradeSettlementHeaderMonetarySummation', 'SpecifiedFinancialAdjustment', 'InvoiceReferencedDocument', 'ProFormaInvoiceReferencedDocument', 'LetterOfCreditReferencedDocument', 'FactoringAgreementReferencedDocument', 'FactoringListReferencedDocument', 'PayableSpecifiedTradeAccountingAccount', 'ReceivableSpecifiedTradeAccountingAccount', 'PurchaseSpecifiedTradeAccountingAccount', 'SalesSpecifiedTradeAccountingAccount', 'SpecifiedTradeSettlementFinancialCard', 'SpecifiedAdvancePayment', 'UltimatePayeeTradeParty'],
70
+ SpecifiedTradeSettlementPaymentMeans: ['PaymentChannelCode', 'TypeCode', 'GuaranteeMethodCode', 'PaymentMethodCode', 'Information', 'ID', 'ApplicableTradeSettlementFinancialCard', 'PayerPartyDebtorFinancialAccount', 'PayeePartyCreditorFinancialAccount', 'PayerSpecifiedDebtorFinancialInstitution', 'PayeeSpecifiedCreditorFinancialInstitution'],
71
+ PayeePartyCreditorFinancialAccount: ['IBANID', 'AccountName', 'ProprietaryID'],
72
+ ApplicableTradeTax: ['CalculatedAmount', 'TypeCode', 'ExemptionReason', 'CalculatedRate', 'CalculationSequenceNumeric', 'BasisQuantity', 'BasisAmount', 'UnitBasisAmount', 'LineTotalBasisAmount', 'AllowanceChargeBasisAmount', 'CategoryCode', 'CurrencyCode', 'Jurisdiction', 'CustomsDutyIndicator', 'ExemptionReasonCode', 'TaxBasisAllowanceRate', 'TaxPointDate', 'Type', 'InformationAmount', 'CategoryName', 'DueDateTypeCode', 'RateApplicablePercent', 'SpecifiedTradeAccountingAccount', 'ServiceSupplyTradeCountry', 'BuyerRepayableTaxSpecifiedTradeAccountingAccount', 'SellerPayableTaxSpecifiedTradeAccountingAccount', 'SellerRefundableTaxSpecifiedTradeAccountingAccount', 'BuyerDeductibleTaxSpecifiedTradeAccountingAccount', 'BuyerNonDeductibleTaxSpecifiedTradeAccountingAccount', 'PlaceApplicableTradeLocation'],
73
+ BillingSpecifiedPeriod: ['DurationMeasure', 'InclusiveIndicator', 'Description', 'StartDateTime', 'EndDateTime', 'CompleteDateTime', 'OpenIndicator', 'SeasonCode', 'ID', 'Name', 'SequenceNumeric', 'StartDateFlexibilityCode', 'ContinuousIndicator', 'PurposeCode'],
74
+ SpecifiedTradePaymentTerms: ['ID', 'FromEventCode', 'SettlementPeriodMeasure', 'Description', 'DueDateDateTime', 'TypeCode', 'InstructionTypeCode', 'DirectDebitMandateID', 'PartialPaymentPercent', 'PaymentMeansID', 'PartialPaymentAmount', 'ApplicableTradePaymentPenaltyTerms', 'ApplicableTradePaymentDiscountTerms', 'PayeeTradeParty'],
75
+ SpecifiedTradeSettlementHeaderMonetarySummation: ['LineTotalAmount', 'ChargeTotalAmount', 'AllowanceTotalAmount', 'TaxBasisTotalAmount', 'TaxTotalAmount', 'RoundingAmount', 'GrandTotalAmount', 'InformationAmount', 'TotalPrepaidAmount', 'TotalDiscountAmount', 'TotalAllowanceChargeAmount', 'DuePayableAmount', 'RetailValueExcludingTaxInformationAmount', 'TotalDepositFeeInformationAmount', 'ProductValueExcludingTobaccoTaxInformationAmount', 'TotalRetailValueInformationAmount', 'GrossLineTotalAmount', 'NetLineTotalAmount', 'NetIncludingTaxesLineTotalAmount'],
76
+ InvoiceReferencedDocument: ['IssuerAssignedID', 'URIID', 'StatusCode', 'CopyIndicator', 'LineID', 'TypeCode', 'GlobalID', 'RevisionID', 'Name', 'AttachmentBinaryObject', 'Information', 'ReferenceTypeCode', 'SectionName', 'PreviousRevisionID', 'FormattedIssueDateTime', 'EffectiveSpecifiedPeriod', 'IssuerTradeParty', 'AttachedSpecifiedBinaryFile'],
77
+ IncludedSupplyChainTradeLineItem: ['DescriptionCode', 'AssociatedDocumentLineDocument', 'SpecifiedTradeProduct', 'SpecifiedLineTradeAgreement', 'SpecifiedLineTradeDelivery', 'SpecifiedLineTradeSettlement', 'IncludedSubordinateTradeLineItem'],
78
+ AssociatedDocumentLineDocument: ['LineID', 'ParentLineID', 'LineStatusCode', 'LineStatusReasonCode', 'IncludedNote'],
79
+ SpecifiedTradeProduct: ['ID', 'GlobalID', 'SellerAssignedID', 'BuyerAssignedID', 'ManufacturerAssignedID', 'Name', 'TradeName', 'Description', 'TypeCode', 'NetWeightMeasure', 'GrossWeightMeasure', 'ProductGroupID', 'EndItemTypeCode', 'EndItemName', 'AreaDensityMeasure', 'UseDescription', 'BrandName', 'SubBrandName', 'DrainedNetWeightMeasure', 'VariableMeasureIndicator', 'ColourCode', 'ColourDescription', 'Designation', 'FormattedCancellationAnnouncedLaunchDateTime', 'FormattedLatestProductDataChangeDateTime', 'ApplicableProductCharacteristic', 'ApplicableMaterialGoodsCharacteristic', 'DesignatedProductClassification', 'IndividualTradeProductInstance', 'CertificationEvidenceReferenceReferencedDocument', 'InspectionReferenceReferencedDocument', 'OriginTradeCountry', 'LinearSpatialDimension', 'MinimumLinearSpatialDimension', 'MaximumLinearSpatialDimension', 'ManufacturerTradeParty', 'PresentationSpecifiedBinaryFile', 'MSDSReferenceReferencedDocument', 'AdditionalReferenceReferencedDocument', 'LegalRightsOwnerTradeParty', 'BrandOwnerTradeParty', 'IncludedReferencedProduct', 'InformationNote'],
80
+ SpecifiedLineTradeAgreement: ['BuyerReference', 'BuyerRequisitionerTradeParty', 'ApplicableTradeDeliveryTerms', 'SellerOrderReferencedDocument', 'BuyerOrderReferencedDocument', 'QuotationReferencedDocument', 'ContractReferencedDocument', 'DemandForecastReferencedDocument', 'PromotionalDealReferencedDocument', 'AdditionalReferencedDocument', 'GrossPriceProductTradePrice', 'NetPriceProductTradePrice', 'RequisitionerReferencedDocument', 'ItemSellerTradeParty', 'ItemBuyerTradeParty', 'IncludedSpecifiedMarketplace', 'UltimateCustomerOrderReferencedDocument'],
81
+ NetPriceProductTradePrice: ['TypeCode', 'ChargeAmount', 'BasisQuantity', 'MinimumQuantity', 'MaximumQuantity', 'ChangeReason', 'OrderUnitConversionFactorNumeric', 'AppliedTradeAllowanceCharge', 'ValiditySpecifiedPeriod', 'IncludedTradeTax', 'DeliveryTradeLocation', 'TradeComparisonReferencePrice', 'AssociatedReferencedDocument'],
82
+ SpecifiedLineTradeDelivery: ['RequestedQuantity', 'ReceivedQuantity', 'BilledQuantity', 'ChargeFreeQuantity', 'PackageQuantity', 'ProductUnitQuantity', 'PerPackageUnitQuantity', 'NetWeightMeasure', 'GrossWeightMeasure', 'TheoreticalWeightMeasure', 'DespatchedQuantity', 'SpecifiedDeliveryAdjustment', 'IncludedSupplyChainPackaging', 'RelatedSupplyChainConsignment', 'ShipToTradeParty', 'UltimateShipToTradeParty', 'ShipFromTradeParty', 'ActualDespatchSupplyChainEvent', 'ActualPickUpSupplyChainEvent', 'RequestedDeliverySupplyChainEvent', 'ActualDeliverySupplyChainEvent', 'ActualReceiptSupplyChainEvent', 'AdditionalReferencedDocument', 'DespatchAdviceReferencedDocument', 'ReceivingAdviceReferencedDocument', 'DeliveryNoteReferencedDocument', 'ConsumptionReportReferencedDocument', 'PackingListReferencedDocument'],
83
+ SpecifiedLineTradeSettlement: ['PaymentReference', 'InvoiceIssuerReference', 'TotalAdjustmentAmount', 'DiscountIndicator', 'ApplicableTradeTax', 'BillingSpecifiedPeriod', 'SpecifiedTradeAllowanceCharge', 'SubtotalCalculatedTradeTax', 'SpecifiedLogisticsServiceCharge', 'SpecifiedTradePaymentTerms', 'SpecifiedTradeSettlementLineMonetarySummation', 'SpecifiedFinancialAdjustment', 'InvoiceReferencedDocument', 'AdditionalReferencedDocument', 'PayableSpecifiedTradeAccountingAccount', 'ReceivableSpecifiedTradeAccountingAccount', 'PurchaseSpecifiedTradeAccountingAccount', 'SalesSpecifiedTradeAccountingAccount', 'SpecifiedTradeSettlementFinancialCard'],
84
+ SpecifiedTradeSettlementLineMonetarySummation: ['LineTotalAmount', 'ChargeTotalAmount', 'AllowanceTotalAmount', 'TaxBasisTotalAmount', 'TaxTotalAmount', 'GrandTotalAmount', 'InformationAmount', 'TotalAllowanceChargeAmount', 'TotalRetailValueInformationAmount', 'GrossLineTotalAmount', 'NetLineTotalAmount', 'NetIncludingTaxesLineTotalAmount', 'ProductWeightLossInformationAmount'],
85
+ IncludedNote: ['Subject', 'ContentCode', 'Content', 'SubjectCode', 'ID'],
86
+ };
@@ -94,8 +94,8 @@ export class FacturXDecoder extends CIIBaseDecoder {
94
94
  // Extract the actual delivery date, if stated
95
95
  const deliveryDate = this.extractDeliveryDate();
96
96
 
97
- // Check for reverse charge
98
- const reverseCharge = this.exists('//ram:SpecifiedTradeAllowanceCharge/ram:ReasonCode[text()="62"]');
97
+ // Reverse charge: every line has the VAT category AE
98
+ const reverseCharge = this.isReverseChargeDocument();
99
99
 
100
100
  // Create the common invoice data
101
101
  const invoiceData = {
@@ -158,7 +158,12 @@ export class FacturXDecoder extends CIIBaseDecoder {
158
158
  const vatId = this.getText(`${partyXPath}/ram:SpecifiedTaxRegistration/ram:ID[@schemeID="VA"]`) || '';
159
159
 
160
160
  // Extract registration ID
161
- const registrationId = this.getText(`${partyXPath}/ram:SpecifiedTaxRegistration/ram:ID[@schemeID="FC"]`) || '';
161
+ // the legal registration identifier (BT-30, BT-47); a document that states only a tax
162
+ // number (scheme FC, BT-32) keeps it here as before, the envelope has no field of its own for it
163
+ const registrationId =
164
+ this.getText(`${partyXPath}/ram:SpecifiedLegalOrganization/ram:ID`) ||
165
+ this.getText(`${partyXPath}/ram:SpecifiedTaxRegistration/ram:ID[@schemeID="FC"]`) ||
166
+ '';
162
167
 
163
168
  // Create contact object
164
169
  return {
@@ -4,6 +4,7 @@ import { FACTURX_PROFILE_IDS } from './facturx.types.js';
4
4
  import { DOMParser, XMLSerializer } from '../../../plugins.js';
5
5
  import { getDocumentTypeCode } from '../../utils/document.typecode.js';
6
6
  import { computeDocumentTotals } from '../../utils/document.totals.js';
7
+ import { toPlainDecimalString } from '../../utils/number.text.js';
7
8
  import { getWritableDate, getWritableDueDate } from '../../utils/date.value.js';
8
9
 
9
10
  /**
@@ -26,6 +27,9 @@ export class FacturXEncoder extends CIIBaseEncoder {
26
27
  this.addCommonInvoiceData(xmlDoc, creditNote);
27
28
 
28
29
  // Serialize to string
30
+ // the schema order of every element, whatever order the passes above added them in
31
+ this.orderCiiElements(xmlDoc);
32
+
29
33
  return new XMLSerializer().serializeToString(xmlDoc);
30
34
  }
31
35
 
@@ -45,6 +49,9 @@ export class FacturXEncoder extends CIIBaseEncoder {
45
49
  this.addCommonInvoiceData(xmlDoc, invoice);
46
50
 
47
51
  // Serialize to string
52
+ // the schema order of every element, whatever order the passes above added them in
53
+ this.orderCiiElements(xmlDoc);
54
+
48
55
  return new XMLSerializer().serializeToString(xmlDoc);
49
56
  }
50
57
 
@@ -223,6 +230,17 @@ export class FacturXEncoder extends CIIBaseEncoder {
223
230
  nameElement.textContent = party.name;
224
231
  partyElement.appendChild(nameElement);
225
232
 
233
+ // Legal registration identifier (BT-30 seller, BT-47 buyer), e.g. the commercial register
234
+ // number; in the schema order it follows the name. It is no tax registration: the scheme
235
+ // FC of ram:SpecifiedTaxRegistration is the seller's tax number (BT-32)
236
+ if (party.registrationDetails && party.registrationDetails.registrationId) {
237
+ const legalOrganizationElement = doc.createElement('ram:SpecifiedLegalOrganization');
238
+ const legalIdElement = doc.createElement('ram:ID');
239
+ legalIdElement.textContent = party.registrationDetails.registrationId;
240
+ legalOrganizationElement.appendChild(legalIdElement);
241
+ partyElement.appendChild(legalOrganizationElement);
242
+ }
243
+
226
244
  // Add postal address
227
245
  const addressElement = doc.createElement('ram:PostalTradeAddress');
228
246
 
@@ -263,15 +281,6 @@ export class FacturXEncoder extends CIIBaseEncoder {
263
281
  partyElement.appendChild(taxRegistrationElement);
264
282
  }
265
283
 
266
- // Add registration ID if available
267
- if (party.registrationDetails && party.registrationDetails.registrationId) {
268
- const regRegistrationElement = doc.createElement('ram:SpecifiedTaxRegistration');
269
- const regIdElement = doc.createElement('ram:ID');
270
- regIdElement.setAttribute('schemeID', 'FC');
271
- regIdElement.textContent = party.registrationDetails.registrationId;
272
- regRegistrationElement.appendChild(regIdElement);
273
- partyElement.appendChild(regRegistrationElement);
274
- }
275
284
  }
276
285
 
277
286
  /**
@@ -317,9 +326,9 @@ export class FacturXEncoder extends CIIBaseEncoder {
317
326
  // the EN 16931 totals in decimal arithmetic, rounded as the business rules check them
318
327
  const totals = computeDocumentTotals(invoice);
319
328
 
320
- // VAT breakdown (BG-23), one per rate, before the payment terms as the CII schema orders them;
321
- // the category is the one the lines carry
322
- for (const { rate, taxableAmount, taxAmount } of totals.vatGroups) {
329
+ // VAT breakdown (BG-23), one per VAT category and rate, before the payment terms as the CII
330
+ // schema orders them
331
+ for (const { category, rate, taxableAmount, taxAmount, exemption } of totals.vatGroups) {
323
332
  const taxElement = doc.createElement('ram:ApplicableTradeTax');
324
333
  const appendText = (name: string, text: string) => {
325
334
  const element = doc.createElement(name);
@@ -328,8 +337,14 @@ export class FacturXEncoder extends CIIBaseEncoder {
328
337
  };
329
338
  appendText('ram:CalculatedAmount', taxAmount.toFixed(totals.minorUnits)); // BT-117
330
339
  appendText('ram:TypeCode', 'VAT');
340
+ if (exemption) {
341
+ appendText('ram:ExemptionReason', exemption.reason); // BT-120 (BR-AE-10)
342
+ }
331
343
  appendText('ram:BasisAmount', taxableAmount.toFixed(totals.minorUnits)); // BT-116
332
- appendText('ram:CategoryCode', 'S'); // BT-118
344
+ appendText('ram:CategoryCode', category); // BT-118
345
+ if (exemption) {
346
+ appendText('ram:ExemptionReasonCode', exemption.code); // BT-121
347
+ }
333
348
  appendText('ram:RateApplicablePercent', rate.toString()); // BT-119
334
349
  settlementElement.appendChild(taxElement);
335
350
  }
@@ -414,7 +429,8 @@ export class FacturXEncoder extends CIIBaseEncoder {
414
429
  const agreementElement = doc.createElement('ram:SpecifiedLineTradeAgreement');
415
430
  const priceElement = doc.createElement('ram:NetPriceProductTradePrice');
416
431
  const chargeAmountElement = doc.createElement('ram:ChargeAmount');
417
- chargeAmountElement.textContent = item.unitNetPrice.toFixed(2);
432
+ // Item net price (BT-146) with the precision it has; EN 16931 does not limit its decimals
433
+ chargeAmountElement.textContent = toPlainDecimalString(item.unitNetPrice);
418
434
  priceElement.appendChild(chargeAmountElement);
419
435
  agreementElement.appendChild(priceElement);
420
436
  lineItemElement.appendChild(agreementElement);
@@ -422,7 +438,7 @@ export class FacturXEncoder extends CIIBaseEncoder {
422
438
  // Add delivery information (quantity)
423
439
  const deliveryElement = doc.createElement('ram:SpecifiedLineTradeDelivery');
424
440
  const quantityElement = doc.createElement('ram:BilledQuantity');
425
- quantityElement.textContent = item.unitQuantity.toString();
441
+ quantityElement.textContent = toPlainDecimalString(item.unitQuantity); // BT-129
426
442
  // a line without a unit is written without one, so that BR-23 reports it
427
443
  if (item.unitType) {
428
444
  quantityElement.setAttribute('unitCode', item.unitType);
@@ -443,7 +459,7 @@ export class FacturXEncoder extends CIIBaseEncoder {
443
459
 
444
460
  // Add tax category code
445
461
  const taxCategoryCodeElement = doc.createElement('ram:CategoryCode');
446
- taxCategoryCodeElement.textContent = 'S';
462
+ taxCategoryCodeElement.textContent = totals.lineVatCategories[index]; // BT-151
447
463
  taxElement.appendChild(taxCategoryCodeElement);
448
464
 
449
465
  // Add tax rate