@fin.cx/einvoice 8.2.3 → 8.3.0

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 (41) hide show
  1. package/dist_ts/00_commitinfo_data.js +1 -1
  2. package/dist_ts/formats/cii/cii.encoder.d.ts +10 -0
  3. package/dist_ts/formats/cii/cii.encoder.js +39 -2
  4. package/dist_ts/formats/cii/cii.types.d.ts +11 -0
  5. package/dist_ts/formats/cii/cii.types.js +41 -1
  6. package/dist_ts/formats/cii/facturx/facturx.decoder.js +7 -8
  7. package/dist_ts/formats/cii/facturx/facturx.encoder.d.ts +1 -0
  8. package/dist_ts/formats/cii/facturx/facturx.encoder.js +23 -6
  9. package/dist_ts/formats/cii/zugferd/zugferd.decoder.js +7 -8
  10. package/dist_ts/formats/cii/zugferd/zugferd.encoder.d.ts +1 -0
  11. package/dist_ts/formats/cii/zugferd/zugferd.encoder.js +51 -33
  12. package/dist_ts/formats/cii/zugferd/zugferd.v1.decoder.js +8 -8
  13. package/dist_ts/formats/ubl/generic/ubl.encoder.js +16 -3
  14. package/dist_ts/formats/ubl/xrechnung/xrechnung.decoder.js +19 -5
  15. package/dist_ts/formats/ubl/xrechnung.validator.js +14 -11
  16. package/dist_ts/formats/utils/document.totals.js +5 -2
  17. package/dist_ts/formats/utils/eu.memberstates.d.ts +11 -0
  18. package/dist_ts/formats/utils/eu.memberstates.js +16 -0
  19. package/dist_ts/formats/utils/number.text.d.ts +9 -0
  20. package/dist_ts/formats/utils/number.text.js +26 -0
  21. package/dist_ts/formats/utils/vat.category.d.ts +13 -8
  22. package/dist_ts/formats/utils/vat.category.js +16 -11
  23. package/dist_ts/formats/validation/vat-categories.validator.js +3 -7
  24. package/package.json +3 -3
  25. package/readme.md +14 -6
  26. package/ts/00_commitinfo_data.ts +1 -1
  27. package/ts/formats/cii/cii.encoder.ts +39 -1
  28. package/ts/formats/cii/cii.types.ts +41 -0
  29. package/ts/formats/cii/facturx/facturx.decoder.ts +7 -8
  30. package/ts/formats/cii/facturx/facturx.encoder.ts +25 -5
  31. package/ts/formats/cii/zugferd/zugferd.decoder.ts +7 -8
  32. package/ts/formats/cii/zugferd/zugferd.encoder.ts +57 -36
  33. package/ts/formats/cii/zugferd/zugferd.v1.decoder.ts +7 -8
  34. package/ts/formats/ubl/generic/ubl.encoder.ts +18 -2
  35. package/ts/formats/ubl/xrechnung/xrechnung.decoder.ts +17 -4
  36. package/ts/formats/ubl/xrechnung.validator.ts +21 -17
  37. package/ts/formats/utils/document.totals.ts +7 -1
  38. package/ts/formats/utils/eu.memberstates.ts +16 -0
  39. package/ts/formats/utils/number.text.ts +25 -0
  40. package/ts/formats/utils/vat.category.ts +15 -10
  41. package/ts/formats/validation/vat-categories.validator.ts +2 -6
@@ -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
 
@@ -217,23 +224,24 @@ export class ZUGFeRDEncoder extends CIIBaseEncoder {
217
224
 
218
225
  // Add seller
219
226
  const sellerElement = doc.createElement('ram:SellerTradeParty');
220
- this.addPartyInfo(doc, sellerElement, invoice.from);
227
+ this.addPartyInfo(doc, sellerElement, invoice.from, 'seller');
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);
233
241
 
234
242
  // Add buyer
235
243
  const buyerElement = doc.createElement('ram:BuyerTradeParty');
236
- this.addPartyInfo(doc, buyerElement, invoice.to);
244
+ this.addPartyInfo(doc, buyerElement, invoice.to, 'buyer');
237
245
  agreementElement.appendChild(buyerElement);
238
246
  }
239
247
 
