@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.
Files changed (48) hide show
  1. package/dist_ts/00_commitinfo_data.js +1 -1
  2. package/dist_ts/einvoice.js +12 -23
  3. package/dist_ts/formats/cii/cii.decoder.d.ts +6 -0
  4. package/dist_ts/formats/cii/cii.decoder.js +11 -1
  5. package/dist_ts/formats/cii/cii.encoder.js +4 -1
  6. package/dist_ts/formats/cii/facturx/facturx.decoder.js +8 -4
  7. package/dist_ts/formats/cii/facturx/facturx.encoder.js +57 -57
  8. package/dist_ts/formats/cii/zugferd/zugferd.decoder.js +8 -4
  9. package/dist_ts/formats/cii/zugferd/zugferd.encoder.js +55 -67
  10. package/dist_ts/formats/cii/zugferd/zugferd.v1.decoder.d.ts +5 -0
  11. package/dist_ts/formats/cii/zugferd/zugferd.v1.decoder.js +18 -5
  12. package/dist_ts/formats/semantic/semantic.validator.js +20 -1
  13. package/dist_ts/formats/ubl/generic/ubl.encoder.js +31 -57
  14. package/dist_ts/formats/ubl/ubl.encoder.js +4 -1
  15. package/dist_ts/formats/ubl/xrechnung/xrechnung.decoder.d.ts +4 -0
  16. package/dist_ts/formats/ubl/xrechnung/xrechnung.decoder.js +12 -2
  17. package/dist_ts/formats/utils/currency.calculator.decimal.d.ts +9 -1
  18. package/dist_ts/formats/utils/currency.calculator.decimal.js +10 -3
  19. package/dist_ts/formats/utils/document.totals.d.ts +89 -0
  20. package/dist_ts/formats/utils/document.totals.js +108 -0
  21. package/dist_ts/formats/utils/vat.category.d.ts +50 -0
  22. package/dist_ts/formats/utils/vat.category.js +64 -0
  23. package/dist_ts/formats/validation/codelist.validator.js +3 -11
  24. package/dist_ts/formats/validation/en16931.business-rules.validator.js +38 -44
  25. package/dist_ts/formats/validation/facturx.validator.js +19 -5
  26. package/dist_ts/formats/validation/vat-categories.validator.js +9 -1
  27. package/package.json +2 -2
  28. package/readme.md +25 -0
  29. package/ts/00_commitinfo_data.ts +1 -1
  30. package/ts/einvoice.ts +11 -25
  31. package/ts/formats/cii/cii.decoder.ts +14 -0
  32. package/ts/formats/cii/cii.encoder.ts +3 -0
  33. package/ts/formats/cii/facturx/facturx.decoder.ts +8 -3
  34. package/ts/formats/cii/facturx/facturx.encoder.ts +59 -65
  35. package/ts/formats/cii/zugferd/zugferd.decoder.ts +8 -3
  36. package/ts/formats/cii/zugferd/zugferd.encoder.ts +58 -77
  37. package/ts/formats/cii/zugferd/zugferd.v1.decoder.ts +22 -4
  38. package/ts/formats/semantic/semantic.validator.ts +20 -0
  39. package/ts/formats/ubl/generic/ubl.encoder.ts +36 -69
  40. package/ts/formats/ubl/ubl.encoder.ts +3 -0
  41. package/ts/formats/ubl/xrechnung/xrechnung.decoder.ts +15 -1
  42. package/ts/formats/utils/currency.calculator.decimal.ts +10 -2
  43. package/ts/formats/utils/document.totals.ts +167 -0
  44. package/ts/formats/utils/vat.category.ts +89 -0
  45. package/ts/formats/validation/codelist.validator.ts +3 -12
  46. package/ts/formats/validation/en16931.business-rules.validator.ts +44 -51
  47. package/ts/formats/validation/facturx.validator.ts +19 -4
  48. 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
- this.decimalCalculator = new DecimalCurrencyCalculator(invoice.currency);
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
- // Calculation rules (BR-CO-*)
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
- ? group.reduce((sum, item) => {
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
- ? this.decimalCalculator!.calculateVAT(expectedTaxableAmount, new Decimal(rate))
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
- const lineNetAmount = item.unitNetPrice * item.unitQuantity;
413
- if (isNaN(lineNetAmount)) {
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
- if (item.vatPercentage === undefined) {
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
- let totalVAT = Decimal.ZERO;
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
- results.push(...this.validateProfileSpecificRules(invoice, detectedProfile));
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 || []);