@fin.cx/einvoice 8.2.0 → 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.
Files changed (59) 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/base/base.decoder.js +4 -3
  4. package/dist_ts/formats/cii/cii.decoder.d.ts +15 -0
  5. package/dist_ts/formats/cii/cii.decoder.js +28 -1
  6. package/dist_ts/formats/cii/facturx/facturx.decoder.js +3 -5
  7. package/dist_ts/formats/cii/facturx/facturx.encoder.js +46 -53
  8. package/dist_ts/formats/cii/zugferd/zugferd.decoder.js +4 -7
  9. package/dist_ts/formats/cii/zugferd/zugferd.encoder.js +42 -63
  10. package/dist_ts/formats/cii/zugferd/zugferd.v1.decoder.d.ts +12 -0
  11. package/dist_ts/formats/cii/zugferd/zugferd.v1.decoder.js +25 -7
  12. package/dist_ts/formats/semantic/semantic.validator.js +20 -1
  13. package/dist_ts/formats/ubl/generic/ubl.encoder.js +28 -52
  14. package/dist_ts/formats/ubl/ubl.types.d.ts +6 -0
  15. package/dist_ts/formats/ubl/ubl.types.js +9 -3
  16. package/dist_ts/formats/ubl/xrechnung/xrechnung.decoder.js +10 -8
  17. package/dist_ts/formats/ubl/xrechnung/xrechnung.encoder.js +4 -11
  18. package/dist_ts/formats/utils/currency.calculator.decimal.d.ts +9 -1
  19. package/dist_ts/formats/utils/currency.calculator.decimal.js +10 -3
  20. package/dist_ts/formats/utils/date.value.d.ts +18 -0
  21. package/dist_ts/formats/utils/date.value.js +30 -1
  22. package/dist_ts/formats/utils/document.totals.d.ts +79 -0
  23. package/dist_ts/formats/utils/document.totals.js +93 -0
  24. package/dist_ts/formats/utils/format.detector.js +6 -4
  25. package/dist_ts/formats/utils/payment.terms.d.ts +18 -0
  26. package/dist_ts/formats/utils/payment.terms.js +32 -0
  27. package/dist_ts/formats/validation/codelist.validator.js +3 -11
  28. package/dist_ts/formats/validation/en16931.business-rules.validator.js +38 -44
  29. package/dist_ts/formats/validation/facturx.validator.js +19 -5
  30. package/dist_ts/formats/validation/integrated.validator.js +6 -3
  31. package/dist_ts/formats/validation/vat-categories.validator.js +9 -1
  32. package/dist_ts/formats/validation/xrechnung.validator.js +6 -4
  33. package/package.json +2 -2
  34. package/readme.md +17 -0
  35. package/ts/00_commitinfo_data.ts +1 -1
  36. package/ts/einvoice.ts +11 -25
  37. package/ts/formats/base/base.decoder.ts +3 -2
  38. package/ts/formats/cii/cii.decoder.ts +31 -0
  39. package/ts/formats/cii/facturx/facturx.decoder.ts +2 -4
  40. package/ts/formats/cii/facturx/facturx.encoder.ts +47 -61
  41. package/ts/formats/cii/zugferd/zugferd.decoder.ts +3 -6
  42. package/ts/formats/cii/zugferd/zugferd.encoder.ts +42 -73
  43. package/ts/formats/cii/zugferd/zugferd.v1.decoder.ts +28 -6
  44. package/ts/formats/semantic/semantic.validator.ts +20 -0
  45. package/ts/formats/ubl/generic/ubl.encoder.ts +33 -63
  46. package/ts/formats/ubl/ubl.types.ts +8 -2
  47. package/ts/formats/ubl/xrechnung/xrechnung.decoder.ts +11 -7
  48. package/ts/formats/ubl/xrechnung/xrechnung.encoder.ts +3 -11
  49. package/ts/formats/utils/currency.calculator.decimal.ts +10 -2
  50. package/ts/formats/utils/date.value.ts +31 -0
  51. package/ts/formats/utils/document.totals.ts +144 -0
  52. package/ts/formats/utils/format.detector.ts +5 -5
  53. package/ts/formats/utils/payment.terms.ts +37 -0
  54. package/ts/formats/validation/codelist.validator.ts +3 -12
  55. package/ts/formats/validation/en16931.business-rules.validator.ts +44 -51
  56. package/ts/formats/validation/facturx.validator.ts +19 -4
  57. package/ts/formats/validation/integrated.validator.ts +5 -2
  58. package/ts/formats/validation/vat-categories.validator.ts +9 -0
  59. package/ts/formats/validation/xrechnung.validator.ts +5 -3
