@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.
- package/dist_ts/00_commitinfo_data.js +1 -1
- package/dist_ts/formats/cii/cii.decoder.d.ts +6 -0
- package/dist_ts/formats/cii/cii.decoder.js +11 -1
- package/dist_ts/formats/cii/cii.encoder.d.ts +10 -0
- package/dist_ts/formats/cii/cii.encoder.js +42 -2
- package/dist_ts/formats/cii/cii.types.d.ts +11 -0
- package/dist_ts/formats/cii/cii.types.js +41 -1
- package/dist_ts/formats/cii/facturx/facturx.decoder.js +8 -4
- package/dist_ts/formats/cii/facturx/facturx.encoder.js +30 -17
- package/dist_ts/formats/cii/zugferd/zugferd.decoder.js +8 -4
- package/dist_ts/formats/cii/zugferd/zugferd.encoder.js +63 -43
- package/dist_ts/formats/cii/zugferd/zugferd.v1.decoder.d.ts +5 -0
- package/dist_ts/formats/cii/zugferd/zugferd.v1.decoder.js +18 -5
- package/dist_ts/formats/ubl/generic/ubl.encoder.js +14 -15
- package/dist_ts/formats/ubl/ubl.encoder.js +4 -1
- package/dist_ts/formats/ubl/xrechnung/xrechnung.decoder.d.ts +4 -0
- package/dist_ts/formats/ubl/xrechnung/xrechnung.decoder.js +12 -2
- package/dist_ts/formats/utils/document.totals.d.ts +16 -6
- package/dist_ts/formats/utils/document.totals.js +29 -11
- package/dist_ts/formats/utils/number.text.d.ts +9 -0
- package/dist_ts/formats/utils/number.text.js +26 -0
- package/dist_ts/formats/utils/vat.category.d.ts +50 -0
- package/dist_ts/formats/utils/vat.category.js +64 -0
- package/package.json +2 -2
- package/readme.md +25 -0
- package/ts/00_commitinfo_data.ts +1 -1
- package/ts/formats/cii/cii.decoder.ts +14 -0
- package/ts/formats/cii/cii.encoder.ts +42 -1
- package/ts/formats/cii/cii.types.ts +41 -0
- package/ts/formats/cii/facturx/facturx.decoder.ts +8 -3
- package/ts/formats/cii/facturx/facturx.encoder.ts +32 -16
- package/ts/formats/cii/zugferd/zugferd.decoder.ts +8 -3
- package/ts/formats/cii/zugferd/zugferd.encoder.ts +71 -46
- package/ts/formats/cii/zugferd/zugferd.v1.decoder.ts +22 -4
- package/ts/formats/ubl/generic/ubl.encoder.ts +13 -15
- package/ts/formats/ubl/ubl.encoder.ts +3 -0
- package/ts/formats/ubl/xrechnung/xrechnung.decoder.ts +15 -1
- package/ts/formats/utils/document.totals.ts +43 -14
- package/ts/formats/utils/number.text.ts +25 -0
- 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
|
-
*
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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 = [...
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
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,
|
|
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.
|
|
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
|
package/ts/00_commitinfo_data.ts
CHANGED
|
@@ -3,6 +3,6 @@
|
|
|
3
3
|
*/
|
|
4
4
|
export const commitinfo = {
|
|
5
5
|
name: '@fin.cx/einvoice',
|
|
6
|
-
version: '8.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 {
|
|
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
|
-
//
|
|
98
|
-
const reverseCharge = this.
|
|
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
|
-
|
|
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
|
|
321
|
-
//
|
|
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',
|
|
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
|
-
|
|
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
|
|
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 =
|
|
462
|
+
taxCategoryCodeElement.textContent = totals.lineVatCategories[index]; // BT-151
|
|
447
463
|
taxElement.appendChild(taxCategoryCodeElement);
|
|
448
464
|
|
|
449
465
|
// Add tax rate
|