@fin.cx/einvoice 10.0.1 → 10.1.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 (44) hide show
  1. package/dist_ts/00_commitinfo_data.js +1 -1
  2. package/dist_ts/einvoice.d.ts +2 -0
  3. package/dist_ts/einvoice.js +7 -13
  4. package/dist_ts/formats/cii/cii.decoder.d.ts +11 -3
  5. package/dist_ts/formats/cii/cii.decoder.js +27 -8
  6. package/dist_ts/formats/cii/cii.encoder.d.ts +12 -0
  7. package/dist_ts/formats/cii/cii.encoder.js +36 -1
  8. package/dist_ts/formats/cii/cii.types.js +2 -1
  9. package/dist_ts/formats/cii/facturx/facturx.encoder.js +12 -8
  10. package/dist_ts/formats/cii/zugferd/zugferd.encoder.js +12 -8
  11. package/dist_ts/formats/semantic/semantic.adapter.d.ts +0 -4
  12. package/dist_ts/formats/semantic/semantic.adapter.js +7 -18
  13. package/dist_ts/formats/ubl/generic/ubl.encoder.d.ts +6 -1
  14. package/dist_ts/formats/ubl/generic/ubl.encoder.js +40 -8
  15. package/dist_ts/formats/ubl/ubl.decoder.d.ts +11 -0
  16. package/dist_ts/formats/ubl/ubl.decoder.js +30 -1
  17. package/dist_ts/formats/ubl/xrechnung/xrechnung.decoder.js +3 -3
  18. package/dist_ts/formats/utils/delivery.address.d.ts +18 -0
  19. package/dist_ts/formats/utils/delivery.address.js +30 -0
  20. package/dist_ts/formats/utils/document.totals.d.ts +5 -4
  21. package/dist_ts/formats/utils/document.totals.js +24 -8
  22. package/dist_ts/formats/utils/stated.values.js +4 -2
  23. package/dist_ts/formats/utils/vat.category.d.ts +146 -55
  24. package/dist_ts/formats/utils/vat.category.js +357 -69
  25. package/dist_ts/formats/validation/vat-categories.validator.d.ts +14 -15
  26. package/dist_ts/formats/validation/vat-categories.validator.js +123 -85
  27. package/package.json +3 -3
  28. package/readme.md +55 -19
  29. package/ts/00_commitinfo_data.ts +1 -1
  30. package/ts/einvoice.ts +8 -14
  31. package/ts/formats/cii/cii.decoder.ts +28 -7
  32. package/ts/formats/cii/cii.encoder.ts +36 -0
  33. package/ts/formats/cii/cii.types.ts +1 -0
  34. package/ts/formats/cii/facturx/facturx.encoder.ts +12 -7
  35. package/ts/formats/cii/zugferd/zugferd.encoder.ts +12 -7
  36. package/ts/formats/semantic/semantic.adapter.ts +6 -15
  37. package/ts/formats/ubl/generic/ubl.encoder.ts +40 -7
  38. package/ts/formats/ubl/ubl.decoder.ts +31 -0
  39. package/ts/formats/ubl/xrechnung/xrechnung.decoder.ts +2 -2
  40. package/ts/formats/utils/delivery.address.ts +36 -0
  41. package/ts/formats/utils/document.totals.ts +38 -11
  42. package/ts/formats/utils/stated.values.ts +4 -1
  43. package/ts/formats/utils/vat.category.ts +426 -95
  44. package/ts/formats/validation/vat-categories.validator.ts +169 -88
@@ -284,6 +284,42 @@ export abstract class CIIBaseEncoder extends BaseEncoder {
284
284
  agreementElement.appendChild(buyerReferenceElement);
285
285
  }
286
286
 