@@ -127,6 +127,37 @@ export abstract class CIIBaseDecoder extends BaseDecoder {
127
127
  return references;
128
128
  }
129
129
 
130
+ /**
131
+ * Reads the invoice issue date (BT-2); a missing or invalid one is refused
132
+ * with an `EInvoiceParsingError`, as no other date can stand in for it.
133
+ */
134
+ protected extractIssueDate(): number {
135
+ const issueDatePath = '/rsm:CrossIndustryInvoice/rsm:ExchangedDocument/ram:IssueDateTime/udt:DateTimeString';
136
+ return this.parseRequiredCIIDate(this.getText(issueDatePath), this.getText(`${issueDatePath}/@format`).trim());
137
+ }
138
+
139
+ /**
140
+ * Reads the payment due date (BT-9) of the header payment terms; undefined
141
+ * when the document states none.
142
+ */
143
+ protected extractDueDate(): number | undefined {
144
+ const dueDatePath =
145
+ '/rsm:CrossIndustryInvoice/rsm:SupplyChainTradeTransaction/ram:ApplicableHeaderTradeSettlement/ram:SpecifiedTradePaymentTerms/ram:DueDateDateTime/udt:DateTimeString';
146
+ const dueDate = this.getText(dueDatePath).trim();
147
+ if (!dueDate) {
148
+ return undefined;
149
+ }
150
+ return this.parseRequiredCIIDate(dueDate, this.getText(`${dueDatePath}/@format`).trim());
151
+ }
152
+
153
+ /**
154
+ * The payment term in days from the issue date to the stated due date, or
155
+ * `absentDueInDays` when the document states no due date.
156
+ */
157
+ protected dueInDaysOf(issueDate: number, dueDate: number | undefined, absentDueInDays: number): number {
158
+ return dueDate === undefined ? absentDueInDays : Math.round((dueDate - issueDate) / (1000 * 60 * 60 * 24));
159
+ }
160
+
130
161
  /**
131
162
  * Reads the actual delivery date (BT-72) of the header delivery; undefined
132
163
  * when the document does not state one.
@@ -67,10 +67,8 @@ export class FacturXDecoder extends CIIBaseDecoder {
67
67
  const items = this.extractItems();
68
68
 
69
69
  // Extract due date
70
- const dueDateStr = this.getText('//ram:SpecifiedTradePaymentTerms/ram:DueDateDateTime/udt:DateTimeString');
71
- const dueDateFormat = this.getText('//ram:SpecifiedTradePaymentTerms/ram:DueDateDateTime/udt:DateTimeString/@format');
72
- const dueDate = dueDateStr ? this.parseCIIDate(dueDateStr, dueDateFormat) : issueDate;
73
- const dueInDays = Math.round((dueDate - issueDate) / (1000 * 60 * 60 * 24));
70
+ // no due date stated: `dueInDays` cannot express it yet (required field in @tsclass/tsclass); value unchanged: 0 days
71
+ const dueInDays = this.dueInDaysOf(issueDate, this.extractDueDate(), 0);
74
72
 
75
73
  // Extract currency
76
74
  const currencyCode = this.getText('//ram:InvoiceCurrencyCode') || 'EUR';
@@ -2,11 +2,9 @@ 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';
9
- import { getWritableDate } from '../../utils/date.value.js';
6
+ import { computeDocumentTotals } from '../../utils/document.totals.js';
7
+ import { getWritableDate, getWritableDueDate } from '../../utils/date.value.js';
10
8
 
11
9
  /**
12
10
  * Encoder for Factur-X invoice format
@@ -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
 
@@ -325,8 +343,7 @@ export class FacturXEncoder extends CIIBaseEncoder {
325
343
  dateStringElement.setAttribute('format', '102'); // YYYYMMDD format
326
344
 
327
345
  // Calculate due date
328
- const dueDate = new Date(invoice.date);
329
- dueDate.setDate(dueDate.getDate() + invoice.dueInDays);
346
+ const dueDate = getWritableDueDate(invoice.date, invoice.dueInDays, 'cii');
330
347
 
331
348
  dateStringElement.textContent = this.formatDateYYYYMMDD(dueDate.getTime(), 'BT-9 payment due date');
332
349
  dueDateElement.appendChild(dateStringElement);
@@ -334,50 +351,21 @@ export class FacturXEncoder extends CIIBaseEncoder {
334
351
 
335
352
  settlementElement.appendChild(paymentTermsElement);
336
353
 
337
- // Add totals
354
+ // Add totals, in the order of the CII schema
338
355
  const monetarySummationElement = doc.createElement('ram:SpecifiedTradeSettlementHeaderMonetarySummation');
339
-
340
- // Calculate totals with decimal arithmetic to avoid floating point drift.
341
- const calculator = new DecimalCurrencyCalculator(invoice.currency);
342
- const minorUnits = getCurrencyMinorUnits(invoice.currency);
343
- let totalNetAmount = Decimal.ZERO;
344
- let totalTaxAmount = Decimal.ZERO;
345
-
346
- // Calculate from items
347
- if (invoice.items) {
348
- for (const item of invoice.items) {
349
- const itemNetAmount = calculator.calculateLineNet(item.unitQuantity, item.unitNetPrice);
350
- const itemTaxAmount = calculator.calculateVAT(itemNetAmount, item.vatPercentage);
351
-
352
- totalNetAmount = totalNetAmount.add(itemNetAmount);
353
- 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);
354
361
  }
355
- }
356
-
357
- totalNetAmount = calculator.round(totalNetAmount);
358
- totalTaxAmount = calculator.round(totalTaxAmount);
359
- const totalGrossAmount = calculator.calculateGrossAmount(totalNetAmount, totalTaxAmount);
360
-
361
- // Add line total amount
362
- const lineTotalElement = doc.createElement('ram:LineTotalAmount');
363
- lineTotalElement.textContent = totalNetAmount.toFixed(minorUnits);
364
- monetarySummationElement.appendChild(lineTotalElement);
365
-
366
- // Add tax total amount
367
- const taxTotalElement = doc.createElement('ram:TaxTotalAmount');
368
- taxTotalElement.textContent = totalTaxAmount.toFixed(minorUnits);
369
- taxTotalElement.setAttribute('currencyID', invoice.currency);
370
- monetarySummationElement.appendChild(taxTotalElement);
371
-
372
- // Add grand total amount
373
- const grandTotalElement = doc.createElement('ram:GrandTotalAmount');
374
- grandTotalElement.textContent = totalGrossAmount.toFixed(minorUnits);
375
- monetarySummationElement.appendChild(grandTotalElement);
376
-
377
- // Add due payable amount
378
- const duePayableElement = doc.createElement('ram:DuePayableAmount');
379
- duePayableElement.textContent = totalGrossAmount.toFixed(minorUnits);
380
- 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
381
369
 
382
370
  settlementElement.appendChild(monetarySummationElement);
383
371
  }
@@ -389,12 +377,12 @@ export class FacturXEncoder extends CIIBaseEncoder {
389
377
  * @param invoice Invoice data
390
378
  */
