@fin.cx/einvoice 8.2.1 → 8.2.2
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.js +12 -23
- package/dist_ts/formats/cii/facturx/facturx.encoder.js +40 -47
- package/dist_ts/formats/cii/zugferd/zugferd.encoder.js +31 -56
- package/dist_ts/formats/semantic/semantic.validator.js +20 -1
- package/dist_ts/formats/ubl/generic/ubl.encoder.js +23 -46
- package/dist_ts/formats/utils/currency.calculator.decimal.d.ts +9 -1
- package/dist_ts/formats/utils/currency.calculator.decimal.js +10 -3
- package/dist_ts/formats/utils/document.totals.d.ts +79 -0
- package/dist_ts/formats/utils/document.totals.js +93 -0
- package/dist_ts/formats/validation/codelist.validator.js +3 -11
- package/dist_ts/formats/validation/en16931.business-rules.validator.js +38 -44
- package/dist_ts/formats/validation/facturx.validator.js +19 -5
- package/dist_ts/formats/validation/vat-categories.validator.js +9 -1
- package/package.json +2 -2
- package/ts/00_commitinfo_data.ts +1 -1
- package/ts/einvoice.ts +11 -25
- package/ts/formats/cii/facturx/facturx.encoder.ts +41 -55
- package/ts/formats/cii/zugferd/zugferd.encoder.ts +31 -66
- package/ts/formats/semantic/semantic.validator.ts +20 -0
- package/ts/formats/ubl/generic/ubl.encoder.ts +28 -57
- package/ts/formats/utils/currency.calculator.decimal.ts +10 -2
- package/ts/formats/utils/document.totals.ts +144 -0
- package/ts/formats/validation/codelist.validator.ts +3 -12
- package/ts/formats/validation/en16931.business-rules.validator.ts +44 -51
- package/ts/formats/validation/facturx.validator.ts +19 -4
- package/ts/formats/validation/vat-categories.validator.ts +9 -0
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
import type { TAccountingDoc } from '../../interfaces/common.js';
|
|
2
|
+
import { Decimal } from './decimal.js';
|
|
3
|
+
import { DecimalCurrencyCalculator } from './currency.calculator.decimal.js';
|
|
4
|
+
import { EInvoiceFormatError } from '../../errors.js';
|
|
5
|
+
import type { ValidationResult } from '../validation/validation.types.js';
|
|
6
|
+
|
|
7
|
+
/** An item amount that is no finite number, so no total can be computed from it */
|
|
8
|
+
export interface IInvalidItemAmount {
|
|
9
|
+
index: number;
|
|
10
|
+
field: 'unitQuantity' | 'unitNetPrice' | 'vatPercentage';
|
|
11
|
+
value: unknown;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* The item amounts of a document that are no finite number. The totals cannot
|
|
16
|
+
* be computed while there is one: the encoders and the total getters refuse
|
|
17
|
+
* the document, and a validator reports the lines and skips the rules that
|
|
18
|
+
* need the totals.
|
|
19
|
+
* @param items The document's items
|
|
20
|
+
*/
|
|
21
|
+
export const findInvalidItemAmounts = (items: TAccountingDoc['items'] | undefined): IInvalidItemAmount[] => {
|
|
22
|
+
const invalid: IInvalidItemAmount[] = [];
|
|
23
|
+
for (const [index, item] of (items ?? []).entries()) {
|
|
24
|
+
for (const field of ['unitQuantity', 'unitNetPrice', 'vatPercentage'] as const) {
|
|
25
|
+
const value: unknown = item[field];
|
|
26
|
+
if (typeof value !== 'number' || !Number.isFinite(value)) {
|
|
27
|
+
invalid.push({ index, field, value });
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
return invalid;
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* The result a validator adds when it skips the rules that need the totals,
|
|
36
|
+
* because a line amount is no number (the line rules report the lines)
|
|
37
|
+
* @param invalid The invalid item amounts
|
|
38
|
+
* @param source The validator's source
|
|
39
|
+
*/
|
|
40
|
+
export const getTotalsSkippedResult = (invalid: IInvalidItemAmount[], source: string): ValidationResult => ({
|
|
41
|
+
ruleId: 'TOTALS-NOT-CHECKED',
|
|
42
|
+
source,
|
|
43
|
+
severity: 'info',
|
|
44
|
+
message: `The rules on totals and the VAT breakdown were not checked: ${invalid
|
|
45
|
+
.map((entry) => `items[${entry.index}].${entry.field} is no number`)
|
|
46
|
+
.join(', ')}`,
|
|
47
|
+
field: invalid.map((entry) => `items[${entry.index}].${entry.field}`).join(', '),
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
/** The most decimals an amount may have in EN 16931 (BR-DEC-09 to BR-DEC-23) */
|
|
51
|
+
export const EN16931_MAX_AMOUNT_DECIMALS = 2;
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* The calculator every document amount is computed with: rounding half up to
|
|
55
|
+
* the currency's minor unit, at most two decimals as EN 16931 allows. A
|
|
56
|
+
* currency without minor unit (JPY) keeps 0 decimals; one with three (BHD,
|
|
57
|
+
* KWD, ...) is rounded to two.
|
|
58
|
+
* @param currency The document currency
|
|
59
|
+
*/
|
|
60
|
+
export const getDocumentCalculator = (currency: TAccountingDoc['currency']): DecimalCurrencyCalculator =>
|
|
61
|
+
new DecimalCurrencyCalculator(currency, 'HALF_UP', { maxDecimals: EN16931_MAX_AMOUNT_DECIMALS });
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* The VAT breakdown of one rate (BG-23): the taxable amount (BT-116), the tax
|
|
65
|
+
* on it (BT-117) and the rate (BT-119).
|
|
66
|
+
*/
|
|
67
|
+
export interface IDocumentVatGroup {
|
|
68
|
+
rate: number;
|
|
69
|
+
taxableAmount: Decimal;
|
|
70
|
+
taxAmount: Decimal;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* The totals of a document as EN 16931 computes them, in decimal arithmetic.
|
|
75
|
+
* Every amount is rounded to the minor unit of the currency; all encoders
|
|
76
|
+
* write these values, so the XML is consistent in every syntax.
|
|
77
|
+
*/
|
|
78
|
+
export interface IDocumentTotals {
|
|
79
|
+
/** decimals the amounts are rounded to and written with: the currency's minor unit, at most two */
|
|
80
|
+
minorUnits: number;
|
|
81
|
+
/** the Invoice line net amount (BT-131) of each item, in item order: quantity × net price, rounded */
|
|
82
|
+
lineNetAmounts: Decimal[];
|
|
83
|
+
/** Sum of Invoice line net amount (BT-106) */
|
|
84
|
+
lineTotal: Decimal;
|
|
85
|
+
/** Invoice total amount without VAT (BT-109): BT-106, as the envelope has no document level allowances or charges */
|
|
86
|
+
taxBasisTotal: Decimal;
|
|
87
|
+
/** VAT breakdown (BG-23), in the order the rates first appear */
|
|
88
|
+
vatGroups: IDocumentVatGroup[];
|
|
89
|
+
/** Invoice total VAT amount (BT-110): the sum of the rounded VAT category tax amounts */
|
|
90
|
+
taxTotal: Decimal;
|
|
91
|
+
/** Invoice total amount with VAT (BT-112): BT-109 + BT-110 */
|
|
92
|
+
grandTotal: Decimal;
|
|
93
|
+
/** Amount due for payment (BT-115): BT-112, as the envelope states no paid amount yet */
|
|
94
|
+
duePayable: Decimal;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Computes the totals of a document the way the EN 16931 business rules check
|
|
99
|
+
* them, every amount rounded half up to the currency's minor unit, at most two
|
|
100
|
+
* decimals (BR-DEC-*): each line net amount (BT-131) is quantity × net price
|
|
101
|
+
* rounded; the sum of line net amounts (BT-106) is their sum (BR-CO-10);
|
|
102
|
+
* each VAT category taxable amount (BT-116) is the sum of the line net amounts
|
|
103
|
+
* at that rate, and its tax amount (BT-117) is BT-116 × rate / 100 rounded
|
|
104
|
+
* (BR-CO-17); the total VAT (BT-110) is the sum of the category tax amounts
|
|
105
|
+
* (BR-CO-14); the total with VAT (BT-112) is BT-109 + BT-110 (BR-CO-15).
|
|
106
|
+
* @param accountingDoc The document
|
|
107
|
+
*/
|
|
108
|
+
export const computeDocumentTotals = (accountingDoc: Pick<TAccountingDoc, 'currency' | 'items'>): IDocumentTotals => {
|
|
109
|
+
const calculator = getDocumentCalculator(accountingDoc.currency);
|
|
110
|
+
const minorUnits = calculator.getCurrencyInfo().minorUnits;
|
|
111
|
+
const lineNetAmounts: Decimal[] = [];
|
|
112
|
+
const taxableByRate = new Map<number, Decimal>();
|
|
113
|
+
// an amount that is no number cannot be computed with; it is refused, not treated as 0
|
|
114
|
+
const [firstInvalid] = findInvalidItemAmounts(accountingDoc.items);
|
|
115
|
+
if (firstInvalid) {
|
|
116
|
+
throw new EInvoiceFormatError(
|
|
117
|
+
`items[${firstInvalid.index}].${firstInvalid.field} is no number: ${String(firstInvalid.value)}`,
|
|
118
|
+
{ unsupportedFeatures: [`items[${firstInvalid.index}].${firstInvalid.field}`] },
|
|
119
|
+
);
|
|
120
|
+
}
|
|
121
|
+
for (const item of accountingDoc.items ?? []) {
|
|
122
|
+
const lineNet = calculator.calculateLineNet(item.unitQuantity, item.unitNetPrice);
|
|
123
|
+
lineNetAmounts.push(lineNet);
|
|
124
|
+
taxableByRate.set(item.vatPercentage, (taxableByRate.get(item.vatPercentage) ?? Decimal.ZERO).add(lineNet));
|
|
125
|
+
}
|
|
126
|
+
const vatGroups: IDocumentVatGroup[] = [...taxableByRate.entries()].map(([rate, taxableAmount]) => ({
|
|
127
|
+
rate,
|
|
128
|
+
taxableAmount: calculator.round(taxableAmount),
|
|
129
|
+
taxAmount: calculator.calculateVAT(taxableAmount, rate),
|
|
130
|
+
}));
|
|
131
|
+
const lineTotal = calculator.round(Decimal.sum(lineNetAmounts));
|
|
132
|
+
const taxTotal = calculator.round(Decimal.sum(vatGroups.map((group) => group.taxAmount)));
|
|
133
|
+
const grandTotal = calculator.round(lineTotal.add(taxTotal));
|
|
134
|
+
return {
|
|
135
|
+
minorUnits,
|
|
136
|
+
lineNetAmounts,
|
|
137
|
+
lineTotal,
|
|
138
|
+
taxBasisTotal: lineTotal,
|
|
139
|
+
vatGroups,
|
|
140
|
+
taxTotal,
|
|
141
|
+
grandTotal,
|
|
142
|
+
duePayable: grandTotal,
|
|
143
|
+
};
|
|
144
|
+
};
|
|
@@ -146,18 +146,9 @@ export class CodeListValidator {
|
|
|
146
146
|
* Validate tax category codes (UNCL5305)
|
|
147
147
|
*/
|
|
148
148
|
private validateTaxCategories(invoice: EInvoice): void {
|
|
149
|
-
//
|
|
150
|
-
//
|
|
151
|
-
|
|
152
|
-
invoice.taxBreakdown?.forEach((breakdown, index) => {
|
|
153
|
-
// Since the computed taxBreakdown doesn't have metadata,
|
|
154
|
-
// we'll skip the tax category code validation for now
|
|
155
|
-
// This would need to be implemented differently to access the raw data
|
|
156
|
-
|
|
157
|
-
// TODO: Access raw tax breakdown data with metadata from invoice.metadata.taxBreakdown
|
|
158
|
-
// when that structure is implemented
|
|
159
|
-
});
|
|
160
|
-
|
|
149
|
+
// The document level VAT breakdown is computed from the lines (`taxBreakdown`) and
|
|
150
|
+
// carries no category code of its own, so only the lines' codes are checked here.
|
|
151
|
+
|
|
161
152
|
// Line level tax categories
|
|
162
153
|
invoice.items?.forEach((item, index) => {
|
|
163
154
|
// Cast to extended type to access metadata
|
|
@@ -4,6 +4,12 @@ import type { EInvoice } from '../../einvoice.js';
|
|
|
4
4
|
import { CurrencyCalculator, areMonetaryValuesEqual } from '../utils/currency.utils.js';
|
|
5
5
|
import { DecimalCurrencyCalculator } from '../utils/currency.calculator.decimal.js';
|
|
6
6
|
import { Decimal } from '../utils/decimal.js';
|
|
7
|
+
import {
|
|
8
|
+
computeDocumentTotals,
|
|
9
|
+
findInvalidItemAmounts,
|
|
10
|
+
getDocumentCalculator,
|
|
11
|
+
getTotalsSkippedResult,
|
|
12
|
+
} from '../utils/document.totals.js';
|
|
7
13
|
import type { ValidationResult, ValidationOptions } from './validation.types.js';
|
|
8
14
|
|
|
9
15
|
/**
|
|
@@ -24,24 +30,34 @@ export class EN16931BusinessRulesValidator {
|
|
|
24
30
|
// Initialize currency calculators if currency is available
|
|
25
31
|
if (invoice.currency) {
|
|
26
32
|
this.currencyCalculator = new CurrencyCalculator(invoice.currency);
|
|
27
|
-
|
|
33
|
+
// the rounding the encoders use: the currency's minor unit, at most two decimals
|
|
34
|
+
this.decimalCalculator = getDocumentCalculator(invoice.currency);
|
|
28
35
|
}
|
|
29
36
|
|
|
30
37
|
// Document level rules (BR-01 to BR-65)
|
|
31
38
|
this.validateDocumentRules(invoice);
|
|
32
39
|
|
|
33
|
-
//
|
|
34
|
-
if (options.checkCalculations !== false) {
|
|
35
|
-
this.validateCalculationRules(invoice);
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
// VAT rules (BR-S-*, BR-Z-*, BR-E-*, BR-AE-*, BR-IC-*, BR-G-*, BR-O-*)
|
|
39
|
-
if (options.checkVAT !== false) {
|
|
40
|
-
this.validateVATRules(invoice);
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
// Line level rules (BR-21 to BR-30)
|
|
40
|
+
// Line level rules (BR-21 to BR-30), first: they report a line amount that is no number
|
|
44
41
|
this.validateLineRules(invoice);
|
|
42
|
+
|
|
43
|
+
// The totals are computed from the line amounts; while one is no number they cannot be,
|
|
44
|
+
// so the rules that need them are skipped and the result says so
|
|
45
|
+
const invalidAmounts = findInvalidItemAmounts(invoice.items);
|
|
46
|
+
if (invalidAmounts.length > 0) {
|
|
47
|
+
if (options.checkCalculations !== false || options.checkVAT !== false) {
|
|
48
|
+
this.results.push(getTotalsSkippedResult(invalidAmounts, 'EN16931'));
|
|
49
|
+
}
|
|
50
|
+
} else {
|
|
51
|
+
// Calculation rules (BR-CO-*)
|
|
52
|
+
if (options.checkCalculations !== false) {
|
|
53
|
+
this.validateCalculationRules(invoice);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// VAT rules (BR-S-*, BR-Z-*, BR-E-*, BR-AE-*, BR-IC-*, BR-G-*, BR-O-*)
|
|
57
|
+
if (options.checkVAT !== false) {
|
|
58
|
+
this.validateVATRules(invoice);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
45
61
|
|
|
46
62
|
// Allowances and charges rules
|
|
47
63
|
if (options.checkAllowances !== false) {
|
|
@@ -292,18 +308,19 @@ export class EN16931BusinessRulesValidator {
|
|
|
292
308
|
// BR-S-03: VAT category tax amount for standard rated
|
|
293
309
|
vatGroups.forEach((group, rate) => {
|
|
294
310
|
if (rate > 0) { // Standard rated
|
|
311
|
+
// the category amounts as EN 16931 computes them: the sum of the rounded line net
|
|
312
|
+
// amounts at the rate, and the tax on it rounded once (BR-S-08, BR-CO-17)
|
|
313
|
+
const documentGroup = useDecimal
|
|
314
|
+
? computeDocumentTotals(invoice).vatGroups.find((groupArg) => groupArg.rate === rate)
|
|
315
|
+
: undefined;
|
|
295
316
|
const expectedTaxableAmount = useDecimal
|
|
296
|
-
?
|
|
297
|
-
const unitPrice = new Decimal(item.unitNetPrice);
|
|
298
|
-
const quantity = new Decimal(item.unitQuantity);
|
|
299
|
-
return sum.add(unitPrice.multiply(quantity));
|
|
300
|
-
}, Decimal.ZERO)
|
|
317
|
+
? documentGroup?.taxableAmount ?? Decimal.ZERO
|
|
301
318
|
: group.reduce((sum, item) =>
|
|
302
319
|
sum + (item.unitNetPrice * item.unitQuantity), 0
|
|
303
320
|
);
|
|
304
321
|
|
|
305
322
|
const expectedTaxAmount = useDecimal
|
|
306
|
-
?
|
|
323
|
+
? documentGroup?.taxAmount ?? Decimal.ZERO
|
|
307
324
|
: (expectedTaxableAmount as number) * (rate / 100);
|
|
308
325
|
|
|
309
326
|
// Find corresponding breakdown
|
|
@@ -408,9 +425,10 @@ export class EN16931BusinessRulesValidator {
|
|
|
408
425
|
);
|
|
409
426
|
}
|
|
410
427
|
|
|
411
|
-
// BR-24: Each Invoice line shall have an Invoice line net amount
|
|
412
|
-
|
|
413
|
-
|
|
428
|
+
// BR-24: Each Invoice line shall have an Invoice line net amount; it is quantity × net
|
|
429
|
+
// price, so a quantity or net price that is no number leaves the line without one
|
|
430
|
+
const isAmount = (value: unknown) => typeof value === 'number' && Number.isFinite(value);
|
|
431
|
+
if (!isAmount(item.unitNetPrice) || !isAmount(item.unitQuantity)) {
|
|
414
432
|
this.addError(
|
|
415
433
|
'BR-24',
|
|
416
434
|
`Invoice line ${index + 1} must have a valid net amount`,
|
|
@@ -418,8 +436,9 @@ export class EN16931BusinessRulesValidator {
|
|
|
418
436
|
);
|
|
419
437
|
}
|
|
420
438
|
|
|
421
|
-
// BR-CO-04: Each Invoice line shall be categorized with an Invoiced item VAT category code
|
|
422
|
-
|
|
439
|
+
// BR-CO-04: Each Invoice line shall be categorized with an Invoiced item VAT category code;
|
|
440
|
+
// the envelope derives the category from the rate, which has to be a number
|
|
441
|
+
if (!isAmount(item.vatPercentage)) {
|
|
423
442
|
this.addError(
|
|
424
443
|
'BR-CO-04',
|
|
425
444
|
`Invoice line ${index + 1} must have a VAT category code`,
|
|
@@ -579,34 +598,8 @@ export class EN16931BusinessRulesValidator {
|
|
|
579
598
|
* Calculate total VAT using decimal arithmetic
|
|
580
599
|
*/
|
|
581
600
|
private calculateTotalVATDecimal(invoice: EInvoice): Decimal {
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
// Group items by VAT rate
|
|
585
|
-
const vatGroups = new Map<string, Decimal>();
|
|
586
|
-
|
|
587
|
-
for (const item of invoice.items || []) {
|
|
588
|
-
const vatRate = item.vatPercentage || 0;
|
|
589
|
-
const rateKey = vatRate.toString();
|
|
590
|
-
|
|
591
|
-
const unitPrice = new Decimal(item.unitNetPrice || 0);
|
|
592
|
-
const quantity = new Decimal(item.unitQuantity || 0);
|
|
593
|
-
const lineNet = unitPrice.multiply(quantity);
|
|
594
|
-
|
|
595
|
-
if (vatGroups.has(rateKey)) {
|
|
596
|
-
vatGroups.set(rateKey, vatGroups.get(rateKey)!.add(lineNet));
|
|
597
|
-
} else {
|
|
598
|
-
vatGroups.set(rateKey, lineNet);
|
|
599
|
-
}
|
|
600
|
-
}
|
|
601
|
-
|
|
602
|
-
// Calculate VAT for each group
|
|
603
|
-
for (const [rateKey, baseAmount] of vatGroups) {
|
|
604
|
-
const rate = new Decimal(rateKey);
|
|
605
|
-
const vat = this.decimalCalculator!.calculateVAT(baseAmount, rate);
|
|
606
|
-
totalVAT = totalVAT.add(vat);
|
|
607
|
-
}
|
|
608
|
-
|
|
609
|
-
return totalVAT;
|
|
601
|
+
// the sum of the VAT category tax amounts, each rounded once (BR-CO-14, BR-CO-17)
|
|
602
|
+
return computeDocumentTotals(invoice).taxTotal;
|
|
610
603
|
}
|
|
611
604
|
|
|
612
605
|
private calculateDocumentAllowances(invoice: EInvoice): number {
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
* Implements validation for MINIMUM, BASIC, EN16931, and EXTENDED profiles
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
+
import { findInvalidItemAmounts, getTotalsSkippedResult } from '../utils/document.totals.js';
|
|
6
7
|
import type { ValidationResult } from './validation.types.js';
|
|
7
8
|
import type { EInvoice } from '../../einvoice.js';
|
|
8
9
|
|
|
@@ -211,9 +212,19 @@ export class FacturXValidator {
|
|
|
211
212
|
return results;
|
|
212
213
|
}
|
|
213
214
|
|
|
215
|
+
// The totals are computed from the line amounts; while one is no number the checks on
|
|
216
|
+
// totals are skipped and the result says so
|
|
217
|
+
const invalidAmounts = findInvalidItemAmounts(invoice.items);
|
|
218
|
+
const totalsComputable = invalidAmounts.length === 0;
|
|
219
|
+
if (!totalsComputable) {
|
|
220
|
+
results.push(getTotalsSkippedResult(invalidAmounts, 'FACTURX'));
|
|
221
|
+
}
|
|
222
|
+
|
|
214
223
|
// Validate according to profile
|
|
215
|
-
results.push(...this.validateProfileRequirements(invoice, detectedProfile));
|
|
216
|
-
|
|
224
|
+
results.push(...this.validateProfileRequirements(invoice, detectedProfile, totalsComputable));
|
|
225
|
+
if (totalsComputable) {
|
|
226
|
+
results.push(...this.validateProfileSpecificRules(invoice, detectedProfile));
|
|
227
|
+
}
|
|
217
228
|
|
|
218
229
|
// Add profile-specific business rules
|
|
219
230
|
if (detectedProfile === FacturXProfile.MINIMUM) {
|
|
@@ -267,12 +278,16 @@ export class FacturXValidator {
|
|
|
267
278
|
/**
|
|
268
279
|
* Validate field requirements for a specific profile
|
|
269
280
|
*/
|
|
270
|
-
private validateProfileRequirements(invoice: EInvoice, profile: FacturXProfile): ValidationResult[] {
|
|
281
|
+
private validateProfileRequirements(invoice: EInvoice, profile: FacturXProfile, totalsComputable = true): ValidationResult[] {
|
|
271
282
|
const results: ValidationResult[] = [];
|
|
272
283
|
const requirements = this.profileRequirements[profile];
|
|
284
|
+
const totalFields = ['totalInvoiceAmount', 'totalNetAmount', 'totalVatAmount'];
|
|
273
285
|
|
|
274
|
-
// Check mandatory fields
|
|
286
|
+
// Check mandatory fields; the totals only when they can be computed
|
|
275
287
|
for (const field of requirements.mandatory) {
|
|
288
|
+
if (!totalsComputable && totalFields.includes(field)) {
|
|
289
|
+
continue;
|
|
290
|
+
}
|
|
276
291
|
const value = this.getFieldValue(invoice, field);
|
|
277
292
|
if (value === undefined || value === null || value === '') {
|
|
278
293
|
results.push({
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import * as plugins from '../../plugins.js';
|
|
2
|
+
import { findInvalidItemAmounts, getTotalsSkippedResult } from '../utils/document.totals.js';
|
|
2
3
|
import type { TAccountingDocItem } from '@tsclass/tsclass/dist_ts/finance/index.js';
|
|
3
4
|
import type { EInvoice } from '../../einvoice.js';
|
|
4
5
|
import { CurrencyCalculator } from '../utils/currency.utils.js';
|
|
@@ -50,6 +51,14 @@ export class VATCategoriesValidator {
|
|
|
50
51
|
this.currencyCalculator = new CurrencyCalculator(invoice.currency);
|
|
51
52
|
}
|
|
52
53
|
|
|
54
|
+
// The VAT breakdown is computed from the line amounts; while one is no number it cannot
|
|
55
|
+
// be, so these rules are skipped and the result says so (the line rules report the lines)
|
|
56
|
+
const invalidAmounts = findInvalidItemAmounts(invoice.items);
|
|
57
|
+
if (invalidAmounts.length > 0) {
|
|
58
|
+
this.results.push(getTotalsSkippedResult(invalidAmounts, 'EN16931'));
|
|
59
|
+
return this.results;
|
|
60
|
+
}
|
|
61
|
+
|
|
53
62
|
// Group items by VAT category
|
|
54
63
|
const itemsByCategory = this.groupItemsByVATCategory(invoice.items || []);
|
|
55
64
|
const breakdownsByCategory = this.groupBreakdownsByCategory(invoice.taxBreakdown || []);
|