@@ -242,8 +250,9 @@ export class ZUGFeRDEncoder extends CIIBaseEncoder {
242
250
  * @param doc XML document
243
251
  * @param partyElement Party element
244
252
  * @param party Party data
253
+ * @param partyRole Whether the party is the seller or the buyer
245
254
  */
246
- private addPartyInfo(doc: Document, partyElement: Element, party: any): void {
255
+ private addPartyInfo(doc: Document, partyElement: Element, party: any, partyRole: 'seller' | 'buyer'): void {
247
256
  // Add name
248
257
  const nameElement = doc.createElement('ram:Name');
249
258
  nameElement.textContent = party.name;
@@ -310,6 +319,17 @@ export class ZUGFeRDEncoder extends CIIBaseEncoder {
310
319
  partyElement.appendChild(taxRegistrationElement);
311
320
  }
312
321
 
322
+ // Seller tax registration identifier (BT-32), the Steuernummer: scheme FC, after the VAT
323
+ // identifier. EN 16931 has no such element for the buyer, so a buyer's tax number is not written.
324
+ if (partyRole === 'seller' && party.registrationDetails?.taxNumber) {
325
+ const taxRegistrationElement = doc.createElement('ram:SpecifiedTaxRegistration');
326
+ const taxIdElement = doc.createElement('ram:ID');
327
+ taxIdElement.setAttribute('schemeID', 'FC');
328
+ taxIdElement.textContent = party.registrationDetails.taxNumber;
329
+ taxRegistrationElement.appendChild(taxIdElement);
330
+ partyElement.appendChild(taxRegistrationElement);
331
+ }
332
+
313
333
  }
314
334
 
315
335
  /**
@@ -334,33 +354,6 @@ export class ZUGFeRDEncoder extends CIIBaseEncoder {
334
354
  deliveryDateElement.appendChild(occurrenceDateElement);
335
355
  deliveryElement.appendChild(deliveryDateElement);
336
356
  }
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
357
  }
365
358
 
366
359
  /**
@@ -448,6 +441,33 @@ export class ZUGFeRDEncoder extends CIIBaseEncoder {
448
441
  // Add tax details
449
442
  this.addTaxDetails(doc, settlementElement, invoice);
450
443
 
444
+ // Invoicing period (BG-14): in the header settlement, where the CII schema puts it
445
+ if (invoice.periodOfPerformance) {
446
+ const periodElement = doc.createElement('ram:BillingSpecifiedPeriod');
447
+
448
+ // Start date
449
+ if (invoice.periodOfPerformance.from !== undefined) {
450
+ const startDateElement = doc.createElement('ram:StartDateTime');
451
+ const startDateStringElement = doc.createElement('udt:DateTimeString');
452
+ startDateStringElement.setAttribute('format', '102'); // YYYYMMDD format
453
+ startDateStringElement.textContent = this.formatDateYYYYMMDD(invoice.periodOfPerformance.from, 'BT-73 invoicing period start date');
454
+ startDateElement.appendChild(startDateStringElement);
455
+ periodElement.appendChild(startDateElement);
456
+ }
457
+
458
+ // End date
459
+ if (invoice.periodOfPerformance.to !== undefined) {
460
+ const endDateElement = doc.createElement('ram:EndDateTime');
461
+ const endDateStringElement = doc.createElement('udt:DateTimeString');
462
+ endDateStringElement.setAttribute('format', '102'); // YYYYMMDD format
463
+ endDateStringElement.textContent = this.formatDateYYYYMMDD(invoice.periodOfPerformance.to, 'BT-74 invoicing period end date');
464
+ endDateElement.appendChild(endDateStringElement);
465
+ periodElement.appendChild(endDateElement);
466
+ }
467
+
468
+ settlementElement.appendChild(periodElement);
469
+ }
470
+
451
471
  // Add totals
452
472
  this.addMonetarySummation(doc, settlementElement, invoice);
453
473
  }
@@ -581,7 +601,8 @@ export class ZUGFeRDEncoder extends CIIBaseEncoder {
581
601
  const agreementElement = doc.createElement('ram:SpecifiedLineTradeAgreement');
582
602
  const priceElement = doc.createElement('ram:NetPriceProductTradePrice');
583
603
  const chargeAmountElement = doc.createElement('ram:ChargeAmount');
584
- chargeAmountElement.textContent = item.unitNetPrice.toFixed(2);
604
+ // Item net price (BT-146) with the precision it has; EN 16931 does not limit its decimals
605
+ chargeAmountElement.textContent = toPlainDecimalString(item.unitNetPrice);
585
606
  priceElement.appendChild(chargeAmountElement);
586
607
  agreementElement.appendChild(priceElement);
587
608
  lineItemElement.appendChild(agreementElement);
@@ -589,7 +610,7 @@ export class ZUGFeRDEncoder extends CIIBaseEncoder {
589
610
  // Add delivery information (quantity)
590
611
  const deliveryElement = doc.createElement('ram:SpecifiedLineTradeDelivery');
591
612
  const quantityElement = doc.createElement('ram:BilledQuantity');
592
- quantityElement.textContent = item.unitQuantity.toString();
613
+ quantityElement.textContent = toPlainDecimalString(item.unitQuantity); // BT-129
593
614
  // a line without a unit is written without one, so that BR-23 reports it
594
615
  if (item.unitType) {
595
616
  quantityElement.setAttribute('unitCode', item.unitType);
@@ -230,13 +230,11 @@ export class ZUGFeRDV1Decoder extends CIIBaseDecoder {
230
230
  // Extract VAT ID
231
231
  const vatId = this.getText(`${partyXPath}/ram:SpecifiedTaxRegistration/ram:ID[@schemeID="VA"]`) || '';
232
232
 
233
- // the legal registration identifier, and a tax number (scheme FC) as before when that is all
234
- // the document states; the party is still read with the ZUGFeRD 2 namespaces (the known v1
235
- // header defect), so for a v1 document this finds nothing yet
236
- const registrationId =
237
- this.getText(`${partyXPath}/ram:SpecifiedLegalOrganization/ram:ID`) ||
238
- this.getText(`${partyXPath}/ram:SpecifiedTaxRegistration/ram:ID[@schemeID="FC"]`) ||
239
- '';
233
+ // the legal registration identifier and the tax number (scheme FC); the party is still read
234
+ // with the ZUGFeRD 2 namespaces (the known v1 header defect), so for a v1 document neither is
235
+ // found yet
236
+ const registrationId = this.getText(`${partyXPath}/ram:SpecifiedLegalOrganization/ram:ID`) || '';
237
+ const taxNumber = this.getText(`${partyXPath}/ram:SpecifiedTaxRegistration/ram:ID[@schemeID="FC"]`) || '';
240
238
 
241
239
  // Create contact object
242
240
  return {
@@ -249,7 +247,8 @@ export class ZUGFeRDV1Decoder extends CIIBaseDecoder {
249
247
  registrationDetails: {
250
248
  vatId: vatId,
251
249
  registrationId: registrationId,
252
- registrationName: ''
250
+ registrationName: '',
251
+ ...(taxNumber ? { taxNumber } : {})
253
252
  }
254
253
  } as business.TContact;
255
254
  }
@@ -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
@@ -276,6 +277,20 @@ export class UBLEncoder extends UBLBaseEncoder {
276
277
  partyTaxSchemeNode.appendChild(taxSchemeNode);
277
278
  this.appendElement(doc, taxSchemeNode, 'cbc:ID', 'VAT');
278
279
  }
280
+
281
+ // Seller tax registration identifier (BT-32), the Steuernummer: a party tax scheme other than
282
+ // VAT, after the VAT one; FC as in CII and the German reference examples. EN 16931 has no
283
+ // such element for the buyer, so a buyer's tax number is not written.
284
+ if (elementName === 'cac:AccountingSupplierParty' && party.registrationDetails?.taxNumber) {
285
+ const partyTaxSchemeNode = doc.createElement('cac:PartyTaxScheme');
286
+ partyNode.appendChild(partyTaxSchemeNode);
287
+
288
+ this.appendElement(doc, partyTaxSchemeNode, 'cbc:CompanyID', party.registrationDetails.taxNumber);
289
+
290
+ const taxSchemeNode = doc.createElement('cac:TaxScheme');
291
+ partyTaxSchemeNode.appendChild(taxSchemeNode);
292
+ this.appendElement(doc, taxSchemeNode, 'cbc:ID', 'FC');
293
+ }
279
294
 
280
295
  // Party legal entity (registration information)
281
296
  if (party.registrationDetails) {
@@ -495,7 +510,7 @@ export class UBLEncoder extends UBLBaseEncoder {
495
510
  if (item.unitType) {
496
511
  quantityElement.setAttribute('unitCode', item.unitType);
497
512
  }
498
- quantityElement.textContent = item.unitQuantity.toString();
513
+ quantityElement.textContent = toPlainDecimalString(item.unitQuantity); // BT-129
499
514
  invoiceLineNode.appendChild(quantityElement);
500
515
 
501
516
  // Invoice line net amount (BT-131): quantity × net price, rounded
@@ -542,7 +557,8 @@ export class UBLEncoder extends UBLBaseEncoder {
542
557
  // Price amount
543
558
  const priceAmountElement = doc.createElement('cbc:PriceAmount');
544
559
  priceAmountElement.setAttribute('currencyID', invoice.currency);
545
- priceAmountElement.textContent = item.unitNetPrice.toFixed(2);
560
+ // Item net price (BT-146) with the precision it has; EN 16931 does not limit its decimals
561
+ priceAmountElement.textContent = toPlainDecimalString(item.unitNetPrice);
546
562
  priceNode.appendChild(priceAmountElement);
547
563
  }
548
564
  }
@@ -354,6 +354,7 @@ export class XRechnungDecoder extends UBLBaseDecoder {
354
354
  let country = '';
355
355
  let countryCode = '';
356
356
  let vatId = '';
357
+ let taxNumber = '';
357
358
  let registrationId = '';
358
359
  let registrationName = '';
359
360
  let contactPhone = '';
@@ -411,10 +412,21 @@ export class XRechnungDecoder extends UBLBaseDecoder {
411
412
  }
412
413
  }
413
414
 
414
- // Extract tax information
415
+ // Extract tax information by scheme, as the EN 16931 UBL binding selects it: the VAT
416
+ // identifier (BT-31, BT-48) under the tax scheme VAT, the tax registration identifier
417
+ // (BT-32, the Steuernummer) under any other, whichever order the document lists them in;
418
+ // an entry without a tax scheme identifier is neither
415
419
  const taxSchemeNodes = this.select('./cac:PartyTaxScheme', party);
416
- if (taxSchemeNodes && Array.isArray(taxSchemeNodes) && taxSchemeNodes.length > 0) {
417
- vatId = this.getText('./cbc:CompanyID', taxSchemeNodes[0]) || '';
420
+ if (taxSchemeNodes && Array.isArray(taxSchemeNodes)) {
421
+ for (const taxSchemeNode of taxSchemeNodes) {
422
+ const companyId = this.getText('./cbc:CompanyID', taxSchemeNode);
423
+ const taxSchemeId = this.getText('./cac:TaxScheme/cbc:ID', taxSchemeNode).trim();
424
+ if (taxSchemeId === 'VAT') {
425
+ vatId = vatId || companyId;
426
+ } else if (taxSchemeId) {
427
+ taxNumber = taxNumber || companyId;
428
+ }
429
+ }
418
430
  }
419
431
 
420
432
  // Extract registration information
@@ -456,7 +468,8 @@ export class XRechnungDecoder extends UBLBaseDecoder {
456
468
  registrationDetails: {
457
469
  vatId: vatId,
458
470
  registrationId: registrationId,
459
- registrationName: registrationName
471
+ registrationName: registrationName,
472
+ ...(taxNumber ? { taxNumber } : {})
460
473
  }
461
474
  };
462
475
 
@@ -53,24 +53,28 @@ export class XRechnungValidator extends EN16931UBLValidator {
53
53
  valid = false;
54
54
  }
55
55
 
56
- // BR-DE-5: In Germany, the element "Seller VAT identifier" (BT-31) shall be provided.
57
- const sellerCountry = this.getText('//cac:AccountingSupplierParty//cac:PostalAddress//cac:Country/cbc:IdentificationCode');
58
- if (sellerCountry === 'DE' && !this.exists('//cac:AccountingSupplierParty//cac:PartyTaxScheme[cac:TaxScheme/cbc:ID="VAT"]/cbc:CompanyID')) {
56
+ // BR-DE-16 (XRechnung): when a VAT category code S, Z, E, AE, K, G, L or M is used (BT-95,
57
+ // BT-102, BT-151), the seller states a VAT identifier (BT-31) or a tax registration
58
+ // identifier (BT-32), which is any party tax scheme of the seller, or a tax representative
59
+ // (BG-11). A German seller may state its tax number instead of its VAT identification number
60
+ // (§ 14 Abs. 4 Satz 1 Nr. 2 UStG). No XRechnung rule asks for the buyer's VAT identifier.
61
+ const vatCategoryCodes = ['S', 'Z', 'E', 'AE', 'K', 'G', 'L', 'M'];
62
+ const categoryNodes = this.select(
63
+ '/*/cac:AllowanceCharge/cac:TaxCategory/cbc:ID | /*/cac:InvoiceLine/cac:Item/cac:ClassifiedTaxCategory/cbc:ID | /*/cac:CreditNoteLine/cac:Item/cac:ClassifiedTaxCategory/cbc:ID',
64
+ this.doc
65
+ );
66
+ const usesVatCategory =
67
+ Array.isArray(categoryNodes) &&
68
+ categoryNodes.some((node) => vatCategoryCodes.includes(((node as Node).textContent ?? '').trim()));
69
+ if (
70
+ usesVatCategory &&
71
+ !this.exists('/*/cac:AccountingSupplierParty/cac:Party/cac:PartyTaxScheme/cbc:CompanyID[normalize-space(.)]') &&
72
+ !this.exists('/*/cac:TaxRepresentativeParty')
73
+ ) {
59
74
  this.addError(
60
- 'BR-DE-5',
61
- 'Seller VAT identifier is required for German sellers',
62
- '//cac:AccountingSupplierParty//cac:PartyTaxScheme'
63
- );
64
- valid = false;
65
- }
66
-
67
- // BR-DE-6: In Germany, the element "Buyer VAT identifier" (BT-48) shall be provided.
68
- const buyerCountry = this.getText('//cac:AccountingCustomerParty//cac:PostalAddress//cac:Country/cbc:IdentificationCode');
69
- if (buyerCountry === 'DE' && !this.exists('//cac:AccountingCustomerParty//cac:PartyTaxScheme[cac:TaxScheme/cbc:ID="VAT"]/cbc:CompanyID')) {
70
- this.addError(
71
- 'BR-DE-6',
72
- 'Buyer VAT identifier is required for German buyers',
73
- '//cac:AccountingCustomerParty//cac:PartyTaxScheme'
75
+ 'BR-DE-16',
76
+ 'A document with the VAT category code S, Z, E, AE, K, G, L or M states the seller VAT identifier (BT-31), the seller tax registration identifier (BT-32) or a seller tax representative (BG-11)',
77
+ '//cac:AccountingSupplierParty/cac:Party/cac:PartyTaxScheme'
74
78
  );
75
79
  valid = false;
76
80
  }
@@ -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,16 @@
1
+ /**
2
+ * The member states of the European Union, by ISO 3166-1 alpha-2 code (27
3
+ * since 1 February 2020). Greece is `GR` here, as in an address; its VAT
4
+ * identification numbers carry the prefix `EL` instead.
5
+ */
6
+ export const EU_MEMBER_STATES: ReadonlySet<string> = new Set([
7
+ 'AT', 'BE', 'BG', 'HR', 'CY', 'CZ', 'DK', 'EE', 'FI', 'FR',
8
+ 'DE', 'GR', 'HU', 'IE', 'IT', 'LV', 'LT', 'LU', 'MT', 'NL',
9
+ 'PL', 'PT', 'RO', 'SK', 'SI', 'ES', 'SE',
10
+ ]);
11
+
12
+ /**
13
+ * Whether an ISO 3166-1 alpha-2 country code is that of an EU member state
14
+ * @param countryCode The country code, in upper case
15
+ */
16
+ export const isEuMemberState = (countryCode: string): boolean => EU_MEMBER_STATES.has(countryCode);
@@ -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
+ };
@@ -49,14 +49,19 @@ export const getVatExemption = (category: TVatCategoryCode, language: string | u
49
49
  * - BR-AE-05: every line of a reverse charge document has the VAT rate 0; the
50
50
  * recipient owes the tax, and the invoice states none (§ 14a Abs. 5 Satz 2
51
51
  * UStG; a stated amount would be owed under § 14c Abs. 1 UStG).
52
- * - BR-AE-02: the seller states a VAT identifier (BT-31) and the buyer a VAT
53
- * identifier (BT-48) or a legal registration identifier (BT-47). The rule
54
- * also accepts the seller's tax registration identifier (BT-32, e.g. the
55
- * Steuernummer) or a tax representative's VAT identifier (BT-63) instead of
56
- * BT-31; the envelope can state neither, so a seller without a VAT
57
- * identifier is refused. A domestic § 13b issuer that states only a
58
- * Steuernummer, which § 14 Abs. 4 Satz 1 Nr. 2 UStG allows, is therefore
59
- * refused until the envelope carries a tax number.
52
+ * - BR-AE-02: the seller states a VAT identifier (BT-31) or a tax
53
+ * registration identifier (BT-32, `registrationDetails.taxNumber`, the
54
+ * Steuernummer, which § 14 Abs. 4 Satz 1 Nr. 2 UStG allows instead), and the
55
+ * buyer a VAT identifier (BT-48) or a legal registration identifier (BT-47).
56
+ * The rule also accepts a tax representative's VAT identifier (BT-63) for
57
+ * the seller; the envelope cannot state one, so a seller with neither BT-31
58
+ * nor BT-32 is refused.
59
+ * The envelope has no place of supply, so whether a document is one for a
60
+ * supply in another member state, whose invoice states the VAT identification
61
+ * numbers of both parties (§ 14a Abs. 1 Satz 3 UStG), cannot be told here: a
62
+ * domestic § 13b supply to a buyer based in another member state is taxed in
63
+ * Germany and may state a tax number. Only what BR-AE-02 requires is checked;
64
+ * the issuing application enforces § 14a Abs. 1.
60
65
  * @param accountingDoc The document