391
379
  private addLineItems(doc: Document, transactionElement: Element, invoice: TAccountingDoc): void {
392
- const calculator = new DecimalCurrencyCalculator(invoice.currency);
393
- const minorUnits = getCurrencyMinorUnits(invoice.currency);
380
+ // the line net amounts of the one EN 16931 totals computation
381
+ const totals = computeDocumentTotals(invoice);
394
382
 
395
383
  // Add each line item
396
384
  if (invoice.items) {
397
- for (const item of invoice.items) {
385
+ for (const [index, item] of invoice.items.entries()) {
398
386
  // Create line item element
399
387
  const lineItemElement = doc.createElement('ram:IncludedSupplyChainTradeLineItem');
400
388
 
@@ -465,15 +453,12 @@ export class FacturXEncoder extends CIIBaseEncoder {
465
453
 
466
454
  settlementElement.appendChild(taxElement);
467
455
 
468
- // Add monetary summation
469
- const monetarySummationElement = doc.createElement('ram:SpecifiedLineTradeSettlementMonetarySummation');
470
-
471
- // Calculate item total with decimal arithmetic and currency rounding.
472
- 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');
473
458
 
474
- // Add line total amount
459
+ // Invoice line net amount (BT-131): quantity × net price, rounded
475
460
  const lineTotalElement = doc.createElement('ram:LineTotalAmount');
476
- lineTotalElement.textContent = itemNetAmount.toFixed(minorUnits);
461
+ lineTotalElement.textContent = totals.lineNetAmounts[index].toFixed(totals.minorUnits);
477
462
  monetarySummationElement.appendChild(lineTotalElement);
478
463
 
479
464
  settlementElement.appendChild(monetarySummationElement);
@@ -493,9 +478,10 @@ export class FacturXEncoder extends CIIBaseEncoder {
493
478
  */
494
479
  private formatDateYYYYMMDD(timestamp: number, field: string): string {
495
480
  const date = getWritableDate(timestamp, field, 'cii');
496
- const year = date.getFullYear();
497
- const month = (date.getMonth() + 1).toString().padStart(2, '0');
498
- const day = date.getDate().toString().padStart(2, '0');
481
+ // the UTC fields: a calendar day is its UTC midnight, whatever the server's zone
482
+ const year = date.getUTCFullYear();
483
+ const month = (date.getUTCMonth() + 1).toString().padStart(2, '0');
484
+ const day = date.getUTCDate().toString().padStart(2, '0');
499
485
  return `${year}${month}${day}`;
500
486
  }
501
487
  }
@@ -44,9 +44,7 @@ export class ZUGFeRDDecoder extends CIIBaseDecoder {
44
44
  const invoiceId = this.getText('//rsm:ExchangedDocument/ram:ID');
45
45
 
46
46
  // Extract issue date
47
- const issueDateStr = this.getText('//ram:IssueDateTime/udt:DateTimeString');
48
- const issueDateFormat = this.getText('//ram:IssueDateTime/udt:DateTimeString/@format');
49
- const issueDate = this.parseCIIDate(issueDateStr, issueDateFormat);
47
+ const issueDate = this.extractIssueDate();
50
48
 
51
49
  // Extract seller information
52
50
  const seller = this.extractParty('//ram:SellerTradeParty');
@@ -58,9 +56,8 @@ export class ZUGFeRDDecoder extends CIIBaseDecoder {
58
56
  const items = this.extractItems();
59
57
 
60
58
  // Extract due date
61
- const dueDateStr = this.getText('//ram:SpecifiedTradePaymentTerms/ram:DueDateDateTime/udt:DateTimeString');
62
- const dueDate = dueDateStr ? new Date(dueDateStr).getTime() : Date.now();
63
- const dueInDays = Math.round((dueDate - issueDate) / (1000 * 60 * 60 * 24));
59
+ // no due date stated: `dueInDays` cannot express it yet (required field in @tsclass/tsclass); value unchanged: the days from the issue date to the moment of decoding
60
+ const dueInDays = this.dueInDaysOf(issueDate, this.extractDueDate(), Math.round((Date.now() - issueDate) / (1000 * 60 * 60 * 24)));
64
61
 
65
62
  // Extract currency
66
63
  const currencyCode = this.getText('//ram:InvoiceCurrencyCode') || 'EUR';
@@ -4,7 +4,9 @@ import { ZUGFERD_PROFILE_IDS } from './zugferd.types.js';
4
4
  import { CIIProfile } from '../cii.types.js';
5
5
  import { DOMParser, XMLSerializer } from '../../../plugins.js';
6
6
  import { getDocumentTypeCode } from '../../utils/document.typecode.js';
7
- import { getWritableDate } from '../../utils/date.value.js';
7
+ import { getWritableDate, getWritableDueDate } from '../../utils/date.value.js';
8
+ import { getPaymentTermsNote } from '../../utils/payment.terms.js';
9
+ import { computeDocumentTotals } from '../../utils/document.totals.js';
8
10
 
9
11
  /**
10
12
  * Encoder for ZUGFeRD invoice format
@@ -382,7 +384,10 @@ export class ZUGFeRDEncoder extends CIIBaseEncoder {
382
384
  if (invoice.paymentOptions) {
383
385
  // Add payment instructions as description - this is generic enough to work with any payment type
384
386
  const descriptionElement = doc.createElement('ram:Description');
385
- descriptionElement.textContent = `Due in ${invoice.dueInDays} days. ${invoice.paymentOptions.description || ''}`;
387
+ descriptionElement.textContent = [
388
+ getPaymentTermsNote(invoice.date, invoice.dueInDays, invoice.language, 'cii'),
389
+ invoice.paymentOptions.description,
390
+ ].filter(Boolean).join('. ');
386
391
  paymentTermsElement.appendChild(descriptionElement);
387
392
  }
388
393
 
@@ -392,8 +397,7 @@ export class ZUGFeRDEncoder extends CIIBaseEncoder {
392
397
  dateStringElement.setAttribute('format', '102'); // YYYYMMDD format
393
398
 
394
399
  // Calculate due date
395
- const dueDate = new Date(invoice.date);
396
- dueDate.setDate(dueDate.getDate() + invoice.dueInDays);
400
+ const dueDate = getWritableDueDate(invoice.date, invoice.dueInDays, 'cii');
397
401
 
398
402
  dateStringElement.textContent = this.formatDateYYYYMMDD(dueDate.getTime(), 'BT-9 payment due date');
399
403
  dueDateElement.appendChild(dateStringElement);
@@ -453,30 +457,16 @@ export class ZUGFeRDEncoder extends CIIBaseEncoder {
453
457
  * @param invoice Invoice data
454
458
  */
455
459
  private addTaxDetails(doc: Document, settlementElement: Element, invoice: TAccountingDoc): void {
456
- // Calculate tax categories and totals
457
- const taxCategories = new Map<number, number>(); // Map of VAT rate to net amount
460
+ // the EN 16931 totals in decimal arithmetic, rounded as the business rules check them
461
+ const totals = computeDocumentTotals(invoice);
458
462
 
459
- // Calculate from items
460
- if (invoice.items) {
461
- for (const item of invoice.items) {
462
- const itemNetAmount = item.unitNetPrice * item.unitQuantity;
463
- const vatRate = item.vatPercentage;
464
-
465
- const currentAmount = taxCategories.get(vatRate) || 0;
466
- taxCategories.set(vatRate, currentAmount + itemNetAmount);
467
- }
468
- }
469
-
470
- // Add each tax category
471
- for (const [rate, baseAmount] of taxCategories.entries()) {
463
+ // VAT breakdown (BG-23), one per rate
464
+ for (const { rate, taxableAmount, taxAmount } of totals.vatGroups) {
472
465
  const taxElement = doc.createElement('ram:ApplicableTradeTax');
473
-
474
- // Calculate tax amount
475
- const taxAmount = baseAmount * (rate / 100);
476
-
477
- // Add calculated amount
466
+
467
+ // VAT category tax amount (BT-117)
478
468
  const calculatedAmountElement = doc.createElement('ram:CalculatedAmount');
479
- calculatedAmountElement.textContent = taxAmount.toFixed(2);
469
+ calculatedAmountElement.textContent = taxAmount.toFixed(totals.minorUnits);
480
470
  taxElement.appendChild(calculatedAmountElement);
481
471
 
482
472
  // Add type code (VAT)
@@ -484,9 +474,9 @@ export class ZUGFeRDEncoder extends CIIBaseEncoder {
484
474
  typeCodeElement.textContent = 'VAT';
485
475
  taxElement.appendChild(typeCodeElement);
486
476
 
487
- // Add basis amount
477
+ // VAT category taxable amount (BT-116)
488
478
  const basisAmountElement = doc.createElement('ram:BasisAmount');
489
- basisAmountElement.textContent = baseAmount.toFixed(2);
479
+ basisAmountElement.textContent = taxableAmount.toFixed(totals.minorUnits);
490
480
  taxElement.appendChild(basisAmountElement);
491
481
 
492
482
  // Add category code
@@ -512,43 +502,23 @@ export class ZUGFeRDEncoder extends CIIBaseEncoder {
512
502
  private addMonetarySummation(doc: Document, settlementElement: Element, invoice: TAccountingDoc): void {
513
503
  const monetarySummationElement = doc.createElement('ram:SpecifiedTradeSettlementHeaderMonetarySummation');
514
504
 
515
- // Calculate totals
516
- let totalNetAmount = 0;
517
- let totalTaxAmount = 0;
518
-
519
- // Calculate from items
520
- if (invoice.items) {
521
- for (const item of invoice.items) {
522
- const itemNetAmount = item.unitNetPrice * item.unitQuantity;
523
- const itemTaxAmount = itemNetAmount * (item.vatPercentage / 100);
524
-
525
- totalNetAmount += itemNetAmount;
526
- 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);
527
512
  }
528
- }
529
-
530
- const totalGrossAmount = totalNetAmount + totalTaxAmount;
513
+ monetarySummationElement.appendChild(element);
514
+ };
531
515
 
532
- // Add line total amount
533
- const lineTotalElement = doc.createElement('ram:LineTotalAmount');
534
- lineTotalElement.textContent = totalNetAmount.toFixed(2);
535
- monetarySummationElement.appendChild(lineTotalElement);
536
-
537
- // Add tax total amount
538
- const taxTotalElement = doc.createElement('ram:TaxTotalAmount');
539
- taxTotalElement.textContent = totalTaxAmount.toFixed(2);
540
- taxTotalElement.setAttribute('currencyID', invoice.currency);
541
- monetarySummationElement.appendChild(taxTotalElement);
542
-
543
- // Add grand total amount
544
- const grandTotalElement = doc.createElement('ram:GrandTotalAmount');
545
- grandTotalElement.textContent = totalGrossAmount.toFixed(2);
546
- monetarySummationElement.appendChild(grandTotalElement);
547
-
548
- // Add due payable amount
549
- const duePayableElement = doc.createElement('ram:DuePayableAmount');
550
- duePayableElement.textContent = totalGrossAmount.toFixed(2);
551
- 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
552
522
 
553
523
  settlementElement.appendChild(monetarySummationElement);
554
524
  }
@@ -562,7 +532,8 @@ export class ZUGFeRDEncoder extends CIIBaseEncoder {
562
532
  private addLineItems(doc: Document, transactionElement: Element, invoice: TAccountingDoc): void {
563
533
  // Add each line item
564
534
  if (invoice.items) {
565
- for (const item of invoice.items) {
535
+ const totals = computeDocumentTotals(invoice);
536
+ for (const [index, item] of invoice.items.entries()) {
566
537
  // Create line item element
567
538
  const lineItemElement = doc.createElement('ram:IncludedSupplyChainTradeLineItem');
568
539
 
@@ -633,15 +604,12 @@ export class ZUGFeRDEncoder extends CIIBaseEncoder {
633
604
 
634
605
  settlementElement.appendChild(taxElement);
635
606
 
636
- // Add monetary summation
637
- const monetarySummationElement = doc.createElement('ram:SpecifiedLineTradeSettlementMonetarySummation');
638
-
639
- // Calculate item total
640
- const itemNetAmount = item.unitNetPrice * item.unitQuantity;
607
+ // Line monetary summation, as the CII schema names it
608
+ const monetarySummationElement = doc.createElement('ram:SpecifiedTradeSettlementLineMonetarySummation');
641
609
 
642
- // Add line total amount
610
+ // Invoice line net amount (BT-131): quantity × net price, rounded
643
611
  const lineTotalElement = doc.createElement('ram:LineTotalAmount');
644
- lineTotalElement.textContent = itemNetAmount.toFixed(2);
612
+ lineTotalElement.textContent = totals.lineNetAmounts[index].toFixed(totals.minorUnits);
645
613
  monetarySummationElement.appendChild(lineTotalElement);
646
614
 
647
615
  settlementElement.appendChild(monetarySummationElement);
@@ -661,9 +629,10 @@ export class ZUGFeRDEncoder extends CIIBaseEncoder {
661
629
  */
662
630
  private formatDateYYYYMMDD(timestamp: number, field: string): string {
663
631
  const date = getWritableDate(timestamp, field, 'cii');
664
- const year = date.getFullYear();
665
- const month = (date.getMonth() + 1).toString().padStart(2, '0');
666
- const day = date.getDate().toString().padStart(2, '0');
632
+ // the UTC fields: a calendar day is its UTC midnight, whatever the server's zone
633
+ const year = date.getUTCFullYear();
634
+ const month = (date.getUTCMonth() + 1).toString().padStart(2, '0');
635
+ const day = date.getUTCDate().toString().padStart(2, '0');
667
636
  return `${year}${month}${day}`;
668
637
  }
669
638
  }
@@ -35,6 +35,31 @@ export class ZUGFeRDV1Decoder extends CIIBaseDecoder {
35
35
  };
36
36
  }
37
37
 
38
+ /**
39
+ * Reads the issue date of the v1 header (`rsm:HeaderExchangedDocument`); a
40
+ * missing or invalid one is refused with an `EInvoiceParsingError`.
41
+ */
42
+ protected override extractIssueDate(): number {
43
+ const issueDatePath = '/rsm:CrossIndustryDocument/rsm:HeaderExchangedDocument/ram:IssueDateTime/udt:DateTimeString';
44
+ return this.parseRequiredCIIDate(this.v1Text(issueDatePath), this.v1Text(`${issueDatePath}/@format`));
45
+ }
46
+
47
+ /**
48
+ * Reads the payment due date of the v1 header payment terms; undefined when
49
+ * the document states none.
50
+ */
51
+ protected override extractDueDate(): number | undefined {
52
+ const dueDatePath =
53
+ '/rsm:CrossIndustryDocument/rsm:SpecifiedSupplyChainTradeTransaction/ram:ApplicableSupplyChainTradeSettlement/ram:SpecifiedTradePaymentTerms/ram:DueDateDateTime/udt:DateTimeString';
54
+ const dueDate = this.v1Text(dueDatePath);
55
+ return dueDate ? this.parseRequiredCIIDate(dueDate, this.v1Text(`${dueDatePath}/@format`)) : undefined;
56
+ }
57
+
58
+ /** The trimmed text of the first node a v1 path selects, or '' */
59
+ private v1Text(path: string): string {
60
+ return String(zugferdV1Select(`string(${path})`, this.doc)).trim();
61
+ }
62
+
38
63
  /**
39
64
  * Reads the actual delivery date of the v1 header delivery
40
65
  * (`ram:ApplicableSupplyChainTradeDelivery`); undefined when the document
@@ -82,9 +107,7 @@ export class ZUGFeRDV1Decoder extends CIIBaseDecoder {
82
107
  const invoiceId = this.getText('//ram:ID');
83
108
 
84
109
  // Extract issue date
85
- const issueDateStr = this.getText('//ram:IssueDateTime/udt:DateTimeString');
86
- const issueDateFormat = this.getText('//ram:IssueDateTime/udt:DateTimeString/@format');
87
- const issueDate = this.parseCIIDate(issueDateStr, issueDateFormat);
110
+ const issueDate = this.extractIssueDate();
88
111
 
89
112
  // Extract seller information
90
113
  const seller = this.extractParty('//ram:SellerTradeParty');
@@ -96,9 +119,8 @@ export class ZUGFeRDV1Decoder extends CIIBaseDecoder {
96
119
  const items = this.extractItems();
97
120
 
98
121
  // Extract due date
99
- const dueDateStr = this.getText('//ram:SpecifiedTradePaymentTerms/ram:DueDateDateTime/udt:DateTimeString');
100
- const dueDate = dueDateStr ? new Date(dueDateStr).getTime() : Date.now();
101
- const dueInDays = Math.round((dueDate - issueDate) / (1000 * 60 * 60 * 24));
122
+ // no due date stated: `dueInDays` cannot express it yet (required field in @tsclass/tsclass); value unchanged: 0 days, what this decoder returned before it read the v1 dates
123
+ const dueInDays = this.dueInDaysOf(issueDate, this.extractDueDate(), 0);
102
124
 
103
125
  // Extract currency
104
126
  const currencyCode = this.getText('//ram:InvoiceCurrencyCode') || 'EUR';
@@ -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);