@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
package/ts/einvoice.ts
CHANGED
|
@@ -46,6 +46,7 @@ import {
|
|
|
46
46
|
// Import enhanced validators
|
|
47
47
|
import { EN16931Validator } from './formats/validation/en16931.validator.js';
|
|
48
48
|
import { EN16931BusinessRulesValidator } from './formats/validation/en16931.business-rules.validator.js';
|
|
49
|
+
import { computeDocumentTotals } from './formats/utils/document.totals.js';
|
|
49
50
|
import { CodeListValidator } from './formats/validation/codelist.validator.js';
|
|
50
51
|
import type { ValidationOptions } from './formats/validation/validation.types.js';
|
|
51
52
|
|
|
@@ -211,7 +212,7 @@ export class EInvoice implements TAccountingDocEnvelope<TAccountingDocType, {}>
|
|
|
211
212
|
}
|
|
212
213
|
|
|
213
214
|
public get totalGross(): number {
|
|
214
|
-
return this.
|
|
215
|
+
return computeDocumentTotals(this).grandTotal.toNumber();
|
|
215
216
|
}
|
|
216
217
|
|
|
217
218
|
public get taxBreakdown(): Array<{ taxPercent: number; netAmount: number; taxAmount: number }> {
|
|
@@ -795,42 +796,27 @@ export class EInvoice implements TAccountingDocEnvelope<TAccountingDocType, {}>
|
|
|
795
796
|
* Calculates the total net amount
|
|
796
797
|
*/
|
|
797
798
|
private calculateTotalNet(): number {
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
}, 0);
|
|
799
|
+
// the sum of line net amounts (BT-106) the encoders write
|
|
800
|
+
return computeDocumentTotals(this).lineTotal.toNumber();
|
|
801
801
|
}
|
|
802
802
|
|
|
803
803
|
/**
|
|
804
804
|
* Calculates the total VAT amount
|
|
805
805
|
*/
|
|
806
806
|
private calculateTotalVat(): number {
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
return sum + (net * item.vatPercentage / 100);
|
|
810
|
-
}, 0);
|
|
807
|
+
// the total VAT (BT-110) the encoders write
|
|
808
|
+
return computeDocumentTotals(this).taxTotal.toNumber();
|
|
811
809
|
}
|
|
812
810
|
|
|
813
811
|
/**
|
|
814
812
|
* Calculates tax breakdown by rate
|
|
815
813
|
*/
|
|
816
814
|
private calculateTaxBreakdown(): Array<{ taxPercent: number; netAmount: number; taxAmount: number }> {
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
const current = breakdown.get(item.vatPercentage) || { net: 0, tax: 0 };
|
|
824
|
-
breakdown.set(item.vatPercentage, {
|
|
825
|
-
net: current.net + net,
|
|
826
|
-
tax: current.tax + tax
|
|
827
|
-
});
|
|
828
|
-
});
|
|
829
|
-
|
|
830
|
-
return Array.from(breakdown.entries()).map(([rate, amounts]) => ({
|
|
831
|
-
taxPercent: rate,
|
|
832
|
-
netAmount: amounts.net,
|
|
833
|
-
taxAmount: amounts.tax
|
|
815
|
+
// the VAT breakdown (BG-23) the encoders write
|
|
816
|
+
return computeDocumentTotals(this).vatGroups.map((group) => ({
|
|
817
|
+
taxPercent: group.rate,
|
|
818
|
+
netAmount: group.taxableAmount.toNumber(),
|
|
819
|
+
taxAmount: group.taxAmount.toNumber(),
|
|
834
820
|
}));
|
|
835
821
|
}
|
|
836
822
|
|
|
@@ -2,10 +2,8 @@ import { CIIBaseEncoder } from '../cii.encoder.js';
|
|
|
2
2
|
import type { TAccountingDoc, TCreditNote, TInvoiceDocument } from '../../../interfaces/common.js';
|
|
3
3
|
import { FACTURX_PROFILE_IDS } from './facturx.types.js';
|
|
4
4
|
import { DOMParser, XMLSerializer } from '../../../plugins.js';
|
|
5
|
-
import { Decimal } from '../../utils/decimal.js';
|
|
6
|
-
import { DecimalCurrencyCalculator } from '../../utils/currency.calculator.decimal.js';
|
|
7
|
-
import { getCurrencyMinorUnits } from '../../utils/currency.utils.js';
|
|
8
5
|
import { getDocumentTypeCode } from '../../utils/document.typecode.js';
|
|
6
|
+
import { computeDocumentTotals } from '../../utils/document.totals.js';
|
|
9
7
|
import { getWritableDate, getWritableDueDate } from '../../utils/date.value.js';
|
|
10
8
|
|
|
11
9
|
/**
|
|
@@ -316,6 +314,26 @@ export class FacturXEncoder extends CIIBaseEncoder {
|
|
|
316
314
|
currencyElement.textContent = invoice.currency;
|
|
317
315
|
settlementElement.appendChild(currencyElement);
|
|
318
316
|
|
|
317
|
+
// the EN 16931 totals in decimal arithmetic, rounded as the business rules check them
|
|
318
|
+
const totals = computeDocumentTotals(invoice);
|
|
319
|
+
|
|
320
|
+
// VAT breakdown (BG-23), one per rate, before the payment terms as the CII schema orders them;
|
|
321
|
+
// the category is the one the lines carry
|
|
322
|
+
for (const { rate, taxableAmount, taxAmount } of totals.vatGroups) {
|
|
323
|
+
const taxElement = doc.createElement('ram:ApplicableTradeTax');
|
|
324
|
+
const appendText = (name: string, text: string) => {
|
|
325
|
+
const element = doc.createElement(name);
|
|
326
|
+
element.textContent = text;
|
|
327
|
+
taxElement.appendChild(element);
|
|
328
|
+
};
|
|
329
|
+
appendText('ram:CalculatedAmount', taxAmount.toFixed(totals.minorUnits)); // BT-117
|
|
330
|
+
appendText('ram:TypeCode', 'VAT');
|
|
331
|
+
appendText('ram:BasisAmount', taxableAmount.toFixed(totals.minorUnits)); // BT-116
|
|
332
|
+
appendText('ram:CategoryCode', 'S'); // BT-118
|
|
333
|
+
appendText('ram:RateApplicablePercent', rate.toString()); // BT-119
|
|
334
|
+
settlementElement.appendChild(taxElement);
|
|
335
|
+
}
|
|
336
|
+
|
|
319
337
|
// Add payment terms
|
|
320
338
|
const paymentTermsElement = doc.createElement('ram:SpecifiedTradePaymentTerms');
|
|
321
339
|
|
|
@@ -333,50 +351,21 @@ export class FacturXEncoder extends CIIBaseEncoder {
|
|
|
333
351
|
|
|
334
352
|
settlementElement.appendChild(paymentTermsElement);
|
|
335
353
|
|
|
336
|
-
// Add totals
|
|
354
|
+
// Add totals, in the order of the CII schema
|
|
337
355
|
const monetarySummationElement = doc.createElement('ram:SpecifiedTradeSettlementHeaderMonetarySummation');
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
let totalTaxAmount = Decimal.ZERO;
|
|
344
|
-
|
|
345
|
-
// Calculate from items
|
|
346
|
-
if (invoice.items) {
|
|
347
|
-
for (const item of invoice.items) {
|
|
348
|
-
const itemNetAmount = calculator.calculateLineNet(item.unitQuantity, item.unitNetPrice);
|
|
349
|
-
const itemTaxAmount = calculator.calculateVAT(itemNetAmount, item.vatPercentage);
|
|
350
|
-
|
|
351
|
-
totalNetAmount = totalNetAmount.add(itemNetAmount);
|
|
352
|
-
totalTaxAmount = totalTaxAmount.add(itemTaxAmount);
|
|
356
|
+
const appendAmount = (name: string, value: { toFixed(decimalPlaces: number): string }, withCurrency = false) => {
|
|
357
|
+
const element = doc.createElement(name);
|
|
358
|
+
element.textContent = value.toFixed(totals.minorUnits);
|
|
359
|
+
if (withCurrency) {
|
|
360
|
+
element.setAttribute('currencyID', invoice.currency);
|
|
353
361
|
}
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
const lineTotalElement = doc.createElement('ram:LineTotalAmount');
|
|
362
|
-
lineTotalElement.textContent = totalNetAmount.toFixed(minorUnits);
|
|
363
|
-
monetarySummationElement.appendChild(lineTotalElement);
|
|
364
|
-
|
|
365
|
-
// Add tax total amount
|
|
366
|
-
const taxTotalElement = doc.createElement('ram:TaxTotalAmount');
|
|
367
|
-
taxTotalElement.textContent = totalTaxAmount.toFixed(minorUnits);
|
|
368
|
-
taxTotalElement.setAttribute('currencyID', invoice.currency);
|
|
369
|
-
monetarySummationElement.appendChild(taxTotalElement);
|
|
370
|
-
|
|
371
|
-
// Add grand total amount
|
|
372
|
-
const grandTotalElement = doc.createElement('ram:GrandTotalAmount');
|
|
373
|
-
grandTotalElement.textContent = totalGrossAmount.toFixed(minorUnits);
|
|
374
|
-
monetarySummationElement.appendChild(grandTotalElement);
|
|
375
|
-
|
|
376
|
-
// Add due payable amount
|
|
377
|
-
const duePayableElement = doc.createElement('ram:DuePayableAmount');
|
|
378
|
-
duePayableElement.textContent = totalGrossAmount.toFixed(minorUnits);
|
|
379
|
-
monetarySummationElement.appendChild(duePayableElement);
|
|
362
|
+
monetarySummationElement.appendChild(element);
|
|
363
|
+
};
|
|
364
|
+
appendAmount('ram:LineTotalAmount', totals.lineTotal); // BT-106
|
|
365
|
+
appendAmount('ram:TaxBasisTotalAmount', totals.taxBasisTotal); // BT-109
|
|
366
|
+
appendAmount('ram:TaxTotalAmount', totals.taxTotal, true); // BT-110
|
|
367
|
+
appendAmount('ram:GrandTotalAmount', totals.grandTotal); // BT-112
|
|
368
|
+
appendAmount('ram:DuePayableAmount', totals.duePayable); // BT-115
|
|
380
369
|
|
|
381
370
|
settlementElement.appendChild(monetarySummationElement);
|
|
382
371
|
}
|
|
@@ -388,12 +377,12 @@ export class FacturXEncoder extends CIIBaseEncoder {
|
|
|
388
377
|
* @param invoice Invoice data
|
|
389
378
|
*/
|
|
390
379
|
private addLineItems(doc: Document, transactionElement: Element, invoice: TAccountingDoc): void {
|
|
391
|
-
|
|
392
|
-
const
|
|
380
|
+
// the line net amounts of the one EN 16931 totals computation
|
|
381
|
+
const totals = computeDocumentTotals(invoice);
|
|
393
382
|
|
|
394
383
|
// Add each line item
|
|
395
384
|
if (invoice.items) {
|
|
396
|
-
for (const item of invoice.items) {
|
|
385
|
+
for (const [index, item] of invoice.items.entries()) {
|
|
397
386
|
// Create line item element
|
|
398
387
|
const lineItemElement = doc.createElement('ram:IncludedSupplyChainTradeLineItem');
|
|
399
388
|
|
|
@@ -464,15 +453,12 @@ export class FacturXEncoder extends CIIBaseEncoder {
|
|
|
464
453
|
|
|
465
454
|
settlementElement.appendChild(taxElement);
|
|
466
455
|
|
|
467
|
-
//
|
|
468
|
-
const monetarySummationElement = doc.createElement('ram:
|
|
469
|
-
|
|
470
|
-
// Calculate item total with decimal arithmetic and currency rounding.
|
|
471
|
-
const itemNetAmount = calculator.calculateLineNet(item.unitQuantity, item.unitNetPrice);
|
|
456
|
+
// Line monetary summation, as the CII schema names it
|
|
457
|
+
const monetarySummationElement = doc.createElement('ram:SpecifiedTradeSettlementLineMonetarySummation');
|
|
472
458
|
|
|
473
|
-
//
|
|
459
|
+
// Invoice line net amount (BT-131): quantity × net price, rounded
|
|
474
460
|
const lineTotalElement = doc.createElement('ram:LineTotalAmount');
|
|
475
|
-
lineTotalElement.textContent =
|
|
461
|
+
lineTotalElement.textContent = totals.lineNetAmounts[index].toFixed(totals.minorUnits);
|
|
476
462
|
monetarySummationElement.appendChild(lineTotalElement);
|
|
477
463
|
|
|
478
464
|
settlementElement.appendChild(monetarySummationElement);
|
|
@@ -6,6 +6,7 @@ import { DOMParser, XMLSerializer } from '../../../plugins.js';
|
|
|
6
6
|
import { getDocumentTypeCode } from '../../utils/document.typecode.js';
|
|
7
7
|
import { getWritableDate, getWritableDueDate } from '../../utils/date.value.js';
|
|
8
8
|
import { getPaymentTermsNote } from '../../utils/payment.terms.js';
|
|
9
|
+
import { computeDocumentTotals } from '../../utils/document.totals.js';
|
|
9
10
|
|
|
10
11
|
/**
|
|
11
12
|
* Encoder for ZUGFeRD invoice format
|
|
@@ -456,30 +457,16 @@ export class ZUGFeRDEncoder extends CIIBaseEncoder {
|
|
|
456
457
|
* @param invoice Invoice data
|
|
457
458
|
*/
|
|
458
459
|
private addTaxDetails(doc: Document, settlementElement: Element, invoice: TAccountingDoc): void {
|
|
459
|
-
//
|
|
460
|
-
const
|
|
460
|
+
// the EN 16931 totals in decimal arithmetic, rounded as the business rules check them
|
|
461
|
+
const totals = computeDocumentTotals(invoice);
|
|
461
462
|
|
|
462
|
-
//
|
|
463
|
-
|
|
464
|
-
for (const item of invoice.items) {
|
|
465
|
-
const itemNetAmount = item.unitNetPrice * item.unitQuantity;
|
|
466
|
-
const vatRate = item.vatPercentage;
|
|
467
|
-
|
|
468
|
-
const currentAmount = taxCategories.get(vatRate) || 0;
|
|
469
|
-
taxCategories.set(vatRate, currentAmount + itemNetAmount);
|
|
470
|
-
}
|
|
471
|
-
}
|
|
472
|
-
|
|
473
|
-
// Add each tax category
|
|
474
|
-
for (const [rate, baseAmount] of taxCategories.entries()) {
|
|
463
|
+
// VAT breakdown (BG-23), one per rate
|
|
464
|
+
for (const { rate, taxableAmount, taxAmount } of totals.vatGroups) {
|
|
475
465
|
const taxElement = doc.createElement('ram:ApplicableTradeTax');
|
|
476
|
-
|
|
477
|
-
//
|
|
478
|
-
const taxAmount = baseAmount * (rate / 100);
|
|
479
|
-
|
|
480
|
-
// Add calculated amount
|
|
466
|
+
|
|
467
|
+
// VAT category tax amount (BT-117)
|
|
481
468
|
const calculatedAmountElement = doc.createElement('ram:CalculatedAmount');
|
|
482
|
-
calculatedAmountElement.textContent = taxAmount.toFixed(
|
|
469
|
+
calculatedAmountElement.textContent = taxAmount.toFixed(totals.minorUnits);
|
|
483
470
|
taxElement.appendChild(calculatedAmountElement);
|
|
484
471
|
|
|
485
472
|
// Add type code (VAT)
|
|
@@ -487,9 +474,9 @@ export class ZUGFeRDEncoder extends CIIBaseEncoder {
|
|
|
487
474
|
typeCodeElement.textContent = 'VAT';
|
|
488
475
|
taxElement.appendChild(typeCodeElement);
|
|
489
476
|
|
|
490
|
-
//
|
|
477
|
+
// VAT category taxable amount (BT-116)
|
|
491
478
|
const basisAmountElement = doc.createElement('ram:BasisAmount');
|
|
492
|
-
basisAmountElement.textContent =
|
|
479
|
+
basisAmountElement.textContent = taxableAmount.toFixed(totals.minorUnits);
|
|
493
480
|
taxElement.appendChild(basisAmountElement);
|
|
494
481
|
|
|
495
482
|
// Add category code
|
|
@@ -515,43 +502,23 @@ export class ZUGFeRDEncoder extends CIIBaseEncoder {
|
|
|
515
502
|
private addMonetarySummation(doc: Document, settlementElement: Element, invoice: TAccountingDoc): void {
|
|
516
503
|
const monetarySummationElement = doc.createElement('ram:SpecifiedTradeSettlementHeaderMonetarySummation');
|
|
517
504
|
|
|
518
|
-
//
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
const itemNetAmount = item.unitNetPrice * item.unitQuantity;
|
|
526
|
-
const itemTaxAmount = itemNetAmount * (item.vatPercentage / 100);
|
|
527
|
-
|
|
528
|
-
totalNetAmount += itemNetAmount;
|
|
529
|
-
totalTaxAmount += itemTaxAmount;
|
|
505
|
+
// the EN 16931 totals in decimal arithmetic, rounded as the business rules check them
|
|
506
|
+
const totals = computeDocumentTotals(invoice);
|
|
507
|
+
const appendAmount = (name: string, value: { toFixed(decimalPlaces: number): string }, withCurrency = false) => {
|
|
508
|
+
const element = doc.createElement(name);
|
|
509
|
+
element.textContent = value.toFixed(totals.minorUnits);
|
|
510
|
+
if (withCurrency) {
|
|
511
|
+
element.setAttribute('currencyID', invoice.currency);
|
|
530
512
|
}
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
const totalGrossAmount = totalNetAmount + totalTaxAmount;
|
|
513
|
+
monetarySummationElement.appendChild(element);
|
|
514
|
+
};
|
|
534
515
|
|
|
535
|
-
//
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
const taxTotalElement = doc.createElement('ram:TaxTotalAmount');
|
|
542
|
-
taxTotalElement.textContent = totalTaxAmount.toFixed(2);
|
|
543
|
-
taxTotalElement.setAttribute('currencyID', invoice.currency);
|
|
544
|
-
monetarySummationElement.appendChild(taxTotalElement);
|
|
545
|
-
|
|
546
|
-
// Add grand total amount
|
|
547
|
-
const grandTotalElement = doc.createElement('ram:GrandTotalAmount');
|
|
548
|
-
grandTotalElement.textContent = totalGrossAmount.toFixed(2);
|
|
549
|
-
monetarySummationElement.appendChild(grandTotalElement);
|
|
550
|
-
|
|
551
|
-
// Add due payable amount
|
|
552
|
-
const duePayableElement = doc.createElement('ram:DuePayableAmount');
|
|
553
|
-
duePayableElement.textContent = totalGrossAmount.toFixed(2);
|
|
554
|
-
monetarySummationElement.appendChild(duePayableElement);
|
|
516
|
+
// in the order of the CII schema
|
|
517
|
+
appendAmount('ram:LineTotalAmount', totals.lineTotal); // BT-106
|
|
518
|
+
appendAmount('ram:TaxBasisTotalAmount', totals.taxBasisTotal); // BT-109
|
|
519
|
+
appendAmount('ram:TaxTotalAmount', totals.taxTotal, true); // BT-110
|
|
520
|
+
appendAmount('ram:GrandTotalAmount', totals.grandTotal); // BT-112
|
|
521
|
+
appendAmount('ram:DuePayableAmount', totals.duePayable); // BT-115
|
|
555
522
|
|
|
556
523
|
settlementElement.appendChild(monetarySummationElement);
|
|
557
524
|
}
|
|
@@ -565,7 +532,8 @@ export class ZUGFeRDEncoder extends CIIBaseEncoder {
|
|
|
565
532
|
private addLineItems(doc: Document, transactionElement: Element, invoice: TAccountingDoc): void {
|
|
566
533
|
// Add each line item
|
|
567
534
|
if (invoice.items) {
|
|
568
|
-
|
|
535
|
+
const totals = computeDocumentTotals(invoice);
|
|
536
|
+
for (const [index, item] of invoice.items.entries()) {
|
|
569
537
|
// Create line item element
|
|
570
538
|
const lineItemElement = doc.createElement('ram:IncludedSupplyChainTradeLineItem');
|
|
571
539
|
|
|
@@ -636,15 +604,12 @@ export class ZUGFeRDEncoder extends CIIBaseEncoder {
|
|
|
636
604
|
|
|
637
605
|
settlementElement.appendChild(taxElement);
|
|
638
606
|
|
|
639
|
-
//
|
|
640
|
-
const monetarySummationElement = doc.createElement('ram:
|
|
641
|
-
|
|
642
|
-
// Calculate item total
|
|
643
|
-
const itemNetAmount = item.unitNetPrice * item.unitQuantity;
|
|
607
|
+
// Line monetary summation, as the CII schema names it
|
|
608
|
+
const monetarySummationElement = doc.createElement('ram:SpecifiedTradeSettlementLineMonetarySummation');
|
|
644
609
|
|
|
645
|
-
//
|
|
610
|
+
// Invoice line net amount (BT-131): quantity × net price, rounded
|
|
646
611
|
const lineTotalElement = doc.createElement('ram:LineTotalAmount');
|
|
647
|
-
lineTotalElement.textContent =
|
|
612
|
+
lineTotalElement.textContent = totals.lineNetAmounts[index].toFixed(totals.minorUnits);
|
|
648
613
|
monetarySummationElement.appendChild(lineTotalElement);
|
|
649
614
|
|
|
650
615
|
settlementElement.appendChild(monetarySummationElement);
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
* Validates invoices against EN16931 Business Terms and Business Groups
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
+
import { findInvalidItemAmounts, getTotalsSkippedResult } from '../utils/document.totals.js';
|
|
6
7
|
import type { ValidationResult } from '../validation/validation.types.js';
|
|
7
8
|
import type { EN16931SemanticModel, BusinessTerms, BusinessGroups } from './bt-bg.model.js';
|
|
8
9
|
import type { EInvoice } from '../../einvoice.js';
|
|
@@ -36,6 +37,25 @@ export class SemanticModelValidator {
|
|
|
36
37
|
*/
|
|
37
38
|
public validate(invoice: EInvoice): ValidationResult[] {
|
|
38
39
|
const results: ValidationResult[] = [];
|
|
40
|
+
|
|
41
|
+
// The semantic model carries the totals, which are computed from the line amounts; while
|
|
42
|
+
// one is no number the model cannot be built, so each such line is reported (BR-24 for the
|
|
43
|
+
// net amount, BR-CO-04 for the VAT rate) and the model rules are skipped
|
|
44
|
+
const invalidAmounts = findInvalidItemAmounts(invoice.items);
|
|
45
|
+
if (invalidAmounts.length > 0) {
|
|
46
|
+
for (const entry of invalidAmounts) {
|
|
47
|
+
results.push({
|
|
48
|
+
ruleId: entry.field === 'vatPercentage' ? 'BR-CO-04' : 'BR-24',
|
|
49
|
+
source: 'SEMANTIC',
|
|
50
|
+
severity: 'error',
|
|
51
|
+
message: `Invoice line ${entry.index + 1}: ${entry.field} is no number`,
|
|
52
|
+
field: `items[${entry.index}].${entry.field}`,
|
|
53
|
+
value: entry.value,
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
results.push(getTotalsSkippedResult(invalidAmounts, 'SEMANTIC'));
|
|
57
|
+
return results;
|
|
58
|
+
}
|
|
39
59
|
|
|
40
60
|
// Convert to semantic model
|
|
41
61
|
const model = this.adapter.toSemanticModel(invoice);
|
|
@@ -6,6 +6,7 @@ import { getDocumentTypeCode } from '../../utils/document.typecode.js';
|
|
|
6
6
|
import { getPrecedingInvoiceReferences } from '../../utils/preceding.invoice.js';
|
|
7
7
|
import { getWritableDueDate } from '../../utils/date.value.js';
|
|
8
8
|
import { getPaymentTermsNote } from '../../utils/payment.terms.js';
|
|
9
|
+
import { computeDocumentTotals } from '../../utils/document.totals.js';
|
|
9
10
|
|
|
10
11
|
/**
|
|
11
12
|
* UBL Encoder implementation
|
|
@@ -383,48 +384,31 @@ export class UBLEncoder extends UBLBaseEncoder {
|
|
|
383
384
|
private addTaxTotal(doc: Document, parentElement: Element, invoice: TAccountingDoc): void {
|
|
384
385
|
const taxTotalNode = doc.createElement('cac:TaxTotal');
|
|
385
386
|
parentElement.appendChild(taxTotalNode);
|
|
386
|
-
|
|
387
|
-
//
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
// Calculate from items
|
|
392
|
-
if (invoice.items) {
|
|
393
|
-
for (const item of invoice.items) {
|
|
394
|
-
const itemNetAmount = item.unitNetPrice * item.unitQuantity;
|
|
395
|
-
const itemTaxAmount = itemNetAmount * (item.vatPercentage / 100);
|
|
396
|
-
const vatRate = item.vatPercentage;
|
|
397
|
-
|
|
398
|
-
totalTaxAmount += itemTaxAmount;
|
|
399
|
-
|
|
400
|
-
// Aggregate by VAT rate
|
|
401
|
-
const currentAmount = taxCategories.get(vatRate) || 0;
|
|
402
|
-
taxCategories.set(vatRate, currentAmount + itemNetAmount);
|
|
403
|
-
}
|
|
404
|
-
}
|
|
405
|
-
|
|
406
|
-
// Add total tax amount
|
|
387
|
+
|
|
388
|
+
// the EN 16931 totals in decimal arithmetic, rounded as the business rules check them
|
|
389
|
+
const totals = computeDocumentTotals(invoice);
|
|
390
|
+
|
|
391
|
+
// Invoice total VAT amount (BT-110)
|
|
407
392
|
const taxAmountElement = doc.createElement('cbc:TaxAmount');
|
|
408
393
|
taxAmountElement.setAttribute('currencyID', invoice.currency);
|
|
409
|
-
taxAmountElement.textContent =
|
|
394
|
+
taxAmountElement.textContent = totals.taxTotal.toFixed(totals.minorUnits);
|
|
410
395
|
taxTotalNode.appendChild(taxAmountElement);
|
|
411
|
-
|
|
412
|
-
//
|
|
413
|
-
for (const
|
|
396
|
+
|
|
397
|
+
// VAT breakdown (BG-23), one subtotal per rate
|
|
398
|
+
for (const { rate, taxableAmount, taxAmount } of totals.vatGroups) {
|
|
414
399
|
const taxSubtotalNode = doc.createElement('cac:TaxSubtotal');
|
|
415
400
|
taxTotalNode.appendChild(taxSubtotalNode);
|
|
416
|
-
|
|
417
|
-
//
|
|
401
|
+
|
|
402
|
+
// VAT category taxable amount (BT-116)
|
|
418
403
|
const taxableAmountElement = doc.createElement('cbc:TaxableAmount');
|
|
419
404
|
taxableAmountElement.setAttribute('currencyID', invoice.currency);
|
|
420
|
-
taxableAmountElement.textContent =
|
|
405
|
+
taxableAmountElement.textContent = taxableAmount.toFixed(totals.minorUnits);
|
|
421
406
|
taxSubtotalNode.appendChild(taxableAmountElement);
|
|
422
|
-
|
|
423
|
-
//
|
|
424
|
-
const taxAmount = baseAmount * (rate / 100);
|
|
407
|
+
|
|
408
|
+
// VAT category tax amount (BT-117)
|
|
425
409
|
const subtotalTaxAmountElement = doc.createElement('cbc:TaxAmount');
|
|
426
410
|
subtotalTaxAmountElement.setAttribute('currencyID', invoice.currency);
|
|
427
|
-
subtotalTaxAmountElement.textContent = taxAmount.toFixed(
|
|
411
|
+
subtotalTaxAmountElement.textContent = taxAmount.toFixed(totals.minorUnits);
|
|
428
412
|
taxSubtotalNode.appendChild(subtotalTaxAmountElement);
|
|
429
413
|
|
|
430
414
|
// Tax category
|
|
@@ -460,46 +444,33 @@ export class UBLEncoder extends UBLBaseEncoder {
|
|
|
460
444
|
private addLegalMonetaryTotal(doc: Document, parentElement: Element, invoice: TAccountingDoc): void {
|
|
461
445
|
const legalMonetaryTotalNode = doc.createElement('cac:LegalMonetaryTotal');
|
|
462
446
|
parentElement.appendChild(legalMonetaryTotalNode);
|
|
463
|
-
|
|
464
|
-
//
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
// Calculate from items
|
|
469
|
-
if (invoice.items) {
|
|
470
|
-
for (const item of invoice.items) {
|
|
471
|
-
const itemNetAmount = item.unitNetPrice * item.unitQuantity;
|
|
472
|
-
const itemTaxAmount = itemNetAmount * (item.vatPercentage / 100);
|
|
473
|
-
|
|
474
|
-
totalNetAmount += itemNetAmount;
|
|
475
|
-
totalTaxAmount += itemTaxAmount;
|
|
476
|
-
}
|
|
477
|
-
}
|
|
478
|
-
|
|
479
|
-
const totalGrossAmount = totalNetAmount + totalTaxAmount;
|
|
447
|
+
|
|
448
|
+
// the EN 16931 totals in decimal arithmetic, rounded as the business rules check them
|
|
449
|
+
const totals = computeDocumentTotals(invoice);
|
|
450
|
+
const amount = (value: { toFixed(decimalPlaces: number): string }) => value.toFixed(totals.minorUnits);
|
|
480
451
|
|
|
481
452
|
// Line extension amount (sum of line net amounts)
|
|
482
453
|
const lineExtensionAmountElement = doc.createElement('cbc:LineExtensionAmount');
|
|
483
454
|
lineExtensionAmountElement.setAttribute('currencyID', invoice.currency);
|
|
484
|
-
lineExtensionAmountElement.textContent =
|
|
455
|
+
lineExtensionAmountElement.textContent = amount(totals.lineTotal);
|
|
485
456
|
legalMonetaryTotalNode.appendChild(lineExtensionAmountElement);
|
|
486
457
|
|
|
487
458
|
// Tax exclusive amount
|
|
488
459
|
const taxExclusiveAmountElement = doc.createElement('cbc:TaxExclusiveAmount');
|
|
489
460
|
taxExclusiveAmountElement.setAttribute('currencyID', invoice.currency);
|
|
490
|
-
taxExclusiveAmountElement.textContent =
|
|
461
|
+
taxExclusiveAmountElement.textContent = amount(totals.taxBasisTotal);
|
|
491
462
|
legalMonetaryTotalNode.appendChild(taxExclusiveAmountElement);
|
|
492
463
|
|
|
493
464
|
// Tax inclusive amount
|
|
494
465
|
const taxInclusiveAmountElement = doc.createElement('cbc:TaxInclusiveAmount');
|
|
495
466
|
taxInclusiveAmountElement.setAttribute('currencyID', invoice.currency);
|
|
496
|
-
taxInclusiveAmountElement.textContent =
|
|
467
|
+
taxInclusiveAmountElement.textContent = amount(totals.grandTotal);
|
|
497
468
|
legalMonetaryTotalNode.appendChild(taxInclusiveAmountElement);
|
|
498
469
|
|
|
499
470
|
// Payable amount
|
|
500
471
|
const payableAmountElement = doc.createElement('cbc:PayableAmount');
|
|
501
472
|
payableAmountElement.setAttribute('currencyID', invoice.currency);
|
|
502
|
-
payableAmountElement.textContent =
|
|
473
|
+
payableAmountElement.textContent = amount(totals.duePayable);
|
|
503
474
|
legalMonetaryTotalNode.appendChild(payableAmountElement);
|
|
504
475
|
}
|
|
505
476
|
|
|
@@ -512,8 +483,9 @@ export class UBLEncoder extends UBLBaseEncoder {
|
|
|
512
483
|
private addInvoiceLines(doc: Document, parentElement: Element, invoice: TAccountingDoc, documentType: UBLDocumentType = UBLDocumentType.INVOICE): void {
|
|
513
484
|
if (!invoice.items) return;
|
|
514
485
|
const creditNote = documentType === UBLDocumentType.CREDIT_NOTE;
|
|
486
|
+
const totals = computeDocumentTotals(invoice);
|
|
515
487
|
|
|
516
|
-
for (const item of invoice.items) {
|
|
488
|
+
for (const [index, item] of invoice.items.entries()) {
|
|
517
489
|
const invoiceLineNode = doc.createElement(creditNote ? 'cac:CreditNoteLine' : 'cac:InvoiceLine');
|
|
518
490
|
parentElement.appendChild(invoiceLineNode);
|
|
519
491
|
|
|
@@ -529,11 +501,10 @@ export class UBLEncoder extends UBLBaseEncoder {
|
|
|
529
501
|
quantityElement.textContent = item.unitQuantity.toString();
|
|
530
502
|
invoiceLineNode.appendChild(quantityElement);
|
|
531
503
|
|
|
532
|
-
//
|
|
533
|
-
const itemNetAmount = item.unitNetPrice * item.unitQuantity;
|
|
504
|
+
// Invoice line net amount (BT-131): quantity × net price, rounded
|
|
534
505
|
const lineExtensionAmountElement = doc.createElement('cbc:LineExtensionAmount');
|
|
535
506
|
lineExtensionAmountElement.setAttribute('currencyID', invoice.currency);
|
|
536
|
-
lineExtensionAmountElement.textContent =
|
|
507
|
+
lineExtensionAmountElement.textContent = totals.lineNetAmounts[index].toFixed(totals.minorUnits);
|
|
537
508
|
invoiceLineNode.appendChild(lineExtensionAmountElement);
|
|
538
509
|
|
|
539
510
|
// Item information
|
|
@@ -15,12 +15,20 @@ export class DecimalCurrencyCalculator {
|
|
|
15
15
|
private readonly minorUnits: number;
|
|
16
16
|
private readonly roundingMode: RoundingMode;
|
|
17
17
|
|
|
18
|
+
/**
|
|
19
|
+
* @param currency The currency, whose minor unit sets the rounding scale
|
|
20
|
+
* @param roundingMode How values are rounded
|
|
21
|
+
* @param options.maxDecimals The most decimals an amount may have, when fewer than the
|
|
22
|
+
* currency's minor unit (EN 16931 allows two, BR-DEC-*)
|
|
23
|
+
*/
|
|
18
24
|
constructor(
|
|
19
25
|
currency: TCurrency,
|
|
20
|
-
roundingMode: RoundingMode = 'HALF_UP'
|
|
26
|
+
roundingMode: RoundingMode = 'HALF_UP',
|
|
27
|
+
options: { maxDecimals?: number } = {}
|
|
21
28
|
) {
|
|
22
29
|
this.currency = currency;
|
|
23
|
-
|
|
30
|
+
const minorUnits = getCurrencyMinorUnits(currency);
|
|
31
|
+
this.minorUnits = options.maxDecimals === undefined ? minorUnits : Math.min(minorUnits, options.maxDecimals);
|
|
24
32
|
this.roundingMode = roundingMode;
|
|
25
33
|
}
|
|
26
34
|
|