@fin.cx/einvoice 10.0.1 → 10.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist_ts/00_commitinfo_data.js +1 -1
- package/dist_ts/einvoice.d.ts +2 -0
- package/dist_ts/einvoice.js +7 -13
- package/dist_ts/formats/cii/cii.decoder.d.ts +11 -3
- package/dist_ts/formats/cii/cii.decoder.js +27 -8
- package/dist_ts/formats/cii/cii.encoder.d.ts +12 -0
- package/dist_ts/formats/cii/cii.encoder.js +36 -1
- package/dist_ts/formats/cii/cii.types.js +2 -1
- package/dist_ts/formats/cii/facturx/facturx.encoder.js +12 -8
- package/dist_ts/formats/cii/zugferd/zugferd.encoder.js +12 -8
- package/dist_ts/formats/semantic/semantic.adapter.d.ts +0 -4
- package/dist_ts/formats/semantic/semantic.adapter.js +7 -18
- package/dist_ts/formats/ubl/generic/ubl.encoder.d.ts +6 -1
- package/dist_ts/formats/ubl/generic/ubl.encoder.js +40 -8
- package/dist_ts/formats/ubl/ubl.decoder.d.ts +11 -0
- package/dist_ts/formats/ubl/ubl.decoder.js +30 -1
- package/dist_ts/formats/ubl/xrechnung/xrechnung.decoder.js +3 -3
- package/dist_ts/formats/utils/delivery.address.d.ts +18 -0
- package/dist_ts/formats/utils/delivery.address.js +30 -0
- package/dist_ts/formats/utils/document.totals.d.ts +5 -4
- package/dist_ts/formats/utils/document.totals.js +24 -8
- package/dist_ts/formats/utils/stated.values.js +4 -2
- package/dist_ts/formats/utils/vat.category.d.ts +146 -55
- package/dist_ts/formats/utils/vat.category.js +357 -69
- package/dist_ts/formats/validation/vat-categories.validator.d.ts +14 -15
- package/dist_ts/formats/validation/vat-categories.validator.js +123 -85
- package/package.json +3 -3
- package/readme.md +55 -19
- package/ts/00_commitinfo_data.ts +1 -1
- package/ts/einvoice.ts +8 -14
- package/ts/formats/cii/cii.decoder.ts +28 -7
- package/ts/formats/cii/cii.encoder.ts +36 -0
- package/ts/formats/cii/cii.types.ts +1 -0
- package/ts/formats/cii/facturx/facturx.encoder.ts +12 -7
- package/ts/formats/cii/zugferd/zugferd.encoder.ts +12 -7
- package/ts/formats/semantic/semantic.adapter.ts +6 -15
- package/ts/formats/ubl/generic/ubl.encoder.ts +40 -7
- package/ts/formats/ubl/ubl.decoder.ts +31 -0
- package/ts/formats/ubl/xrechnung/xrechnung.decoder.ts +2 -2
- package/ts/formats/utils/delivery.address.ts +36 -0
- package/ts/formats/utils/document.totals.ts +38 -11
- package/ts/formats/utils/stated.values.ts +4 -1
- package/ts/formats/utils/vat.category.ts +426 -95
- package/ts/formats/validation/vat-categories.validator.ts +169 -88
|
@@ -1,47 +1,152 @@
|
|
|
1
1
|
import type { TAccountingDoc } from '../../interfaces/common.js';
|
|
2
|
+
import type { finance } from '@tsclass/tsclass';
|
|
3
|
+
|
|
4
|
+
type TAccountingDocItem = finance.TAccountingDocItem;
|
|
2
5
|
import { EInvoiceFormatError } from '../../errors.js';
|
|
6
|
+
import type { IEInvoiceStatedValues } from '../../interfaces/stated.values.js';
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* The VAT category codes (UNTDID 5305, EN 16931 BT-118/BT-151) an item can
|
|
10
|
+
* state (`TAccountingDocItem.vatCategory`): standard rated (S), zero rated (Z),
|
|
11
|
+
* exempt (E), reverse charge (AE), intra-community supply (K), export outside
|
|
12
|
+
* the EU (G), not subject to VAT (O), IGIC (L) and IPSI (M).
|
|
13
|
+
*/
|
|
14
|
+
export type TVatCategoryCode = finance.TVatCategory;
|
|
15
|
+
|
|
16
|
+
/** Every VAT category code EN 16931 allows, in the order of `TVatCategoryCode` */
|
|
17
|
+
export const VAT_CATEGORY_CODES: readonly TVatCategoryCode[] = ['S', 'Z', 'E', 'AE', 'K', 'G', 'O', 'L', 'M'];
|
|
3
18
|
|
|
4
19
|
/**
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
* for the whole document (`reverseCharge`); an item carries no category of its
|
|
8
|
-
* own, so a document cannot mix reverse charge lines with standard rated ones.
|
|
20
|
+
* Whether a value is a VAT category code EN 16931 allows
|
|
21
|
+
* @param value The value
|
|
9
22
|
*/
|
|
10
|
-
export
|
|
23
|
+
export const isVatCategoryCode = (value: unknown): value is TVatCategoryCode =>
|
|
24
|
+
typeof value === 'string' && (VAT_CATEGORY_CODES as readonly string[]).includes(value);
|
|
11
25
|
|
|
12
|
-
/**
|
|
26
|
+
/**
|
|
27
|
+
* The categories whose VAT breakdown states an exemption reason, as a code
|
|
28
|
+
* (BT-121) or a text (BT-120): exempt (BR-E-10), reverse charge (BR-AE-10),
|
|
29
|
+
* intra-community supply (BR-IC-10), export (BR-G-10) and not subject to VAT
|
|
30
|
+
* (BR-O-10). The others state none (BR-S-10, BR-Z-10, BR-AF-10, BR-AG-10).
|
|
31
|
+
*/
|
|
32
|
+
export const VAT_CATEGORIES_WITH_EXEMPTION: readonly TVatCategoryCode[] = ['E', 'AE', 'K', 'G', 'O'];
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* The VAT exemption reason code (BT-121) and text (BT-120) a VAT breakdown
|
|
36
|
+
* states; at least one of them
|
|
37
|
+
*/
|
|
13
38
|
export interface IVatExemption {
|
|
14
|
-
code
|
|
15
|
-
reason
|
|
39
|
+
code?: string;
|
|
40
|
+
reason?: string;
|
|
16
41
|
}
|
|
17
42
|
|
|
18
43
|
/**
|
|
19
|
-
* The
|
|
20
|
-
*
|
|
44
|
+
* The CEF VATEX code list, as the CEN/TC 434 EN 16931 validation artefacts
|
|
45
|
+
* 1.3.16 check a VAT exemption reason code (BT-121) against it (BR-CL-22,
|
|
46
|
+
* compared in upper case)
|
|
47
|
+
*/
|
|
48
|
+
export const VATEX_CODES: ReadonlySet<string> = new Set(
|
|
49
|
+
(
|
|
50
|
+
'VATEX-EU-79-C VATEX-EU-132 VATEX-EU-132-1A VATEX-EU-132-1B VATEX-EU-132-1C VATEX-EU-132-1D VATEX-EU-132-1E ' +
|
|
51
|
+
'VATEX-EU-132-1F VATEX-EU-132-1G VATEX-EU-132-1H VATEX-EU-132-1I VATEX-EU-132-1J VATEX-EU-132-1K VATEX-EU-132-1L ' +
|
|
52
|
+
'VATEX-EU-132-1M VATEX-EU-132-1N VATEX-EU-132-1O VATEX-EU-132-1P VATEX-EU-132-1Q VATEX-EU-135-1 VATEX-EU-143 ' +
|
|
53
|
+
'VATEX-EU-143-1A VATEX-EU-143-1B VATEX-EU-143-1C VATEX-EU-143-1D VATEX-EU-143-1E VATEX-EU-143-1F VATEX-EU-143-1FA ' +
|
|
54
|
+
'VATEX-EU-143-1G VATEX-EU-143-1H VATEX-EU-143-1I VATEX-EU-143-1J VATEX-EU-143-1K VATEX-EU-143-1L VATEX-EU-144 ' +
|
|
55
|
+
'VATEX-EU-146-1E VATEX-EU-159 VATEX-EU-309 VATEX-EU-148 VATEX-EU-148-A VATEX-EU-148-B VATEX-EU-148-C VATEX-EU-148-D ' +
|
|
56
|
+
'VATEX-EU-148-E VATEX-EU-148-F VATEX-EU-148-G VATEX-EU-151 VATEX-EU-151-1A VATEX-EU-151-1AA VATEX-EU-151-1B ' +
|
|
57
|
+
'VATEX-EU-151-1C VATEX-EU-151-1D VATEX-EU-151-1E VATEX-EU-G VATEX-EU-O VATEX-EU-IC VATEX-EU-AE VATEX-EU-D VATEX-EU-F ' +
|
|
58
|
+
'VATEX-EU-I VATEX-EU-J VATEX-FR-FRANCHISE VATEX-FR-CNWVAT VATEX-EU-153 VATEX-FR-CGI261-1 VATEX-FR-CGI261-2 ' +
|
|
59
|
+
'VATEX-FR-CGI261-3 VATEX-FR-CGI261-4 VATEX-FR-CGI261-5 VATEX-FR-CGI261-7 VATEX-FR-CGI261-8 VATEX-FR-CGI261A ' +
|
|
60
|
+
'VATEX-FR-CGI261B VATEX-FR-CGI261C-1 VATEX-FR-CGI261C-2 VATEX-FR-CGI261C-3 VATEX-FR-CGI261D-1 VATEX-FR-CGI261D-1BIS ' +
|
|
61
|
+
'VATEX-FR-CGI261D-2 VATEX-FR-CGI261D-3 VATEX-FR-CGI261D-4 VATEX-FR-CGI261E-1 VATEX-FR-CGI261E-2 VATEX-FR-CGI277A ' +
|
|
62
|
+
'VATEX-FR-CGI275 VATEX-FR-298SEXDECIESA VATEX-FR-CGI295 VATEX-FR-AE'
|
|
63
|
+
).split(' '),
|
|
64
|
+
);
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Whether a VAT exemption reason code is on the CEF VATEX code list (BR-CL-22)
|
|
68
|
+
* @param code The code
|
|
69
|
+
*/
|
|
70
|
+
export const isVatexCode = (code: string): boolean => VATEX_CODES.has(code.trim().toUpperCase());
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* The VAT category of an item: the one it states (`vatCategory`, BT-151), else
|
|
74
|
+
* reverse charge (AE) when the document states `reverseCharge`, standard rated
|
|
75
|
+
* (S) otherwise.
|
|
21
76
|
* @param accountingDoc The document
|
|
77
|
+
* @param item The item
|
|
22
78
|
*/
|
|
23
|
-
export const
|
|
24
|
-
accountingDoc
|
|
79
|
+
export const getItemVatCategory = (
|
|
80
|
+
accountingDoc: { reverseCharge?: boolean },
|
|
81
|
+
item: Pick<TAccountingDocItem, 'vatCategory'>,
|
|
82
|
+
): TVatCategoryCode => item.vatCategory ?? (accountingDoc.reverseCharge ? 'AE' : 'S');
|
|
25
83
|
|
|
26
84
|
/**
|
|
27
|
-
* The exemption reason
|
|
28
|
-
*
|
|
29
|
-
*
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
85
|
+
* The exemption reason an item states: its VATEX code (BT-121) and its text
|
|
86
|
+
* (BT-120), each trimmed; undefined when it states neither
|
|
87
|
+
* @param item The item
|
|
88
|
+
*/
|
|
89
|
+
export const getItemVatExemption = (
|
|
90
|
+
item: Pick<TAccountingDocItem, 'vatExemptionReason' | 'vatExemptionReasonCode'>,
|
|
91
|
+
): IVatExemption | undefined => {
|
|
92
|
+
const code = item.vatExemptionReasonCode?.trim();
|
|
93
|
+
const reason = item.vatExemptionReason?.trim();
|
|
94
|
+
if (!code && !reason) {
|
|
95
|
+
return undefined;
|
|
96
|
+
}
|
|
97
|
+
return { ...(code ? { code } : {}), ...(reason ? { reason } : {}) };
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* The exemption reason a VAT breakdown of a category states when none of its
|
|
102
|
+
* items states one.
|
|
103
|
+
* - Reverse charge (AE): the code `VATEX-EU-AE` and the wording the law
|
|
104
|
+
* prescribes, "Steuerschuldnerschaft des Leistungsempfängers" (§ 14a Abs. 1
|
|
105
|
+
* and 5 UStG). A document in another language may use the wording of
|
|
106
|
+
* Article 226 Nr. 11a of the VAT Directive in that language, "Reverse charge"
|
|
107
|
+
* in English (Abschnitt 14a.1 Abs. 6 Satz 2 UStAE).
|
|
108
|
+
* - Intra-community supply (K), export outside the EU (G) and not subject to
|
|
109
|
+
* VAT (O): the VATEX code that means the category, `VATEX-EU-IC`,
|
|
110
|
+
* `VATEX-EU-G` and `VATEX-EU-O`, which BR-IC-10, BR-G-10 and BR-O-10 accept.
|
|
111
|
+
* - Exempt (E): none. The reason depends on the exemption applied, which only
|
|
112
|
+
* the issuer knows; a document with an exempt item that states none is
|
|
113
|
+
* refused (BR-E-10).
|
|
33
114
|
* @param category The VAT category
|
|
34
115
|
* @param language The document language
|
|
35
116
|
*/
|
|
36
|
-
export const
|
|
37
|
-
category
|
|
38
|
-
|
|
117
|
+
export const getDefaultVatExemption = (category: TVatCategoryCode, language: string | undefined): IVatExemption | undefined => {
|
|
118
|
+
switch (category) {
|
|
119
|
+
case 'AE':
|
|
120
|
+
return {
|
|
39
121
|
code: 'VATEX-EU-AE',
|
|
40
|
-
reason: (language ?? '').toLowerCase().startsWith('de')
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
}
|
|
44
|
-
:
|
|
122
|
+
reason: (language ?? '').toLowerCase().startsWith('de') ? 'Steuerschuldnerschaft des Leistungsempfängers' : 'Reverse charge',
|
|
123
|
+
};
|
|
124
|
+
case 'K':
|
|
125
|
+
return { code: 'VATEX-EU-IC' };
|
|
126
|
+
case 'G':
|
|
127
|
+
return { code: 'VATEX-EU-G' };
|
|
128
|
+
case 'O':
|
|
129
|
+
return { code: 'VATEX-EU-O' };
|
|
130
|
+
default:
|
|
131
|
+
return undefined;
|
|
132
|
+
}
|
|
133
|
+
};
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* The exemption reason a VAT breakdown states: the code and the text its items
|
|
137
|
+
* state, each completed by the category's default where they state none
|
|
138
|
+
* (`getDefaultVatExemption`); undefined when there is neither
|
|
139
|
+
* @param stated The exemption the items state
|
|
140
|
+
* @param categoryDefault The category's default
|
|
141
|
+
*/
|
|
142
|
+
export const mergeVatExemption = (stated: IVatExemption | undefined, categoryDefault: IVatExemption | undefined): IVatExemption | undefined => {
|
|
143
|
+
const code = stated?.code ?? categoryDefault?.code;
|
|
144
|
+
const reason = stated?.reason ?? categoryDefault?.reason;
|
|
145
|
+
if (!code && !reason) {
|
|
146
|
+
return undefined;
|
|
147
|
+
}
|
|
148
|
+
return { ...(code ? { code } : {}), ...(reason ? { reason } : {}) };
|
|
149
|
+
};
|
|
45
150
|
|
|
46
151
|
/**
|
|
47
152
|
* Whether a VAT rate is a number not greater than zero; a rate that is no
|
|
@@ -50,87 +155,313 @@ export const getVatExemption = (category: TVatCategoryCode, language: string | u
|
|
|
50
155
|
*/
|
|
51
156
|
export const isRateNotAboveZero = (rate: unknown): boolean => typeof rate === 'number' && Number.isFinite(rate) && !(rate > 0);
|
|
52
157
|
|
|
53
|
-
/**
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
158
|
+
/**
|
|
159
|
+
* The refusal or finding for a standard rated line whose VAT rate is not
|
|
160
|
+
* greater than zero (BR-S-05)
|
|
161
|
+
* @param line The line index and its rate
|
|
162
|
+
* @param statedCategory Whether the item states the category S itself
|
|
163
|
+
*/
|
|
164
|
+
export const standardRatedLineWithoutRateMessage = (
|
|
165
|
+
line: { index: number; vatPercentage: number },
|
|
166
|
+
statedCategory = false,
|
|
167
|
+
): string =>
|
|
168
|
+
`In an Invoice line (BG-25) where the Invoiced item VAT category code (BT-151) is "Standard rated" the Invoiced item VAT rate (BT-152) shall be greater than zero; items[${line.index}].vatPercentage is ${String(line.vatPercentage)}. ` +
|
|
169
|
+
(statedCategory
|
|
170
|
+
? `items[${line.index}].vatCategory states S`
|
|
171
|
+
: `An item outside reverse charge that states no category (items[${line.index}].vatCategory) is standard rated (S); an item that is zero rated, exempt, an intra-community supply, an export or not subject to VAT states its category, Z, E, K, G or O`);
|
|
172
|
+
|
|
173
|
+
/** One rule a VAT category finding names */
|
|
174
|
+
export interface IVatCategoryViolation {
|
|
175
|
+
/** the EN 16931 rule ID, or the package's own code for a check no official rule states */
|
|
176
|
+
ruleId: string;
|
|
177
|
+
/** what is wrong, with the field */
|
|
178
|
+
message: string;
|
|
179
|
+
/** the envelope field concerned */
|
|
180
|
+
field: string;
|
|
57
181
|
}
|
|
58
182
|
|
|
183
|
+
/** The package's own check: the items of one VAT breakdown state different exemption reasons */
|
|
184
|
+
export const VAT_EXEMPTION_DIFFERS = 'EINVOICE-VAT-EXEMPTION-DIFFERS';
|
|
185
|
+
|
|
59
186
|
/**
|
|
60
|
-
* The
|
|
61
|
-
*
|
|
62
|
-
* and
|
|
63
|
-
*
|
|
64
|
-
* export G, not subject to VAT O) cannot be stated until an item carries a VAT
|
|
65
|
-
* category of its own.
|
|
66
|
-
* @param items The document's items
|
|
187
|
+
* The item VAT rate each category allows: greater than zero for S (BR-S-05);
|
|
188
|
+
* 0 for Z, E, AE, K and G (BR-Z-05, BR-E-05, BR-AE-05, BR-IC-05, BR-G-05);
|
|
189
|
+
* 0 or greater for L and M (BR-AF-05, BR-AG-05). An item not subject to VAT
|
|
190
|
+
* (O) is written without a rate (BR-O-05), so it states 0.
|
|
67
191
|
*/
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
192
|
+
const RATE_RULES: Record<TVatCategoryCode, { ruleId: string; allows: (rate: number) => boolean; expected: string }> = {
|
|
193
|
+
S: { ruleId: 'BR-S-05', allows: (rate) => rate > 0, expected: 'greater than zero' },
|
|
194
|
+
Z: { ruleId: 'BR-Z-05', allows: (rate) => rate === 0, expected: '0' },
|
|
195
|
+
E: { ruleId: 'BR-E-05', allows: (rate) => rate === 0, expected: '0' },
|
|
196
|
+
AE: { ruleId: 'BR-AE-05', allows: (rate) => rate === 0, expected: '0' },
|
|
197
|
+
K: { ruleId: 'BR-IC-05', allows: (rate) => rate === 0, expected: '0' },
|
|
198
|
+
G: { ruleId: 'BR-G-05', allows: (rate) => rate === 0, expected: '0' },
|
|
199
|
+
O: { ruleId: 'BR-O-05', allows: (rate) => rate === 0, expected: '0, as an item not subject to VAT is written without a VAT rate' },
|
|
200
|
+
L: { ruleId: 'BR-AF-05', allows: (rate) => rate >= 0, expected: '0 or greater' },
|
|
201
|
+
M: { ruleId: 'BR-AG-05', allows: (rate) => rate >= 0, expected: '0 or greater' },
|
|
202
|
+
};
|
|
203
|
+
|
|
204
|
+
/**
|
|
205
|
+
* BR-AF-05 as the CII binding of the CEN/TC 434 artefacts 1.3.16 checks it:
|
|
206
|
+
* `ram:RateApplicablePercent > 0`, where the rule text and the UBL binding
|
|
207
|
+
* allow 0. A CII document with IGIC at 0 fails the official check, so it is
|
|
208
|
+
* not written.
|
|
209
|
+
*/
|
|
210
|
+
const CII_IGIC_RATE_RULE = {
|
|
211
|
+
ruleId: 'BR-AF-05',
|
|
212
|
+
allows: (rate: number) => rate > 0,
|
|
213
|
+
expected: 'greater than zero in CII, whose binding of the CEN artefacts checks BR-AF-05 so; UBL allows 0',
|
|
214
|
+
};
|
|
215
|
+
|
|
216
|
+
/** The rule a VAT breakdown of each category breaks by stating, or by lacking, an exemption reason */
|
|
217
|
+
const EXEMPTION_RULES: Record<TVatCategoryCode, string> = {
|
|
218
|
+
S: 'BR-S-10',
|
|
219
|
+
Z: 'BR-Z-10',
|
|
220
|
+
E: 'BR-E-10',
|
|
221
|
+
AE: 'BR-AE-10',
|
|
222
|
+
K: 'BR-IC-10',
|
|
223
|
+
G: 'BR-G-10',
|
|
224
|
+
O: 'BR-O-10',
|
|
225
|
+
L: 'BR-AF-10',
|
|
226
|
+
M: 'BR-AG-10',
|
|
73
227
|
};
|
|
74
228
|
|
|
75
229
|
/**
|
|
76
|
-
*
|
|
77
|
-
*
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
*
|
|
84
|
-
*
|
|
85
|
-
*
|
|
86
|
-
*
|
|
87
|
-
*
|
|
88
|
-
*
|
|
89
|
-
*
|
|
90
|
-
*
|
|
91
|
-
*
|
|
92
|
-
*
|
|
93
|
-
*
|
|
94
|
-
*
|
|
95
|
-
*
|
|
96
|
-
*
|
|
97
|
-
*
|
|
98
|
-
*
|
|
99
|
-
*
|
|
100
|
-
*
|
|
101
|
-
*
|
|
102
|
-
*
|
|
230
|
+
* Every rule of EN 16931 on the VAT categories of a document's items that the
|
|
231
|
+
* envelope can break, in item order and then per document; an empty list when
|
|
232
|
+
* the document can be written. Amounts that are no number are not checked
|
|
233
|
+
* here (`findInvalidItemAmounts`).
|
|
234
|
+
* - The rate each category allows (BR-S-05, BR-Z-05, BR-E-05, BR-AE-05,
|
|
235
|
+
* BR-IC-05, BR-G-05, BR-O-05, BR-AF-05, BR-AG-05).
|
|
236
|
+
* - The exemption reason: stated for E, AE, K, G and O (a default for AE, K,
|
|
237
|
+
* G and O, `getDefaultVatExemption`), not stated for S, Z, L and M (the
|
|
238
|
+
* BR-x-10 rules); a code on the VATEX list (BR-CL-22); one reason per VAT
|
|
239
|
+
* breakdown, as a breakdown states one (`EINVOICE-VAT-EXEMPTION-DIFFERS`).
|
|
240
|
+
* - The parties' identifiers: for Z, E, L and M the seller VAT identifier
|
|
241
|
+
* (BT-31) or tax registration identifier (BT-32) (BR-Z-02, BR-E-02,
|
|
242
|
+
* BR-AF-02, BR-AG-02); for AE that and the buyer VAT identifier (BT-48) or
|
|
243
|
+
* legal registration identifier (BT-47) (BR-AE-02); for K the seller and the
|
|
244
|
+
* buyer VAT identifier (BR-IC-02); for G the seller VAT identifier
|
|
245
|
+
* (BR-G-02); for O neither VAT identifier (BR-O-02). The rules also accept
|
|
246
|
+
* a seller tax representative (BT-63), which the envelope cannot state.
|
|
247
|
+
* - An intra-community supply states the delivery date (BT-72) or the
|
|
248
|
+
* invoicing period (BG-14) (BR-IC-11) and the deliver to country (BT-80,
|
|
249
|
+
* `metadata.deliveryAddress.countryCode`) (BR-IC-12).
|
|
250
|
+
* - A document not subject to VAT has no item of another category (BR-O-11,
|
|
251
|
+
* BR-O-12).
|
|
252
|
+
* The rule for a standard rated item holds whether it states S or is S
|
|
253
|
+
* because it states no category, so an item at the rate 0 outside reverse
|
|
254
|
+
* charge that states no category breaks BR-S-05. Whether a document in
|
|
255
|
+
* reverse charge meets § 14a Abs. 1 UStG (both VAT identification numbers for
|
|
256
|
+
* a supply in another member state) cannot be told from the envelope, which
|
|
257
|
+
* has no place of supply; the issuing application enforces it.
|
|
103
258
|
* @param accountingDoc The document
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
259
|
+
*/
|
|
260
|
+
export const findVatCategoryViolations = (
|
|
261
|
+
accountingDoc: Pick<TAccountingDoc, 'items' | 'from' | 'to' | 'deliveryDate' | 'periodOfPerformance'> & {
|
|
262
|
+
reverseCharge?: boolean;
|
|
263
|
+
language?: string;
|
|
264
|
+
metadata?: { deliveryAddress?: { countryCode?: string } };
|
|
265
|
+
},
|
|
266
|
+
syntax?: 'ubl' | 'cii',
|
|
267
|
+
): IVatCategoryViolation[] => {
|
|
268
|
+
const violations: IVatCategoryViolation[] = [];
|
|
269
|
+
const items = accountingDoc.items ?? [];
|
|
270
|
+
const categories = items.map((item) => getItemVatCategory(accountingDoc, item));
|
|
271
|
+
|
|
272
|
+
items.forEach((item, index) => {
|
|
273
|
+
const category = categories[index];
|
|
274
|
+
const rule = syntax === 'cii' && category === 'L' ? CII_IGIC_RATE_RULE : RATE_RULES[category];
|
|
275
|
+
if (typeof item.vatPercentage === 'number' && Number.isFinite(item.vatPercentage) && !rule.allows(item.vatPercentage)) {
|
|
276
|
+
violations.push({
|
|
277
|
+
ruleId: rule.ruleId,
|
|
278
|
+
field: `items[${index}].vatPercentage`,
|
|
279
|
+
message:
|
|
280
|
+
category === 'S'
|
|
281
|
+
? standardRatedLineWithoutRateMessage({ index, vatPercentage: item.vatPercentage }, item.vatCategory === 'S')
|
|
282
|
+
: `an item of the VAT category ${category} has the VAT rate ${rule.expected}; items[${index}].vatPercentage is ${String(item.vatPercentage)}`,
|
|
283
|
+
});
|
|
114
284
|
}
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
285
|
+
const code = item.vatExemptionReasonCode?.trim();
|
|
286
|
+
if (code && !isVatexCode(code)) {
|
|
287
|
+
violations.push({
|
|
288
|
+
ruleId: 'BR-CL-22',
|
|
289
|
+
field: `items[${index}].vatExemptionReasonCode`,
|
|
290
|
+
message: `the VAT exemption reason code (BT-121) belongs to the CEF VATEX code list; items[${index}].vatExemptionReasonCode is ${code}`,
|
|
291
|
+
});
|
|
292
|
+
}
|
|
293
|
+
});
|
|
294
|
+
|
|
295
|
+
// one VAT breakdown per category and rate, as the encoders write them (computeDocumentTotals)
|
|
296
|
+
const groups = new Map<string, { category: TVatCategoryCode; indexes: number[] }>();
|
|
297
|
+
items.forEach((item, index) => {
|
|
298
|
+
const key = `${categories[index]}|${item.vatPercentage}`;
|
|
299
|
+
const group = groups.get(key) ?? { category: categories[index], indexes: [] };
|
|
300
|
+
group.indexes.push(index);
|
|
301
|
+
groups.set(key, group);
|
|
302
|
+
});
|
|
303
|
+
for (const { category, indexes } of groups.values()) {
|
|
304
|
+
const stated = indexes
|
|
305
|
+
.map((index) => ({ index, exemption: getItemVatExemption(items[index]) }))
|
|
306
|
+
.filter((entry): entry is { index: number; exemption: IVatExemption } => entry.exemption !== undefined);
|
|
307
|
+
const distinct = new Set(stated.map((entry) => `${entry.exemption.code ?? ''}|${entry.exemption.reason ?? ''}`));
|
|
308
|
+
const fields = indexes.map((index) => `items[${index}]`).join(', ');
|
|
309
|
+
if (distinct.size > 1) {
|
|
310
|
+
violations.push({
|
|
311
|
+
ruleId: VAT_EXEMPTION_DIFFERS,
|
|
312
|
+
field: stated.map((entry) => `items[${entry.index}]`).join(', '),
|
|
313
|
+
message: `the items of one VAT breakdown (category ${category}) state one exemption reason, as the breakdown states one (BT-120, BT-121); ${stated
|
|
314
|
+
.map((entry) => `items[${entry.index}] states ${[entry.exemption.code, entry.exemption.reason].filter(Boolean).join(' ')}`)
|
|
315
|
+
.join(', ')}`,
|
|
316
|
+
});
|
|
317
|
+
}
|
|
318
|
+
const withExemption = VAT_CATEGORIES_WITH_EXEMPTION.includes(category);
|
|
319
|
+
if (withExemption && stated.length === 0 && !getDefaultVatExemption(category, accountingDoc.language)) {
|
|
320
|
+
violations.push({
|
|
321
|
+
ruleId: EXEMPTION_RULES[category],
|
|
322
|
+
field: fields,
|
|
323
|
+
message: `a VAT breakdown of the category ${category} states a VAT exemption reason code (BT-121) or text (BT-120); ${fields} state neither vatExemptionReasonCode nor vatExemptionReason`,
|
|
324
|
+
});
|
|
325
|
+
}
|
|
326
|
+
if (!withExemption && stated.length > 0) {
|
|
327
|
+
violations.push({
|
|
328
|
+
ruleId: EXEMPTION_RULES[category],
|
|
329
|
+
field: stated.map((entry) => `items[${entry.index}]`).join(', '),
|
|
330
|
+
message: `a VAT breakdown of the category ${category} states no VAT exemption reason (BT-120, BT-121); ${stated
|
|
331
|
+
.map((entry) => `items[${entry.index}]`)
|
|
332
|
+
.join(', ')} state one`,
|
|
333
|
+
});
|
|
120
334
|
}
|
|
121
335
|
}
|
|
336
|
+
|
|
337
|
+
const has = (category: TVatCategoryCode) => categories.includes(category);
|
|
122
338
|
const seller = accountingDoc.from?.registrationDetails;
|
|
123
|
-
if (!seller?.vatId && !seller?.taxNumber) {
|
|
124
|
-
refuse(
|
|
125
|
-
'BR-AE-02',
|
|
126
|
-
'a reverse charge document states the seller VAT identifier (BT-31) or tax registration identifier (BT-32), from.registrationDetails.vatId and .taxNumber are missing; the rule would also accept a tax representative (BT-63), which the envelope cannot state',
|
|
127
|
-
);
|
|
128
|
-
}
|
|
129
339
|
const buyer = accountingDoc.to?.registrationDetails;
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
340
|
+
const sellerVatId = !!seller?.vatId?.trim();
|
|
341
|
+
const sellerTaxNumber = !!seller?.taxNumber?.trim();
|
|
342
|
+
const buyerVatId = !!buyer?.vatId?.trim();
|
|
343
|
+
const sellerNeither = (rule: string, category: string) =>
|
|
344
|
+
violations.push({
|
|
345
|
+
ruleId: rule,
|
|
346
|
+
field: 'from.registrationDetails',
|
|
347
|
+
message: `a document with an item of the VAT category ${category} states the seller VAT identifier (BT-31) or tax registration identifier (BT-32); from.registrationDetails.vatId and .taxNumber are missing. The rule would also accept a tax representative (BT-63), which the envelope cannot state`,
|
|
348
|
+
});
|
|
349
|
+
for (const [category, rule] of [['Z', 'BR-Z-02'], ['E', 'BR-E-02'], ['L', 'BR-AF-02'], ['M', 'BR-AG-02'], ['AE', 'BR-AE-02']] as const) {
|
|
350
|
+
if (has(category) && !sellerVatId && !sellerTaxNumber) {
|
|
351
|
+
sellerNeither(rule, category);
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
if (has('AE') && !buyerVatId && !buyer?.registrationId?.trim()) {
|
|
355
|
+
violations.push({
|
|
356
|
+
ruleId: 'BR-AE-02',
|
|
357
|
+
field: 'to.registrationDetails',
|
|
358
|
+
message:
|
|
359
|
+
'a reverse charge document states the buyer VAT identifier (BT-48) or legal registration identifier (BT-47), to.registrationDetails.vatId and .registrationId are missing',
|
|
360
|
+
});
|
|
361
|
+
}
|
|
362
|
+
if (has('K') && (!sellerVatId || !buyerVatId)) {
|
|
363
|
+
violations.push({
|
|
364
|
+
ruleId: 'BR-IC-02',
|
|
365
|
+
field: !sellerVatId ? 'from.registrationDetails.vatId' : 'to.registrationDetails.vatId',
|
|
366
|
+
message:
|
|
367
|
+
'a document with an intra-community supply (K) states the seller VAT identifier (BT-31) and the buyer VAT identifier (BT-48) (§ 14a Abs. 3 Satz 2 UStG); ' +
|
|
368
|
+
[!sellerVatId ? 'from.registrationDetails.vatId' : '', !buyerVatId ? 'to.registrationDetails.vatId' : ''].filter(Boolean).join(' and ') +
|
|
369
|
+
' missing. The rule would also accept a seller tax representative (BT-63), which the envelope cannot state',
|
|
370
|
+
});
|
|
371
|
+
}
|
|
372
|
+
if (has('K') && accountingDoc.deliveryDate === undefined && accountingDoc.periodOfPerformance === undefined) {
|
|
373
|
+
violations.push({
|
|
374
|
+
ruleId: 'BR-IC-11',
|
|
375
|
+
field: 'deliveryDate',
|
|
376
|
+
message: 'a document with an intra-community supply (K) states the actual delivery date (BT-72) or the invoicing period (BG-14); deliveryDate and periodOfPerformance are missing',
|
|
377
|
+
});
|
|
378
|
+
}
|
|
379
|
+
if (has('K') && !accountingDoc.metadata?.deliveryAddress?.countryCode?.trim()) {
|
|
380
|
+
violations.push({
|
|
381
|
+
ruleId: 'BR-IC-12',
|
|
382
|
+
field: 'metadata.deliveryAddress.countryCode',
|
|
383
|
+
message: 'a document with an intra-community supply (K) states the deliver to country code (BT-80); metadata.deliveryAddress.countryCode is missing',
|
|
384
|
+
});
|
|
385
|
+
}
|
|
386
|
+
if (has('G') && !sellerVatId) {
|
|
387
|
+
violations.push({
|
|
388
|
+
ruleId: 'BR-G-02',
|
|
389
|
+
field: 'from.registrationDetails.vatId',
|
|
390
|
+
message:
|
|
391
|
+
'a document with an export outside the EU (G) states the seller VAT identifier (BT-31); from.registrationDetails.vatId is missing. The rule would also accept a seller tax representative (BT-63), which the envelope cannot state',
|
|
392
|
+
});
|
|
135
393
|
}
|
|
394
|
+
if (has('O') && (sellerVatId || buyerVatId)) {
|
|
395
|
+
violations.push({
|
|
396
|
+
ruleId: 'BR-O-02',
|
|
397
|
+
field: sellerVatId ? 'from.registrationDetails.vatId' : 'to.registrationDetails.vatId',
|
|
398
|
+
message: `a document with an item not subject to VAT (O) states neither the seller VAT identifier (BT-31) nor the buyer VAT identifier (BT-48); ${[
|
|
399
|
+
sellerVatId ? 'from.registrationDetails.vatId' : '',
|
|
400
|
+
buyerVatId ? 'to.registrationDetails.vatId' : '',
|
|
401
|
+
]
|
|
402
|
+
.filter(Boolean)
|
|
403
|
+
.join(' and ')} stated`,
|
|
404
|
+
});
|
|
405
|
+
}
|
|
406
|
+
if (has('O') && categories.some((category) => category !== 'O')) {
|
|
407
|
+
violations.push({
|
|
408
|
+
ruleId: 'BR-O-11',
|
|
409
|
+
field: categories.map((category, index) => (category === 'O' ? '' : `items[${index}].vatCategory`)).filter(Boolean).join(', '),
|
|
410
|
+
message: `a document with an item not subject to VAT (O) has no item of another VAT category, as its VAT breakdown is the one of O; ${categories
|
|
411
|
+
.map((category, index) => (category === 'O' ? '' : `items[${index}] is ${category}`))
|
|
412
|
+
.filter(Boolean)
|
|
413
|
+
.join(', ')}`,
|
|
414
|
+
});
|
|
415
|
+
}
|
|
416
|
+
return violations;
|
|
417
|
+
};
|
|
418
|
+
|
|
419
|
+
/**
|
|
420
|
+
* Refuses a document whose items cannot be written with a valid VAT category
|
|
421
|
+
* (`findVatCategoryViolations`), naming the first rule it breaks; no value is
|
|
422
|
+
* put in place of a missing one.
|
|
423
|
+
* @param accountingDoc The document
|
|
424
|
+
* @param targetFormat The syntax being written
|
|
425
|
+
*/
|
|
426
|
+
export const assertVatCategoryWritable = (accountingDoc: TAccountingDoc, targetFormat: 'ubl' | 'cii'): void => {
|
|
427
|
+
const [violation] = findVatCategoryViolations(accountingDoc, targetFormat);
|
|
428
|
+
if (violation) {
|
|
429
|
+
throw new EInvoiceFormatError(`${violation.ruleId}: ${violation.message}`, {
|
|
430
|
+
targetFormat,
|
|
431
|
+
unsupportedFeatures: [violation.ruleId],
|
|
432
|
+
});
|
|
433
|
+
}
|
|
434
|
+
};
|
|
435
|
+
|
|
436
|
+
/**
|
|
437
|
+
* Gives the items of a decoded document the VAT category their lines state
|
|
438
|
+
* (BT-151) and the exemption reason (BT-120) and code (BT-121) of the VAT
|
|
439
|
+
* breakdown they belong to: the one of their category and rate, or of their
|
|
440
|
+
* category for an item not subject to VAT, which states no rate. A line that
|
|
441
|
+
* states no category, or a code EN 16931 does not allow, keeps none; the item
|
|
442
|
+
* is then read as AE or S (`getItemVatCategory`) and the code list rules
|
|
443
|
+
* report the code.
|
|
444
|
+
* @param items The decoded items, in line order
|
|
445
|
+
* @param statedValues What the document states; undefined for a ZUGFeRD 1.0 document
|
|
446
|
+
*/
|
|
447
|
+
export const applyStatedVatCategories = (items: TAccountingDocItem[], statedValues: IEInvoiceStatedValues | undefined): void => {
|
|
448
|
+
if (!statedValues) {
|
|
449
|
+
return;
|
|
450
|
+
}
|
|
451
|
+
items.forEach((item, index) => {
|
|
452
|
+
const category = statedValues.lines[index]?.vatCategoryCode;
|
|
453
|
+
if (!isVatCategoryCode(category)) {
|
|
454
|
+
return;
|
|
455
|
+
}
|
|
456
|
+
item.vatCategory = category;
|
|
457
|
+
const breakdown = statedValues.vatBreakdown.find(
|
|
458
|
+
(group) => group.categoryCode === category && (category === 'O' || group.rate === item.vatPercentage),
|
|
459
|
+
);
|
|
460
|
+
if (breakdown?.exemptionReason) {
|
|
461
|
+
item.vatExemptionReason = breakdown.exemptionReason;
|
|
462
|
+
}
|
|
463
|
+
if (breakdown?.exemptionReasonCode) {
|
|
464
|
+
item.vatExemptionReasonCode = breakdown.exemptionReasonCode;
|
|
465
|
+
}
|
|
466
|
+
});
|
|
136
467
|
};
|