287
+ /**
288
+ * Adds the deliver to address (BG-15) of `metadata.deliveryAddress` as
289
+ * `ram:ShipToTradeParty/ram:PostalTradeAddress` of the header delivery, when
290
+ * it states a country (BT-80, which BR-57 requires of the address): the
291
+ * street and house number as line one and two (BT-75, BT-76), the city
292
+ * (BT-77), the post code (BT-78), the country subdivision (BT-79) and the
293
+ * country. An intra-community supply states it (BR-IC-12).
294
+ * @param doc XML document
295
+ * @param deliveryElement The header trade delivery
296
+ * @param invoice Invoice data
297
+ */
298
+ protected addDeliverToAddress(doc: Document, deliveryElement: Element, invoice: TAccountingDoc): void {
299
+ const address = (invoice as TAccountingDoc & { metadata?: IEInvoiceMetadata }).metadata?.deliveryAddress;
300
+ const countryCode = address?.countryCode?.trim();
301
+ if (!address || !countryCode) {
302
+ return;
303
+ }
304
+ const shipTo = doc.createElement('ram:ShipToTradeParty');
305
+ const postal = doc.createElement('ram:PostalTradeAddress');
306
+ const append = (name: string, text: string | undefined) => {
307
+ if (text?.trim()) {
308
+ const element = doc.createElement(name);
309
+ element.textContent = text.trim();
310
+ postal.appendChild(element);
311
+ }
312
+ };
313
+ append('ram:PostcodeCode', address.postalCode);
314
+ append('ram:LineOne', address.streetName);
315
+ append('ram:LineTwo', address.houseNumber);
316
+ append('ram:CityName', address.city);
317
+ append('ram:CountryID', countryCode);
318
+ append('ram:CountrySubDivisionName', address.countrySubdivision);
319
+ shipTo.appendChild(postal);
320
+ deliveryElement.insertBefore(shipTo, deliveryElement.firstChild);
321
+ }
322
+
287
323
  /**
288
324
  * Adds the invoicing period (BG-14) as `ram:BillingSpecifiedPeriod` of the
289
325
  * header settlement, where the CII schema puts it: the start date (BT-73) and
@@ -59,6 +59,7 @@ export const CII_CHILD_SEQUENCES: Readonly<Record<string, readonly string[]>> =
59
59
  ApplicableHeaderTradeAgreement: ['Reference', 'BuyerReference', 'SellerTradeParty', 'BuyerTradeParty', 'SalesAgentTradeParty', 'BuyerRequisitionerTradeParty', 'BuyerAssignedAccountantTradeParty', 'SellerAssignedAccountantTradeParty', 'BuyerTaxRepresentativeTradeParty', 'SellerTaxRepresentativeTradeParty', 'ProductEndUserTradeParty', 'ApplicableTradeDeliveryTerms', 'SellerOrderReferencedDocument', 'BuyerOrderReferencedDocument', 'QuotationReferencedDocument', 'OrderResponseReferencedDocument', 'ContractReferencedDocument', 'DemandForecastReferencedDocument', 'SupplyInstructionReferencedDocument', 'PromotionalDealReferencedDocument', 'PriceListReferencedDocument', 'AdditionalReferencedDocument', 'RequisitionerReferencedDocument', 'BuyerAgentTradeParty', 'PurchaseConditionsReferencedDocument', 'SpecifiedProcuringProject', 'UltimateCustomerOrderReferencedDocument'],
60
60
  SellerTradeParty: ['ID', 'GlobalID', 'Name', 'RoleCode', 'Description', 'SpecifiedLegalOrganization', 'DefinedTradeContact', 'PostalTradeAddress', 'URIUniversalCommunication', 'SpecifiedTaxRegistration', 'EndPointURIUniversalCommunication', 'LogoAssociatedSpecifiedBinaryFile'],
61
61
  BuyerTradeParty: ['ID', 'GlobalID', 'Name', 'RoleCode', 'Description', 'SpecifiedLegalOrganization', 'DefinedTradeContact', 'PostalTradeAddress', 'URIUniversalCommunication', 'SpecifiedTaxRegistration', 'EndPointURIUniversalCommunication', 'LogoAssociatedSpecifiedBinaryFile'],
62
+ ShipToTradeParty: ['ID', 'GlobalID', 'Name', 'RoleCode', 'Description', 'SpecifiedLegalOrganization', 'DefinedTradeContact', 'PostalTradeAddress', 'URIUniversalCommunication', 'SpecifiedTaxRegistration', 'EndPointURIUniversalCommunication', 'LogoAssociatedSpecifiedBinaryFile'],
62
63
  PostalTradeAddress: ['ID', 'PostcodeCode', 'PostOfficeBox', 'BuildingName', 'LineOne', 'LineTwo', 'LineThree', 'LineFour', 'LineFive', 'StreetName', 'CityName', 'CitySubDivisionName', 'CountryID', 'CountryName', 'CountrySubDivisionID', 'CountrySubDivisionName', 'AttentionOf', 'CareOf', 'BuildingNumber', 'DepartmentName', 'AdditionalStreetName'],
63
64
  DefinedTradeContact: ['ID', 'PersonName', 'DepartmentName', 'TypeCode', 'JobTitle', 'Responsibility', 'PersonID', 'TelephoneUniversalCommunication', 'DirectTelephoneUniversalCommunication', 'MobileTelephoneUniversalCommunication', 'FaxUniversalCommunication', 'EmailURIUniversalCommunication', 'TelexUniversalCommunication', 'VOIPUniversalCommunication', 'InstantMessagingUniversalCommunication', 'SpecifiedNote', 'SpecifiedContactPerson'],
64
65
  SpecifiedLegalOrganization: ['LegalClassificationCode', 'Name', 'ID', 'TradingBusinessName', 'PostalTradeAddress', 'AuthorizedLegalRegistration'],
@@ -308,6 +308,9 @@ export class FacturXEncoder extends CIIBaseEncoder {
308
308
  const deliveryElement = doc.createElement('ram:ApplicableHeaderTradeDelivery');
309
309
  transactionElement.appendChild(deliveryElement);
310
310
 
311
+ // Deliver to address (BG-15), when the document states its country (BT-80)
312
+ this.addDeliverToAddress(doc, deliveryElement, invoice);
313
+
311
314
  // Add delivery date if available
312
315
  if (invoice.deliveryDate !== undefined) {
313
316
  const deliveryDateElement = doc.createElement('ram:ActualDeliverySupplyChainEvent');
@@ -351,12 +354,12 @@ export class FacturXEncoder extends CIIBaseEncoder {
351
354
  };
352
355
  appendText('ram:CalculatedAmount', taxAmount.toFixed(totals.minorUnits)); // BT-117
353
356
  appendText('ram:TypeCode', 'VAT');
354
- if (exemption) {
355
- appendText('ram:ExemptionReason', exemption.reason); // BT-120 (BR-AE-10)
357
+ if (exemption?.reason) {
358
+ appendText('ram:ExemptionReason', exemption.reason); // BT-120 (BR-x-10)
356
359
  }
357
360
  appendText('ram:BasisAmount', taxableAmount.toFixed(totals.minorUnits)); // BT-116
358
361
  appendText('ram:CategoryCode', category); // BT-118
359
- if (exemption) {
362
+ if (exemption?.code) {
360
363
  appendText('ram:ExemptionReasonCode', exemption.code); // BT-121
361
364
  }
362
365
  appendText('ram:RateApplicablePercent', rate.toString()); // BT-119
@@ -483,10 +486,12 @@ export class FacturXEncoder extends CIIBaseEncoder {
483
486
  taxCategoryCodeElement.textContent = totals.lineVatCategories[index]; // BT-151
484
487
  taxElement.appendChild(taxCategoryCodeElement);
485
488
 
486
- // Add tax rate
487
- const taxRateElement = doc.createElement('ram:RateApplicablePercent');
488
- taxRateElement.textContent = item.vatPercentage.toString();
489
- taxElement.appendChild(taxRateElement);
489
+ // Invoiced item VAT rate (BT-152); an item not subject to VAT has none (BR-O-05)
490
+ if (totals.lineVatCategories[index] !== 'O') {
491
+ const taxRateElement = doc.createElement('ram:RateApplicablePercent');
492
+ taxRateElement.textContent = item.vatPercentage.toString();
493
+ taxElement.appendChild(taxRateElement);
494
+ }
490
495
 
491
496
  settlementElement.appendChild(taxElement);
492
497
 
@@ -320,6 +320,9 @@ export class ZUGFeRDEncoder extends CIIBaseEncoder {
320
320
  const deliveryElement = doc.createElement('ram:ApplicableHeaderTradeDelivery');
321
321
  transactionElement.appendChild(deliveryElement);
322
322
 
323
+ // Deliver to address (BG-15), when the document states its country (BT-80)
324
+ this.addDeliverToAddress(doc, deliveryElement, invoice);
325
+
323
326
  // Add delivery date if available
324
327
  if (invoice.deliveryDate !== undefined) {
325
328
  const deliveryDateElement = doc.createElement('ram:ActualDeliverySupplyChainEvent');
@@ -449,8 +452,8 @@ export class ZUGFeRDEncoder extends CIIBaseEncoder {
449
452
  typeCodeElement.textContent = 'VAT';
450
453
  taxElement.appendChild(typeCodeElement);
451
454
 
452
- // VAT exemption reason text (BT-120), for reverse charge (BR-AE-10)
453
- if (exemption) {
455
+ // VAT exemption reason text (BT-120), for the categories that state one (BR-x-10)
456
+ if (exemption?.reason) {
454
457
  const exemptionReasonElement = doc.createElement('ram:ExemptionReason');
455
458
  exemptionReasonElement.textContent = exemption.reason;
456
459
  taxElement.appendChild(exemptionReasonElement);
@@ -467,7 +470,7 @@ export class ZUGFeRDEncoder extends CIIBaseEncoder {
467
470
  taxElement.appendChild(categoryCodeElement);
468
471
 
469
472
  // VAT exemption reason code (BT-121)
470
- if (exemption) {
473
+ if (exemption?.code) {
471
474
  const exemptionReasonCodeElement = doc.createElement('ram:ExemptionReasonCode');
472
475
  exemptionReasonCodeElement.textContent = exemption.code;
473
476
  taxElement.appendChild(exemptionReasonCodeElement);
@@ -591,10 +594,12 @@ export class ZUGFeRDEncoder extends CIIBaseEncoder {
591
594
  taxCategoryCodeElement.textContent = totals.lineVatCategories[index]; // BT-151
592
595
  taxElement.appendChild(taxCategoryCodeElement);
593
596
 
594
- // Add tax rate
595
- const taxRateElement = doc.createElement('ram:RateApplicablePercent');
596
- taxRateElement.textContent = item.vatPercentage.toString();
597
- taxElement.appendChild(taxRateElement);
597
+ // Invoiced item VAT rate (BT-152); an item not subject to VAT has none (BR-O-05)
598
+ if (totals.lineVatCategories[index] !== 'O') {
599
+ const taxRateElement = doc.createElement('ram:RateApplicablePercent');
600
+ taxRateElement.textContent = item.vatPercentage.toString();
601
+ taxElement.appendChild(taxRateElement);
602
+ }
598
603
 
599
604
  settlementElement.appendChild(taxElement);
600
605
 
@@ -7,6 +7,7 @@ import { EInvoice } from '../../einvoice.js';
7
7
  import { getAccountingDocType, getDocumentTypeCode } from '../utils/document.typecode.js';
8
8
  import { computeDocumentTotals } from '../utils/document.totals.js';
9
9
  import { getPaymentTotals } from '../utils/paid.amount.js';
10
+ import { getItemVatCategory } from '../utils/vat.category.js';
10
11
  import type {
11
12
  EN16931SemanticModel,
12
13
  Seller,
@@ -119,7 +120,7 @@ export class SemanticModelAdapter {
119
120
  additionalDocuments: invoice.metadata?.extensions?.supportingDocuments,
120
121
 
121
122
  // Invoice lines
122
- invoiceLines: this.mapInvoiceLines(invoice.items || [])
123
+ invoiceLines: this.mapInvoiceLines(invoice)
123
124
  };
124
125
  }
125
126
 
@@ -443,9 +444,8 @@ export class SemanticModelAdapter {
443
444
  /**
444
445
  * Map invoice lines
445
446
  */