61
66
  * @param targetFormat The format being written
62
67
  */
@@ -73,10 +78,10 @@ export const assertVatCategoryWritable = (accountingDoc: TAccountingDoc, targetF
73
78
  }
74
79
  }
75
80
  const seller = accountingDoc.from?.registrationDetails;
76
- if (!seller?.vatId) {
81
+ if (!seller?.vatId && !seller?.taxNumber) {
77
82
  refuse(
78
83
  'BR-AE-02',
79
- 'a reverse charge document states the seller VAT identifier (BT-31), from.registrationDetails.vatId is missing; the rule would also accept a tax registration identifier (BT-32) or a tax representative (BT-63), which the envelope cannot state',
84
+ 'a reverse charge document states the seller VAT identifier (BT-31) or tax registration identifier (BT-32), from.registrationDetails.vatId and .taxNumber are missing; the rule would also accept a tax representative (BT-63), which the envelope cannot state',
80
85
  );
81
86
  }
82
87
  const buyer = accountingDoc.to?.registrationDetails;
@@ -3,6 +3,7 @@ import { findInvalidItemAmounts, getTotalsSkippedResult } from '../utils/documen
3
3
  import type { TAccountingDocItem } from '@tsclass/tsclass/dist_ts/finance/index.js';
4
4
  import type { EInvoice } from '../../einvoice.js';
5
5
  import { CurrencyCalculator } from '../utils/currency.utils.js';
6
+ import { isEuMemberState } from '../utils/eu.memberstates.js';
6
7
  import type { ValidationResult } from './validation.types.js';
7
8
 
8
9
  /**
@@ -748,12 +749,7 @@ export class VATCategoriesValidator {
748
749
  }
749
750
 
750
751
  private isEUCountry(countryCode: string): boolean {
751
- const euCountries = [
752
- 'AT', 'BE', 'BG', 'HR', 'CY', 'CZ', 'DK', 'EE', 'FI', 'FR',
753
- 'DE', 'GR', 'HU', 'IE', 'IT', 'LV', 'LT', 'LU', 'MT', 'NL',
754
- 'PL', 'PT', 'RO', 'SK', 'SI', 'ES', 'SE'
755
- ];
756
- return euCountries.includes(countryCode);
752
+ return isEuMemberState(countryCode);
757
753
  }
758
754
 
759
755
  private isReverseChargeService(item: TAccountingDocItem): boolean {