@fin.cx/einvoice 8.2.3 → 8.2.4

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.
@@ -7,6 +7,7 @@ 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
9
  import { computeDocumentTotals } from '../../utils/document.totals.js';
10
+ import { toPlainDecimalString } from '../../utils/number.text.js';
10
11
 
11
12
  /**
12
13
  * Encoder for ZUGFeRD invoice format
@@ -34,6 +35,9 @@ export class ZUGFeRDEncoder extends CIIBaseEncoder {
34
35
  this.addCommonInvoiceData(xmlDoc, creditNote);
35
36
 
36
37
  // Serialize to string
38
+ // the schema order of every element, whatever order the passes above added them in
39
+ this.orderCiiElements(xmlDoc);
40
+
37
41
  return new XMLSerializer().serializeToString(xmlDoc);
38
42
  }
39
43
 
@@ -53,6 +57,9 @@ export class ZUGFeRDEncoder extends CIIBaseEncoder {
53
57
  this.addCommonInvoiceData(xmlDoc, invoice);
54
58
 
55
59
  // Serialize to string
60
+ // the schema order of every element, whatever order the passes above added them in
61
+ this.orderCiiElements(xmlDoc);
62
+
56
63
  return new XMLSerializer().serializeToString(xmlDoc);
57
64
  }
58
65
 
@@ -219,14 +226,15 @@ export class ZUGFeRDEncoder extends CIIBaseEncoder {
219
226
  const sellerElement = doc.createElement('ram:SellerTradeParty');
220
227
  this.addPartyInfo(doc, sellerElement, invoice.from);
221
228
 
222
- // Add seller electronic address if available
229
+ // Seller electronic address (BT-34): ram:URIUniversalCommunication/ram:URIID of the party; a
230
+ // trade contact (ram:DefinedTradeContact) has no URIID
223
231
  if (invoice.electronicAddress && invoice.from.type === 'company') {
224
- const contactElement = doc.createElement('ram:DefinedTradeContact');
232
+ const communicationElement = doc.createElement('ram:URIUniversalCommunication');
225
233
  const uriElement = doc.createElement('ram:URIID');
226
234
  uriElement.setAttribute('schemeID', invoice.electronicAddress.scheme);
227
235
  uriElement.textContent = invoice.electronicAddress.value;
228
- contactElement.appendChild(uriElement);
229
- sellerElement.appendChild(contactElement);
236
+ communicationElement.appendChild(uriElement);
237
+ sellerElement.appendChild(communicationElement);
230
238
  }
231
239
 
232
240
  agreementElement.appendChild(sellerElement);
@@ -334,33 +342,6 @@ export class ZUGFeRDEncoder extends CIIBaseEncoder {
334
342
  deliveryDateElement.appendChild(occurrenceDateElement);
335
343
  deliveryElement.appendChild(deliveryDateElement);
336
344
  }
337
-
338
- // Add period of performance if available
339
- if (invoice.periodOfPerformance) {
340
- const periodElement = doc.createElement('ram:BillingSpecifiedPeriod');
341
-
342
- // Start date
343
- if (invoice.periodOfPerformance.from !== undefined) {
344
- const startDateElement = doc.createElement('ram:StartDateTime');
345
- const startDateStringElement = doc.createElement('udt:DateTimeString');
346
- startDateStringElement.setAttribute('format', '102'); // YYYYMMDD format
347
- startDateStringElement.textContent = this.formatDateYYYYMMDD(invoice.periodOfPerformance.from, 'BT-73 invoicing period start date');
348
- startDateElement.appendChild(startDateStringElement);
349
- periodElement.appendChild(startDateElement);
350
- }
351
-
352
- // End date
353
- if (invoice.periodOfPerformance.to !== undefined) {
354
- const endDateElement = doc.createElement('ram:EndDateTime');
355
- const endDateStringElement = doc.createElement('udt:DateTimeString');
356
- endDateStringElement.setAttribute('format', '102'); // YYYYMMDD format
357
- endDateStringElement.textContent = this.formatDateYYYYMMDD(invoice.periodOfPerformance.to, 'BT-74 invoicing period end date');
358
- endDateElement.appendChild(endDateStringElement);
359
- periodElement.appendChild(endDateElement);
360
- }
361
-
362
- deliveryElement.appendChild(periodElement);
363
- }
364
345
  }
365
346
 
366
347
  /**
@@ -448,6 +429,33 @@ export class ZUGFeRDEncoder extends CIIBaseEncoder {
448
429
  // Add tax details
449
430
  this.addTaxDetails(doc, settlementElement, invoice);
450
431
 
432
+ // Invoicing period (BG-14): in the header settlement, where the CII schema puts it
433
+ if (invoice.periodOfPerformance) {
434
+ const periodElement = doc.createElement('ram:BillingSpecifiedPeriod');
435
+
436
+ // Start date
437
+ if (invoice.periodOfPerformance.from !== undefined) {
438
+ const startDateElement = doc.createElement('ram:StartDateTime');
439
+ const startDateStringElement = doc.createElement('udt:DateTimeString');
440
+ startDateStringElement.setAttribute('format', '102'); // YYYYMMDD format
441
+ startDateStringElement.textContent = this.formatDateYYYYMMDD(invoice.periodOfPerformance.from, 'BT-73 invoicing period start date');
442
+ startDateElement.appendChild(startDateStringElement);
443
+ periodElement.appendChild(startDateElement);
444
+ }
445
+
446
+ // End date
447
+ if (invoice.periodOfPerformance.to !== undefined) {
448
+ const endDateElement = doc.createElement('ram:EndDateTime');
449
+ const endDateStringElement = doc.createElement('udt:DateTimeString');
450
+ endDateStringElement.setAttribute('format', '102'); // YYYYMMDD format
451
+ endDateStringElement.textContent = this.formatDateYYYYMMDD(invoice.periodOfPerformance.to, 'BT-74 invoicing period end date');
452
+ endDateElement.appendChild(endDateStringElement);
453
+ periodElement.appendChild(endDateElement);
454
+ }
455
+
456
+ settlementElement.appendChild(periodElement);
457
+ }
458
+
451
459
  // Add totals
452
460
  this.addMonetarySummation(doc, settlementElement, invoice);
453
461
  }
@@ -581,7 +589,8 @@ export class ZUGFeRDEncoder extends CIIBaseEncoder {
581
589
  const agreementElement = doc.createElement('ram:SpecifiedLineTradeAgreement');
582
590
  const priceElement = doc.createElement('ram:NetPriceProductTradePrice');
583
591
  const chargeAmountElement = doc.createElement('ram:ChargeAmount');
584
- chargeAmountElement.textContent = item.unitNetPrice.toFixed(2);
592
+ // Item net price (BT-146) with the precision it has; EN 16931 does not limit its decimals
593
+ chargeAmountElement.textContent = toPlainDecimalString(item.unitNetPrice);
585
594
  priceElement.appendChild(chargeAmountElement);
586
595
  agreementElement.appendChild(priceElement);
587
596
  lineItemElement.appendChild(agreementElement);
@@ -589,7 +598,7 @@ export class ZUGFeRDEncoder extends CIIBaseEncoder {
589
598
  // Add delivery information (quantity)
590
599
  const deliveryElement = doc.createElement('ram:SpecifiedLineTradeDelivery');
591
600
  const quantityElement = doc.createElement('ram:BilledQuantity');
592
- quantityElement.textContent = item.unitQuantity.toString();
601
+ quantityElement.textContent = toPlainDecimalString(item.unitQuantity); // BT-129
593
602
  // a line without a unit is written without one, so that BR-23 reports it
594
603
  if (item.unitType) {
595
604
  quantityElement.setAttribute('unitCode', item.unitType);
@@ -7,6 +7,7 @@ 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
9
  import { computeDocumentTotals } from '../../utils/document.totals.js';
10
+ import { toPlainDecimalString } from '../../utils/number.text.js';
10
11
 
11
12
  /**
12
13
  * UBL Encoder implementation
@@ -495,7 +496,7 @@ export class UBLEncoder extends UBLBaseEncoder {
495
496
  if (item.unitType) {
496
497
  quantityElement.setAttribute('unitCode', item.unitType);
497
498
  }
498
- quantityElement.textContent = item.unitQuantity.toString();
499
+ quantityElement.textContent = toPlainDecimalString(item.unitQuantity); // BT-129
499
500
  invoiceLineNode.appendChild(quantityElement);
500
501
 
501
502
  // Invoice line net amount (BT-131): quantity × net price, rounded
@@ -542,7 +543,8 @@ export class UBLEncoder extends UBLBaseEncoder {
542
543
  // Price amount
543
544
  const priceAmountElement = doc.createElement('cbc:PriceAmount');
544
545
  priceAmountElement.setAttribute('currencyID', invoice.currency);
545
- priceAmountElement.textContent = item.unitNetPrice.toFixed(2);
546
+ // Item net price (BT-146) with the precision it has; EN 16931 does not limit its decimals
547
+ priceAmountElement.textContent = toPlainDecimalString(item.unitNetPrice);
546
548
  priceNode.appendChild(priceAmountElement);
547
549
  }
548
550
  }
@@ -1,6 +1,7 @@
1
1
  import type { TAccountingDoc } from '../../interfaces/common.js';
2
2
  import { Decimal } from './decimal.js';
3
3
  import { DecimalCurrencyCalculator } from './currency.calculator.decimal.js';
4
+ import { toPlainDecimalString } from './number.text.js';
4
5
  import { EInvoiceFormatError } from '../../errors.js';
5
6
  import type { ValidationResult } from '../validation/validation.types.js';
6
7
  import { getVatCategory, getVatExemption, type IVatExemption, type TVatCategoryCode } from './vat.category.js';
@@ -130,7 +131,12 @@ export const computeDocumentTotals = (
130
131
  );
131
132
  }
132
133
  for (const item of accountingDoc.items ?? []) {
133
- const lineNet = calculator.calculateLineNet(item.unitQuantity, item.unitNetPrice);
134
+ // quantity × net price from the numerals the encoders write, so the line net amount is the
135
+ // product of the written values, at their full precision
136
+ const lineNet = calculator.calculateLineNet(
137
+ toPlainDecimalString(item.unitQuantity),
138
+ toPlainDecimalString(item.unitNetPrice),
139
+ );
134
140
  lineNetAmounts.push(lineNet);
135
141
  lineVatCategories.push(category);
136
142
  // grouped by VAT category and rate: the same rate under two categories is two groups
@@ -0,0 +1,25 @@
1
+ /**
2
+ * A finite number as a plain decimal numeral (`xsd:decimal`), the shortest one
3
+ * that reads back as the same number, without an exponent: `0.335`, `1234.5`,
4
+ * `0.0000001` (not `1e-7`). Quantities (BT-129) and net prices (BT-146) are
5
+ * written this way, with the precision they have; EN 16931 limits neither to
6
+ * two decimals (BR-DEC-* cover amounts only).
7
+ * @param value A finite number
8
+ */
9
+ export const toPlainDecimalString = (value: number): string => {
10
+ const text = String(value);
11
+ const match = /^(-?)(\d+)(?:\.(\d+))?e([+-]\d+)$/i.exec(text);
12
+ if (!match) {
13
+ return text;
14
+ }
15
+ const [, sign, integerDigits, fractionDigits = '', exponentText] = match;
16
+ const digits = integerDigits + fractionDigits;
17
+ const point = integerDigits.length + Number(exponentText);
18
+ if (point <= 0) {
19
+ return `${sign}0.${'0'.repeat(-point)}${digits}`;
20
+ }
21
+ if (point >= digits.length) {
22
+ return `${sign}${digits}${'0'.repeat(point - digits.length)}`;
23
+ }
24
+ return `${sign}${digits.slice(0, point)}.${digits.slice(point)}`;
25
+ };