446
- private mapInvoiceLines(items: EInvoice['items']): InvoiceLine[] {
447
- if (!items) return [];
448
-
447
+ private mapInvoiceLines(invoice: EInvoice): InvoiceLine[] {
448
+ const items = invoice.items ?? [];
449
449
  return items.map((item, index) => ({
450
450
  identifier: (index + 1).toString(),
451
451
  note: (item as any).description || (item as any).text || '',
@@ -464,7 +464,8 @@ export class SemanticModelAdapter {
464
464
  itemPriceBaseQuantity: (item as any).priceBaseQuantity || 1
465
465
  },
466
466
  vatInformation: {
467
- categoryCode: this.mapVATCategory(item.vatPercentage),
467
+ // the category the encoders write: the item's own, else AE or S (BT-151)
468
+ categoryCode: getItemVatCategory(invoice, item),
468
469
  rate: item.vatPercentage
469
470
  },
470
471
  itemInformation: {
@@ -480,16 +481,6 @@ export class SemanticModelAdapter {
480
481
  }));
481
482
  }
482
483
 
483
- /**
484
- * Map VAT category from percentage
485
- */
486
- private mapVATCategory(percentage?: number): string {
487
- if (percentage === undefined || percentage === null) return 'S';
488
- if (percentage === 0) return 'Z';
489
- if (percentage > 0) return 'S';
490
- return 'E'; // Exempt
491
- }
492
-
493
484
  /**
494
485
  * Reverse map seller
495
486
  */
@@ -11,6 +11,7 @@ import { getPaymentTotals } from '../../utils/paid.amount.js';
11
11
  import { toPlainDecimalString } from '../../utils/number.text.js';
12
12
  import { resolveCountryCode } from '../../utils/country.code.js';
13
13
  import { getPartyIdentifiers } from '../../utils/party.identifier.js';
14
+ import type { IEInvoiceMetadata } from '../../../interfaces/en16931-metadata.js';
14
15
 
15
16
  /**
16
17
  * UBL Encoder implementation
@@ -189,17 +190,45 @@ export class UBLEncoder extends UBLBaseEncoder {
189
190
  }
190
191
 
191
192
  /**
192
- * Writes the actual delivery date (BT-72) the document states.
193
+ * Writes the actual delivery date (BT-72) and the deliver to address (BG-15)
194
+ * the document states. The address is `metadata.deliveryAddress` when it
195
+ * states a country (BT-80, which BR-57 requires of the address): the street
196
+ * (BT-75), the house number, the city (BT-77), the post code (BT-78), the
197
+ * country subdivision (BT-79) and the country. An intra-community supply
198
+ * states it (BR-IC-12).
193
199
  * @param doc XML document
194
200
  * @param root Document root
195
201
  * @param invoice Accounting document
196
202
  */
197
203
  private addDelivery(doc: Document, root: Element, invoice: TAccountingDoc): void {
198
- if (invoice.deliveryDate === undefined) {
204
+ const address = (invoice as TAccountingDoc & { metadata?: IEInvoiceMetadata }).metadata?.deliveryAddress;
205
+ const countryCode = address?.countryCode?.trim();
206
+ if (invoice.deliveryDate === undefined && !countryCode) {
199
207
  return;
200
208
  }
201
209
  const delivery = doc.createElement('cac:Delivery');
202
- this.appendElement(doc, delivery, 'cbc:ActualDeliveryDate', this.formatDate(invoice.deliveryDate, 'BT-72 actual delivery date'));
210
+ if (invoice.deliveryDate !== undefined) {
211
+ this.appendElement(doc, delivery, 'cbc:ActualDeliveryDate', this.formatDate(invoice.deliveryDate, 'BT-72 actual delivery date'));
212
+ }
213
+ if (address && countryCode) {
214
+ const location = doc.createElement('cac:DeliveryLocation');
215
+ const addressNode = doc.createElement('cac:Address');
216
+ const append = (name: string, text: string | undefined) => {
217
+ if (text?.trim()) {
218
+ this.appendElement(doc, addressNode, name, text.trim());
219
+ }
220
+ };
221
+ append('cbc:StreetName', address.streetName);
222
+ append('cbc:BuildingNumber', address.houseNumber);
223
+ append('cbc:CityName', address.city);
224
+ append('cbc:PostalZone', address.postalCode);
225
+ append('cbc:CountrySubentity', address.countrySubdivision);
226
+ const country = doc.createElement('cac:Country');
227
+ this.appendElement(doc, country, 'cbc:IdentificationCode', countryCode);
228
+ addressNode.appendChild(country);
229
+ location.appendChild(addressNode);
230
+ delivery.appendChild(location);
231
+ }
203
232
  root.appendChild(delivery);
204
233
  }
205
234
 
@@ -456,9 +485,11 @@ export class UBLEncoder extends UBLBaseEncoder {
456
485
  this.appendElement(doc, taxCategoryNode, 'cbc:ID', category);
457
486
  this.appendElement(doc, taxCategoryNode, 'cbc:Percent', rate.toFixed(2));
458
487
 
459
- // VAT exemption reason code (BT-121) and text (BT-120), for reverse charge (BR-AE-10)
460
- if (exemption) {
488
+ // VAT exemption reason code (BT-121) and text (BT-120), for the categories that state one (BR-x-10)
489
+ if (exemption?.code) {
461
490
  this.appendElement(doc, taxCategoryNode, 'cbc:TaxExemptionReasonCode', exemption.code);
491
+ }
492
+ if (exemption?.reason) {
462
493
  this.appendElement(doc, taxCategoryNode, 'cbc:TaxExemptionReason', exemption.reason);
463
494
  }
464
495
 
@@ -573,8 +604,10 @@ export class UBLEncoder extends UBLBaseEncoder {
573
604
  // Invoiced item VAT category code (BT-151)
574
605
  this.appendElement(doc, classifiedTaxCategoryNode, 'cbc:ID', totals.lineVatCategories[index]);
575
606
 
576
- // Tax percent with 2 decimal places
577
- this.appendElement(doc, classifiedTaxCategoryNode, 'cbc:Percent', item.vatPercentage.toFixed(2));
607
+ // Invoiced item VAT rate (BT-152) with 2 decimal places; an item not subject to VAT has none (BR-O-05)
608
+ if (totals.lineVatCategories[index] !== 'O') {
609
+ this.appendElement(doc, classifiedTaxCategoryNode, 'cbc:Percent', item.vatPercentage.toFixed(2));
610
+ }
578
611
 
579
612
  // Tax scheme
580
613
  const taxSchemeNode = doc.createElement('cac:TaxScheme');
@@ -5,6 +5,8 @@ import { DOMParser, xpath } from '../../plugins.js';
5
5
  import { EInvoiceParsingError } from '../../errors.js';
6
6
  import { getAccountingDocType } from '../utils/document.typecode.js';
7
7
  import { readUblStatedValues } from '../utils/stated.values.js';
8
+ import { applyStatedVatCategories } from '../utils/vat.category.js';
9
+ import { deliveryAddressOf, withDeliveryAddress, type TDeliveryAddress } from '../utils/delivery.address.js';
8
10
  import type { IEInvoiceStatedValues } from '../../interfaces/stated.values.js';
9
11
 
10
12
  const ublParser = new DOMParser();
@@ -54,6 +56,35 @@ export abstract class UBLBaseDecoder extends BaseDecoder {
54
56
  // the values the document states, read first: an amount that is no number is refused
55
57
  this.statedValues = this.assertStatedValuesReadable(readUblStatedValues(this.doc), 'ubl');
56
58
 
59
+ // a CreditNote root is a credit note whatever its type code says
60
+ const document = await this.decodeByType();
61
+ // every item with the VAT category its line states and its breakdown's exemption reason
62
+ applyStatedVatCategories(document.items, this.statedValues);
63
+ return withDeliveryAddress(document, this.readDeliveryAddress());
64
+ }
65
+
66
+ /**
67
+ * Reads the deliver to address (BG-15) of the first delivery: the street
68
+ * (BT-75), the house number, the city (BT-77), the post code (BT-78), the
69
+ * country subdivision (BT-79) and the country (BT-80); undefined when the
70
+ * document states no address with a country
71
+ */
72
+ private readDeliveryAddress(): TDeliveryAddress | undefined {
73
+ const address = '/*/cac:Delivery[1]/cac:DeliveryLocation/cac:Address';
74
+ return deliveryAddressOf({
75
+ streetName: this.getText(`${address}/cbc:StreetName`, this.doc),
76
+ houseNumber: this.getText(`${address}/cbc:BuildingNumber`, this.doc),
77
+ city: this.getText(`${address}/cbc:CityName`, this.doc),
78
+ postalCode: this.getText(`${address}/cbc:PostalZone`, this.doc),
79
+ countrySubdivision: this.getText(`${address}/cbc:CountrySubentity`, this.doc),
80
+ countryCode: this.getText(`${address}/cac:Country/cbc:IdentificationCode`, this.doc),
81
+ });
82
+ }
83
+
84
+ /**
85
+ * Decodes the document by its root and document type code (BT-3)
86
+ */
87
+ private async decodeByType(): Promise<TAccountingDoc> {
57
88
  // a CreditNote root is a credit note whatever its type code says
58
89
  if (this.getDocumentType() === UBLDocumentType.CREDIT_NOTE) {
59
90
  return this.decodeCreditNote();
@@ -265,8 +265,8 @@ export class XRechnungDecoder extends UBLBaseDecoder {
265
265
  subject: subject,
266
266
  items: items,
267
267
  dueInDays: dueInDays,
268
- // reverse charge when every line is VAT category AE; the envelope states it for the
269
- // whole document, so a document mixing AE lines with others cannot say it
268
+ // reverse charge when every line is VAT category AE; in a document mixing AE lines with
269
+ // others each item carries its category (applyStatedVatCategories)
270
270
  reverseCharge: this.isReverseChargeDocument(),
271
271
  currency: currencyCode as finance.TCurrency,
272
272
  notes: notes,
@@ -0,0 +1,36 @@
1
+ import type { TAccountingDoc } from '../../interfaces/common.js';
2
+ import type { IEInvoiceMetadata } from '../../interfaces/en16931-metadata.js';
3
+
4
+ /** The deliver to address (BG-15) of a document, as `metadata.deliveryAddress` carries it */
5
+ export type TDeliveryAddress = NonNullable<IEInvoiceMetadata['deliveryAddress']>;
6
+
7
+ /**
8
+ * The deliver to address from the texts a document states, each trimmed and
9
+ * left out when empty; undefined when it states no country (BT-80), which
10
+ * every deliver to address has (BR-57)
11
+ * @param texts The texts read, '' where the document states none
12
+ */
13
+ export const deliveryAddressOf = (texts: Record<keyof TDeliveryAddress, string>): TDeliveryAddress | undefined => {
14
+ const address: TDeliveryAddress = {};
15
+ for (const [field, text] of Object.entries(texts) as Array<[keyof TDeliveryAddress, string]>) {
16
+ if (text.trim()) {
17
+ address[field] = text.trim();
18
+ }
19
+ }
20
+ return address.countryCode ? address : undefined;
21
+ };
22
+
23
+ /**
24
+ * The decoded document with the deliver to address in its metadata, or
25
+ * unchanged when it states none
26
+ * @param document The decoded document
27
+ * @param address The deliver to address read
28
+ */
29
+ export const withDeliveryAddress = <TDocument extends TAccountingDoc>(document: TDocument, address: TDeliveryAddress | undefined): TDocument => {
30
+ if (!address) {
31
+ return document;
32
+ }
33
+ const withMetadata = document as TDocument & { metadata?: IEInvoiceMetadata };
34
+ withMetadata.metadata = { ...withMetadata.metadata, deliveryAddress: address };
35
+ return withMetadata;
36
+ };
@@ -4,7 +4,14 @@ import { DecimalCurrencyCalculator } from './currency.calculator.decimal.js';
4
4
  import { toPlainDecimalString } from './number.text.js';
5
5
  import { EInvoiceFormatError } from '../../errors.js';
6
6
  import type { ValidationResult } from '../validation/validation.types.js';
7
- import { getVatCategory, getVatExemption, type IVatExemption, type TVatCategoryCode } from './vat.category.js';
7
+ import {
8
+ getDefaultVatExemption,
9
+ getItemVatCategory,
10
+ getItemVatExemption,
11
+ mergeVatExemption,
12
+ type IVatExemption,
13
+ type TVatCategoryCode,
14
+ } from './vat.category.js';
8
15
 
9
16
  /** An item amount that is no finite number, so no total can be computed from it */
10
17
  export interface IInvalidItemAmount {
@@ -49,6 +56,14 @@ export const getTotalsSkippedResult = (invalid: IInvalidItemAmount[], source: st
49
56
  field: invalid.map((entry) => `items[${entry.index}].${entry.field}`).join(', '),
50
57
  });
51
58
 
59
+ /**
60
+ * The VAT categories whose tax amount (BT-117) is the taxable amount × rate:
61
+ * standard rated (BR-S-09), IGIC (BR-AF-09) and IPSI (BR-AG-09). Every other
62
+ * category states the tax 0 (BR-Z-09, BR-E-09, BR-AE-09, BR-IC-09, BR-G-09,
63
+ * BR-O-09).
64
+ */
65
+ const TAXED_CATEGORIES: readonly TVatCategoryCode[] = ['S', 'L', 'M'];
66
+
52
67
  /** The most decimals an amount may have in EN 16931 (BR-DEC-09 to BR-DEC-23) */
53
68
  export const EN16931_MAX_AMOUNT_DECIMALS = 2;
54
69
 
@@ -65,8 +80,8 @@ export const getDocumentCalculator = (currency: TAccountingDoc['currency']): Dec
65
80
  /**
66
81
  * The VAT breakdown of one VAT category and rate (BG-23): the category
67
82
  * (BT-118), the rate (BT-119), the taxable amount (BT-116), the tax on it
68
- * (BT-117) and, for a category that states one, the exemption reason
69
- * (BT-121, BT-120).
83
+ * (BT-117) and the exemption reason (BT-121, BT-120): the one its items state,
84
+ * else the one its category states by default (`getDefaultVatExemption`).
70
85
  */
71
86
  export interface IDocumentVatGroup {
72
87
  category: TVatCategoryCode;
@@ -86,7 +101,7 @@ export interface IDocumentTotals {
86
101
  minorUnits: number;
87
102
  /** the Invoice line net amount (BT-131) of each item, in item order: quantity × net price, rounded */
88
103
  lineNetAmounts: Decimal[];
89
- /** the VAT category (BT-151) of each item, in item order */
104
+ /** the VAT category (BT-151) of each item, in item order: the one it states, else AE or S (`getItemVatCategory`) */
90
105
  lineVatCategories: TVatCategoryCode[];
91
106
  /** Sum of Invoice line net amount (BT-106) */
92
107
  lineTotal: Decimal;
@@ -107,7 +122,8 @@ export interface IDocumentTotals {
107
122
  * rounded; the sum of line net amounts (BT-106) is their sum (BR-CO-10);
108
123
  * each VAT category taxable amount (BT-116) is the sum of the line net amounts
109
124
  * of that category and rate, and its tax amount (BT-117) is BT-116 × rate / 100
110
- * rounded (BR-CO-17), 0 for reverse charge (BR-AE-09); the total VAT (BT-110) is the sum of the category tax amounts
125
+ * rounded (BR-CO-17), 0 for the categories whose rate is 0 or that state no tax
126
+ * (BR-Z-09, BR-E-09, BR-AE-09, BR-IC-09, BR-G-09, BR-O-09); the total VAT (BT-110) is the sum of the category tax amounts
111
127
  * (BR-CO-14); the total with VAT (BT-112) is BT-109 + BT-110 (BR-CO-15).
112
128
  * @param accountingDoc The document
113
129
  */
@@ -116,10 +132,12 @@ export const computeDocumentTotals = (
116
132
  ): IDocumentTotals => {
117
133
  const calculator = getDocumentCalculator(accountingDoc.currency);
118
134
  const minorUnits = calculator.getCurrencyInfo().minorUnits;
119
- const category = getVatCategory(accountingDoc);
120
135
  const lineNetAmounts: Decimal[] = [];
121
136
  const lineVatCategories: TVatCategoryCode[] = [];
122
- const taxableByGroup = new Map<string, { category: TVatCategoryCode; rate: number; taxable: Decimal }>();
137
+ const taxableByGroup = new Map<
138
+ string,
139
+ { category: TVatCategoryCode; rate: number; taxable: Decimal; exemptions: IVatExemption[] }
140
+ >();
123
141
  // an amount that is no number cannot be computed with; it is refused, not treated as 0
124
142
  const [firstInvalid] = findInvalidItemAmounts(accountingDoc.items);
125
143
  if (firstInvalid) {
@@ -136,21 +154,30 @@ export const computeDocumentTotals = (
136
154
  toPlainDecimalString(item.unitNetPrice),
137
155
  );
138
156
  lineNetAmounts.push(lineNet);
157
+ const category = getItemVatCategory(accountingDoc, item);
139
158
  lineVatCategories.push(category);
140
159
  // grouped by VAT category and rate: the same rate under two categories is two groups
141
160
  const key = `${category}|${item.vatPercentage}`;
142
- const group = taxableByGroup.get(key) ?? { category, rate: item.vatPercentage, taxable: Decimal.ZERO };
161
+ const group = taxableByGroup.get(key) ?? { category, rate: item.vatPercentage, taxable: Decimal.ZERO, exemptions: [] };
143
162
  group.taxable = group.taxable.add(lineNet);
163
+ const exemption = getItemVatExemption(item);
164
+ if (exemption) {
165
+ group.exemptions.push(exemption);
166
+ }
144
167
  taxableByGroup.set(key, group);
145
168
  }
146
169
  const vatGroups: IDocumentVatGroup[] = [...taxableByGroup.values()].map((group) => {
147
- const exemption = getVatExemption(group.category, accountingDoc.language);
170
+ // the reason the items state; items of one breakdown that state different ones are refused
171
+ // on export and reported by the validators (findVatCategoryViolations)
172
+ const exemption = mergeVatExemption(group.exemptions[0], getDefaultVatExemption(group.category, accountingDoc.language));
148
173
  return {
149
174
  category: group.category,
150
175
  rate: group.rate,
151
176
  taxableAmount: calculator.round(group.taxable),
152
- // reverse charge states no tax: the recipient owes it (BR-AE-09)
153
- taxAmount: group.category === 'AE' ? calculator.round(Decimal.ZERO) : calculator.calculateVAT(group.taxable, group.rate),
177
+ // only a taxed category states tax; reverse charge states none, the recipient owes it (BR-AE-09)
178
+ taxAmount: TAXED_CATEGORIES.includes(group.category)
179
+ ? calculator.calculateVAT(group.taxable, group.rate)
180
+ : calculator.round(Decimal.ZERO),
154
181
  ...(exemption ? { exemption } : {}),
155
182
  };
156
183
  });
@@ -485,8 +485,11 @@ export const findEnvelopeDifferences = (
485
485
  }
486
486
  });
487
487
  values.vatBreakdown.forEach((group, index) => {
488
+ // a breakdown not subject to VAT (O) may state no rate; the envelope's has the rate 0
488
489
  const computed = totals.vatGroups.find(
489
- (candidate) => candidate.category === group.categoryCode && group.rate !== undefined && decimal(candidate.rate).equals(decimal(group.rate)),
490
+ (candidate) =>
491
+ candidate.category === group.categoryCode &&
492
+ (group.rate === undefined ? group.categoryCode === 'O' : decimal(candidate.rate).equals(decimal(group.rate))),
490
493
  );
491
494
  if (!computed) {
492
495
  differs('BG-23', `vatBreakdown[${index}]`, `${group.categoryCode ?? '?'} ${group.rate ?? ''}`.trim(), 'no breakdown of that category and rate');