@fin.cx/einvoice 8.2.1 → 8.2.3
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/cii.decoder.d.ts +6 -0
- package/dist_ts/formats/cii/cii.decoder.js +11 -1
- package/dist_ts/formats/cii/cii.encoder.js +4 -1
- package/dist_ts/formats/cii/facturx/facturx.decoder.js +8 -4
- package/dist_ts/formats/cii/facturx/facturx.encoder.js +57 -57
- package/dist_ts/formats/cii/zugferd/zugferd.decoder.js +8 -4
- package/dist_ts/formats/cii/zugferd/zugferd.encoder.js +55 -67
- 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/semantic/semantic.validator.js +20 -1
- package/dist_ts/formats/ubl/generic/ubl.encoder.js +31 -57
- 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/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 +89 -0
- package/dist_ts/formats/utils/document.totals.js +108 -0
- package/dist_ts/formats/utils/vat.category.d.ts +50 -0
- package/dist_ts/formats/utils/vat.category.js +64 -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/readme.md +25 -0
- package/ts/00_commitinfo_data.ts +1 -1
- package/ts/einvoice.ts +11 -25
- package/ts/formats/cii/cii.decoder.ts +14 -0
- package/ts/formats/cii/cii.encoder.ts +3 -0
- package/ts/formats/cii/facturx/facturx.decoder.ts +8 -3
- package/ts/formats/cii/facturx/facturx.encoder.ts +59 -65
- package/ts/formats/cii/zugferd/zugferd.decoder.ts +8 -3
- package/ts/formats/cii/zugferd/zugferd.encoder.ts +58 -77
- package/ts/formats/cii/zugferd/zugferd.v1.decoder.ts +22 -4
- package/ts/formats/semantic/semantic.validator.ts +20 -0
- package/ts/formats/ubl/generic/ubl.encoder.ts +36 -69
- package/ts/formats/ubl/ubl.encoder.ts +3 -0
- package/ts/formats/ubl/xrechnung/xrechnung.decoder.ts +15 -1
- package/ts/formats/utils/currency.calculator.decimal.ts +10 -2
- package/ts/formats/utils/document.totals.ts +167 -0
- package/ts/formats/utils/vat.category.ts +89 -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
|
@@ -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 || []);
|