@fin.cx/einvoice 8.2.2 → 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.
- package/dist_ts/00_commitinfo_data.js +1 -1
- package/dist_ts/formats/cii/cii.decoder.d.ts +6 -0
- package/dist_ts/formats/cii/cii.decoder.js +11 -1
- package/dist_ts/formats/cii/cii.encoder.d.ts +10 -0
- package/dist_ts/formats/cii/cii.encoder.js +42 -2
- package/dist_ts/formats/cii/cii.types.d.ts +11 -0
- package/dist_ts/formats/cii/cii.types.js +41 -1
- package/dist_ts/formats/cii/facturx/facturx.decoder.js +8 -4
- package/dist_ts/formats/cii/facturx/facturx.encoder.js +30 -17
- package/dist_ts/formats/cii/zugferd/zugferd.decoder.js +8 -4
- package/dist_ts/formats/cii/zugferd/zugferd.encoder.js +63 -43
- package/dist_ts/formats/cii/zugferd/zugferd.v1.decoder.d.ts +5 -0
- package/dist_ts/formats/cii/zugferd/zugferd.v1.decoder.js +18 -5
- package/dist_ts/formats/ubl/generic/ubl.encoder.js +14 -15
- package/dist_ts/formats/ubl/ubl.encoder.js +4 -1
- package/dist_ts/formats/ubl/xrechnung/xrechnung.decoder.d.ts +4 -0
- package/dist_ts/formats/ubl/xrechnung/xrechnung.decoder.js +12 -2
- package/dist_ts/formats/utils/document.totals.d.ts +16 -6
- package/dist_ts/formats/utils/document.totals.js +29 -11
- package/dist_ts/formats/utils/number.text.d.ts +9 -0
- package/dist_ts/formats/utils/number.text.js +26 -0
- package/dist_ts/formats/utils/vat.category.d.ts +50 -0
- package/dist_ts/formats/utils/vat.category.js +64 -0
- package/package.json +2 -2
- package/readme.md +25 -0
- package/ts/00_commitinfo_data.ts +1 -1
- package/ts/formats/cii/cii.decoder.ts +14 -0
- package/ts/formats/cii/cii.encoder.ts +42 -1
- package/ts/formats/cii/cii.types.ts +41 -0
- package/ts/formats/cii/facturx/facturx.decoder.ts +8 -3
- package/ts/formats/cii/facturx/facturx.encoder.ts +32 -16
- package/ts/formats/cii/zugferd/zugferd.decoder.ts +8 -3
- package/ts/formats/cii/zugferd/zugferd.encoder.ts +71 -46
- package/ts/formats/cii/zugferd/zugferd.v1.decoder.ts +22 -4
- package/ts/formats/ubl/generic/ubl.encoder.ts +13 -15
- package/ts/formats/ubl/ubl.encoder.ts +3 -0
- package/ts/formats/ubl/xrechnung/xrechnung.decoder.ts +15 -1
- package/ts/formats/utils/document.totals.ts +43 -14
- package/ts/formats/utils/number.text.ts +25 -0
- package/ts/formats/utils/vat.category.ts +89 -0
|
@@ -84,8 +84,8 @@ export class ZUGFeRDDecoder extends CIIBaseDecoder {
|
|
|
84
84
|
// Extract the actual delivery date, if stated
|
|
85
85
|
const deliveryDate = this.extractDeliveryDate();
|
|
86
86
|
|
|
87
|
-
//
|
|
88
|
-
const reverseCharge = this.
|
|
87
|
+
// Reverse charge: every line has the VAT category AE
|
|
88
|
+
const reverseCharge = this.isReverseChargeDocument();
|
|
89
89
|
|
|
90
90
|
// Create the common invoice data
|
|
91
91
|
const invoiceData = {
|
|
@@ -161,7 +161,12 @@ export class ZUGFeRDDecoder extends CIIBaseDecoder {
|
|
|
161
161
|
const vatId = this.getText(`${partyXPath}/ram:SpecifiedTaxRegistration/ram:ID[@schemeID="VA"]`) || '';
|
|
162
162
|
|
|
163
163
|
// Extract registration ID
|
|
164
|
-
|
|
164
|
+
// the legal registration identifier (BT-30, BT-47); a document that states only a tax
|
|
165
|
+
// number (scheme FC, BT-32) keeps it here as before, the envelope has no field of its own for it
|
|
166
|
+
const registrationId =
|
|
167
|
+
this.getText(`${partyXPath}/ram:SpecifiedLegalOrganization/ram:ID`) ||
|
|
168
|
+
this.getText(`${partyXPath}/ram:SpecifiedTaxRegistration/ram:ID[@schemeID="FC"]`) ||
|
|
169
|
+
'';
|
|
165
170
|
|
|
166
171
|
// Create contact object
|
|
167
172
|
return {
|
|
@@ -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
|
-
//
|
|
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
|
|
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
|
-
|
|
229
|
-
sellerElement.appendChild(
|
|
236
|
+
communicationElement.appendChild(uriElement);
|
|
237
|
+
sellerElement.appendChild(communicationElement);
|
|
230
238
|
}
|
|
231
239
|
|
|
232
240
|
agreementElement.appendChild(sellerElement);
|
|
@@ -249,6 +257,17 @@ export class ZUGFeRDEncoder extends CIIBaseEncoder {
|
|
|
249
257
|
nameElement.textContent = party.name;
|
|
250
258
|
partyElement.appendChild(nameElement);
|
|
251
259
|
|
|
260
|
+
// Legal registration identifier (BT-30 seller, BT-47 buyer), e.g. the commercial register
|
|
261
|
+
// number; in the schema order it follows the name. It is no tax registration: the scheme
|
|
262
|
+
// FC of ram:SpecifiedTaxRegistration is the seller's tax number (BT-32)
|
|
263
|
+
if (party.registrationDetails && party.registrationDetails.registrationId) {
|
|
264
|
+
const legalOrganizationElement = doc.createElement('ram:SpecifiedLegalOrganization');
|
|
265
|
+
const legalIdElement = doc.createElement('ram:ID');
|
|
266
|
+
legalIdElement.textContent = party.registrationDetails.registrationId;
|
|
267
|
+
legalOrganizationElement.appendChild(legalIdElement);
|
|
268
|
+
partyElement.appendChild(legalOrganizationElement);
|
|
269
|
+
}
|
|
270
|
+
|
|
252
271
|
// Add postal address
|
|
253
272
|
const addressElement = doc.createElement('ram:PostalTradeAddress');
|
|
254
273
|
|
|
@@ -299,15 +318,6 @@ export class ZUGFeRDEncoder extends CIIBaseEncoder {
|
|
|
299
318
|
partyElement.appendChild(taxRegistrationElement);
|
|
300
319
|
}
|
|
301
320
|
|
|
302
|
-
// Add registration ID if available
|
|
303
|
-
if (party.registrationDetails && party.registrationDetails.registrationId) {
|
|
304
|
-
const regRegistrationElement = doc.createElement('ram:SpecifiedTaxRegistration');
|
|
305
|
-
const regIdElement = doc.createElement('ram:ID');
|
|
306
|
-
regIdElement.setAttribute('schemeID', 'FC');
|
|
307
|
-
regIdElement.textContent = party.registrationDetails.registrationId;
|
|
308
|
-
regRegistrationElement.appendChild(regIdElement);
|
|
309
|
-
partyElement.appendChild(regRegistrationElement);
|
|
310
|
-
}
|
|
311
321
|
}
|
|
312
322
|
|
|
313
323
|
/**
|
|
@@ -332,33 +342,6 @@ export class ZUGFeRDEncoder extends CIIBaseEncoder {
|
|
|
332
342
|
deliveryDateElement.appendChild(occurrenceDateElement);
|
|
333
343
|
deliveryElement.appendChild(deliveryDateElement);
|
|
334
344
|
}
|
|
335
|
-
|
|
336
|
-
// Add period of performance if available
|
|
337
|
-
if (invoice.periodOfPerformance) {
|
|
338
|
-
const periodElement = doc.createElement('ram:BillingSpecifiedPeriod');
|
|
339
|
-
|
|
340
|
-
// Start date
|
|
341
|
-
if (invoice.periodOfPerformance.from !== undefined) {
|
|
342
|
-
const startDateElement = doc.createElement('ram:StartDateTime');
|
|
343
|
-
const startDateStringElement = doc.createElement('udt:DateTimeString');
|
|
344
|
-
startDateStringElement.setAttribute('format', '102'); // YYYYMMDD format
|
|
345
|
-
startDateStringElement.textContent = this.formatDateYYYYMMDD(invoice.periodOfPerformance.from, 'BT-73 invoicing period start date');
|
|
346
|
-
startDateElement.appendChild(startDateStringElement);
|
|
347
|
-
periodElement.appendChild(startDateElement);
|
|
348
|
-
}
|
|
349
|
-
|
|
350
|
-
// End date
|
|
351
|
-
if (invoice.periodOfPerformance.to !== undefined) {
|
|
352
|
-
const endDateElement = doc.createElement('ram:EndDateTime');
|
|
353
|
-
const endDateStringElement = doc.createElement('udt:DateTimeString');
|
|
354
|
-
endDateStringElement.setAttribute('format', '102'); // YYYYMMDD format
|
|
355
|
-
endDateStringElement.textContent = this.formatDateYYYYMMDD(invoice.periodOfPerformance.to, 'BT-74 invoicing period end date');
|
|
356
|
-
endDateElement.appendChild(endDateStringElement);
|
|
357
|
-
periodElement.appendChild(endDateElement);
|
|
358
|
-
}
|
|
359
|
-
|
|
360
|
-
deliveryElement.appendChild(periodElement);
|
|
361
|
-
}
|
|
362
345
|
}
|
|
363
346
|
|
|
364
347
|
/**
|
|
@@ -446,6 +429,33 @@ export class ZUGFeRDEncoder extends CIIBaseEncoder {
|
|
|
446
429
|
// Add tax details
|
|
447
430
|
this.addTaxDetails(doc, settlementElement, invoice);
|
|
448
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
|
+
|
|
449
459
|
// Add totals
|
|
450
460
|
this.addMonetarySummation(doc, settlementElement, invoice);
|
|
451
461
|
}
|
|
@@ -460,8 +470,8 @@ export class ZUGFeRDEncoder extends CIIBaseEncoder {
|
|
|
460
470
|
// the EN 16931 totals in decimal arithmetic, rounded as the business rules check them
|
|
461
471
|
const totals = computeDocumentTotals(invoice);
|
|
462
472
|
|
|
463
|
-
// VAT breakdown (BG-23), one per rate
|
|
464
|
-
for (const { rate, taxableAmount, taxAmount } of totals.vatGroups) {
|
|
473
|
+
// VAT breakdown (BG-23), one per VAT category and rate
|
|
474
|
+
for (const { category, rate, taxableAmount, taxAmount, exemption } of totals.vatGroups) {
|
|
465
475
|
const taxElement = doc.createElement('ram:ApplicableTradeTax');
|
|
466
476
|
|
|
467
477
|
// VAT category tax amount (BT-117)
|
|
@@ -473,6 +483,13 @@ export class ZUGFeRDEncoder extends CIIBaseEncoder {
|
|
|
473
483
|
const typeCodeElement = doc.createElement('ram:TypeCode');
|
|
474
484
|
typeCodeElement.textContent = 'VAT';
|
|
475
485
|
taxElement.appendChild(typeCodeElement);
|
|
486
|
+
|
|
487
|
+
// VAT exemption reason text (BT-120), for reverse charge (BR-AE-10)
|
|
488
|
+
if (exemption) {
|
|
489
|
+
const exemptionReasonElement = doc.createElement('ram:ExemptionReason');
|
|
490
|
+
exemptionReasonElement.textContent = exemption.reason;
|
|
491
|
+
taxElement.appendChild(exemptionReasonElement);
|
|
492
|
+
}
|
|
476
493
|
|
|
477
494
|
// VAT category taxable amount (BT-116)
|
|
478
495
|
const basisAmountElement = doc.createElement('ram:BasisAmount');
|
|
@@ -481,8 +498,15 @@ export class ZUGFeRDEncoder extends CIIBaseEncoder {
|
|
|
481
498
|
|
|
482
499
|
// Add category code
|
|
483
500
|
const categoryCodeElement = doc.createElement('ram:CategoryCode');
|
|
484
|
-
categoryCodeElement.textContent =
|
|
501
|
+
categoryCodeElement.textContent = category;
|
|
485
502
|
taxElement.appendChild(categoryCodeElement);
|
|
503
|
+
|
|
504
|
+
// VAT exemption reason code (BT-121)
|
|
505
|
+
if (exemption) {
|
|
506
|
+
const exemptionReasonCodeElement = doc.createElement('ram:ExemptionReasonCode');
|
|
507
|
+
exemptionReasonCodeElement.textContent = exemption.code;
|
|
508
|
+
taxElement.appendChild(exemptionReasonCodeElement);
|
|
509
|
+
}
|
|
486
510
|
|
|
487
511
|
// Add rate
|
|
488
512
|
const rateElement = doc.createElement('ram:RateApplicablePercent');
|
|
@@ -565,7 +589,8 @@ export class ZUGFeRDEncoder extends CIIBaseEncoder {
|
|
|
565
589
|
const agreementElement = doc.createElement('ram:SpecifiedLineTradeAgreement');
|
|
566
590
|
const priceElement = doc.createElement('ram:NetPriceProductTradePrice');
|
|
567
591
|
const chargeAmountElement = doc.createElement('ram:ChargeAmount');
|
|
568
|
-
|
|
592
|
+
// Item net price (BT-146) with the precision it has; EN 16931 does not limit its decimals
|
|
593
|
+
chargeAmountElement.textContent = toPlainDecimalString(item.unitNetPrice);
|
|
569
594
|
priceElement.appendChild(chargeAmountElement);
|
|
570
595
|
agreementElement.appendChild(priceElement);
|
|
571
596
|
lineItemElement.appendChild(agreementElement);
|
|
@@ -573,7 +598,7 @@ export class ZUGFeRDEncoder extends CIIBaseEncoder {
|
|
|
573
598
|
// Add delivery information (quantity)
|
|
574
599
|
const deliveryElement = doc.createElement('ram:SpecifiedLineTradeDelivery');
|
|
575
600
|
const quantityElement = doc.createElement('ram:BilledQuantity');
|
|
576
|
-
quantityElement.textContent = item.unitQuantity
|
|
601
|
+
quantityElement.textContent = toPlainDecimalString(item.unitQuantity); // BT-129
|
|
577
602
|
// a line without a unit is written without one, so that BR-23 reports it
|
|
578
603
|
if (item.unitType) {
|
|
579
604
|
quantityElement.setAttribute('unitCode', item.unitType);
|
|
@@ -594,7 +619,7 @@ export class ZUGFeRDEncoder extends CIIBaseEncoder {
|
|
|
594
619
|
|
|
595
620
|
// Add tax category code
|
|
596
621
|
const taxCategoryCodeElement = doc.createElement('ram:CategoryCode');
|
|
597
|
-
taxCategoryCodeElement.textContent =
|
|
622
|
+
taxCategoryCodeElement.textContent = totals.lineVatCategories[index]; // BT-151
|
|
598
623
|
taxElement.appendChild(taxCategoryCodeElement);
|
|
599
624
|
|
|
600
625
|
// Add tax rate
|
|
@@ -35,6 +35,19 @@ export class ZUGFeRDV1Decoder extends CIIBaseDecoder {
|
|
|
35
35
|
};
|
|
36
36
|
}
|
|
37
37
|
|
|
38
|
+
/**
|
|
39
|
+
* Whether every line of the v1 document has the VAT category reverse charge (AE), read with
|
|
40
|
+
* the v1 namespaces from the line settlement (`ram:SpecifiedSupplyChainTradeSettlement`)
|
|
41
|
+
*/
|
|
42
|
+
protected override isReverseChargeDocument(): boolean {
|
|
43
|
+
const categories = zugferdV1Select(
|
|
44
|
+
'/rsm:CrossIndustryDocument/rsm:SpecifiedSupplyChainTradeTransaction/ram:IncludedSupplyChainTradeLineItem/ram:SpecifiedSupplyChainTradeSettlement/ram:ApplicableTradeTax/ram:CategoryCode',
|
|
45
|
+
this.doc,
|
|
46
|
+
);
|
|
47
|
+
const codes = (Array.isArray(categories) ? categories : []).map((node) => ((node as Node).textContent ?? '').trim());
|
|
48
|
+
return codes.length > 0 && codes.every((code) => code === 'AE');
|
|
49
|
+
}
|
|
50
|
+
|
|
38
51
|
/**
|
|
39
52
|
* Reads the issue date of the v1 header (`rsm:HeaderExchangedDocument`); a
|
|
40
53
|
* missing or invalid one is refused with an `EInvoiceParsingError`.
|
|
@@ -147,8 +160,8 @@ export class ZUGFeRDV1Decoder extends CIIBaseDecoder {
|
|
|
147
160
|
// Extract the actual delivery date, if stated
|
|
148
161
|
const deliveryDate = this.extractDeliveryDate();
|
|
149
162
|
|
|
150
|
-
//
|
|
151
|
-
const reverseCharge = this.
|
|
163
|
+
// Reverse charge: every line has the VAT category AE
|
|
164
|
+
const reverseCharge = this.isReverseChargeDocument();
|
|
152
165
|
|
|
153
166
|
// Create the common invoice data
|
|
154
167
|
const invoiceData = {
|
|
@@ -217,8 +230,13 @@ export class ZUGFeRDV1Decoder extends CIIBaseDecoder {
|
|
|
217
230
|
// Extract VAT ID
|
|
218
231
|
const vatId = this.getText(`${partyXPath}/ram:SpecifiedTaxRegistration/ram:ID[@schemeID="VA"]`) || '';
|
|
219
232
|
|
|
220
|
-
//
|
|
221
|
-
|
|
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
|
+
'';
|
|
222
240
|
|
|
223
241
|
// Create contact object
|
|
224
242
|
return {
|
|
@@ -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
|
|
@@ -395,7 +396,7 @@ export class UBLEncoder extends UBLBaseEncoder {
|
|
|
395
396
|
taxTotalNode.appendChild(taxAmountElement);
|
|
396
397
|
|
|
397
398
|
// VAT breakdown (BG-23), one subtotal per rate
|
|
398
|
-
for (const { rate, taxableAmount, taxAmount } of totals.vatGroups) {
|
|
399
|
+
for (const { category, rate, taxableAmount, taxAmount, exemption } of totals.vatGroups) {
|
|
399
400
|
const taxSubtotalNode = doc.createElement('cac:TaxSubtotal');
|
|
400
401
|
taxTotalNode.appendChild(taxSubtotalNode);
|
|
401
402
|
|
|
@@ -415,17 +416,14 @@ export class UBLEncoder extends UBLBaseEncoder {
|
|
|
415
416
|
const taxCategoryNode = doc.createElement('cac:TaxCategory');
|
|
416
417
|
taxSubtotalNode.appendChild(taxCategoryNode);
|
|
417
418
|
|
|
418
|
-
//
|
|
419
|
-
|
|
420
|
-
this.appendElement(doc, taxCategoryNode, 'cbc:ID', categoryId);
|
|
421
|
-
|
|
422
|
-
// Add percent with 2 decimal places
|
|
419
|
+
// VAT category code (BT-118) and rate (BT-119)
|
|
420
|
+
this.appendElement(doc, taxCategoryNode, 'cbc:ID', category);
|
|
423
421
|
this.appendElement(doc, taxCategoryNode, 'cbc:Percent', rate.toFixed(2));
|
|
424
422
|
|
|
425
|
-
//
|
|
426
|
-
if (
|
|
427
|
-
this.appendElement(doc, taxCategoryNode, 'cbc:TaxExemptionReasonCode',
|
|
428
|
-
this.appendElement(doc, taxCategoryNode, 'cbc:TaxExemptionReason',
|
|
423
|
+
// VAT exemption reason code (BT-121) and text (BT-120), for reverse charge (BR-AE-10)
|
|
424
|
+
if (exemption) {
|
|
425
|
+
this.appendElement(doc, taxCategoryNode, 'cbc:TaxExemptionReasonCode', exemption.code);
|
|
426
|
+
this.appendElement(doc, taxCategoryNode, 'cbc:TaxExemptionReason', exemption.reason);
|
|
429
427
|
}
|
|
430
428
|
|
|
431
429
|
// Add tax scheme
|
|
@@ -498,7 +496,7 @@ export class UBLEncoder extends UBLBaseEncoder {
|
|
|
498
496
|
if (item.unitType) {
|
|
499
497
|
quantityElement.setAttribute('unitCode', item.unitType);
|
|
500
498
|
}
|
|
501
|
-
quantityElement.textContent = item.unitQuantity
|
|
499
|
+
quantityElement.textContent = toPlainDecimalString(item.unitQuantity); // BT-129
|
|
502
500
|
invoiceLineNode.appendChild(quantityElement);
|
|
503
501
|
|
|
504
502
|
// Invoice line net amount (BT-131): quantity × net price, rounded
|
|
@@ -527,9 +525,8 @@ export class UBLEncoder extends UBLBaseEncoder {
|
|
|
527
525
|
const classifiedTaxCategoryNode = doc.createElement('cac:ClassifiedTaxCategory');
|
|
528
526
|
itemNode.appendChild(classifiedTaxCategoryNode);
|
|
529
527
|
|
|
530
|
-
//
|
|
531
|
-
|
|
532
|
-
this.appendElement(doc, classifiedTaxCategoryNode, 'cbc:ID', categoryId);
|
|
528
|
+
// Invoiced item VAT category code (BT-151)
|
|
529
|
+
this.appendElement(doc, classifiedTaxCategoryNode, 'cbc:ID', totals.lineVatCategories[index]);
|
|
533
530
|
|
|
534
531
|
// Tax percent with 2 decimal places
|
|
535
532
|
this.appendElement(doc, classifiedTaxCategoryNode, 'cbc:Percent', item.vatPercentage.toFixed(2));
|
|
@@ -546,7 +543,8 @@ export class UBLEncoder extends UBLBaseEncoder {
|
|
|
546
543
|
// Price amount
|
|
547
544
|
const priceAmountElement = doc.createElement('cbc:PriceAmount');
|
|
548
545
|
priceAmountElement.setAttribute('currencyID', invoice.currency);
|
|
549
|
-
|
|
546
|
+
// Item net price (BT-146) with the precision it has; EN 16931 does not limit its decimals
|
|
547
|
+
priceAmountElement.textContent = toPlainDecimalString(item.unitNetPrice);
|
|
550
548
|
priceNode.appendChild(priceAmountElement);
|
|
551
549
|
}
|
|
552
550
|
}
|
|
@@ -2,6 +2,7 @@ import { BaseEncoder } from '../base/base.encoder.js';
|
|
|
2
2
|
import type { TAccountingDoc, TCreditNote, TInvoiceDocument } from '../../interfaces/common.js';
|
|
3
3
|
import { UBLDocumentType, UBL_NAMESPACES } from './ubl.types.js';
|
|
4
4
|
import { getWritableDate } from '../utils/date.value.js';
|
|
5
|
+
import { assertVatCategoryWritable } from '../utils/vat.category.js';
|
|
5
6
|
|
|
6
7
|
/**
|
|
7
8
|
* The children of the document root in the order the UBL 2.1 schema requires
|
|
@@ -48,6 +49,8 @@ export abstract class UBLBaseEncoder extends BaseEncoder {
|
|
|
48
49
|
* @returns UBL XML string
|
|
49
50
|
*/
|
|
50
51
|
public async encode(invoice: TAccountingDoc): Promise<string> {
|
|
52
|
+
// a reverse charge document that lacks what EN 16931 requires of one is refused (BR-AE-02, BR-AE-05)
|
|
53
|
+
assertVatCategoryWritable(invoice, 'ubl');
|
|
51
54
|
// a credit note is a CreditNote document; an invoice, a debit note and a self-billed invoice are Invoice documents that differ in their type code
|
|
52
55
|
if (invoice.accountingDocType === 'creditnote') {
|
|
53
56
|
return this.encodeCreditNote(invoice);
|
|
@@ -261,7 +261,9 @@ export class XRechnungDecoder extends UBLBaseDecoder {
|
|
|
261
261
|
subject: subject,
|
|
262
262
|
items: items,
|
|
263
263
|
dueInDays: dueInDays,
|
|
264
|
-
|
|
264
|
+
// reverse charge when every line is VAT category AE; the envelope states it for the
|
|
265
|
+
// whole document, so a document mixing AE lines with others cannot say it
|
|
266
|
+
reverseCharge: this.isReverseChargeDocument(),
|
|
265
267
|
currency: currencyCode as finance.TCurrency,
|
|
266
268
|
notes: notes,
|
|
267
269
|
objectActions: [],
|
|
@@ -301,6 +303,18 @@ export class XRechnungDecoder extends UBLBaseDecoder {
|
|
|
301
303
|
}
|
|
302
304
|
}
|
|
303
305
|
|
|
306
|
+
/**
|
|
307
|
+
* Whether every line of the document has the VAT category reverse charge (AE, BT-151)
|
|
308
|
+
*/
|
|
309
|
+
private isReverseChargeDocument(): boolean {
|
|
310
|
+
const categories = this.select(
|
|
311
|
+
'/*/cac:InvoiceLine/cac:Item/cac:ClassifiedTaxCategory/cbc:ID | /*/cac:CreditNoteLine/cac:Item/cac:ClassifiedTaxCategory/cbc:ID',
|
|
312
|
+
this.doc,
|
|
313
|
+
);
|
|
314
|
+
const codes = (Array.isArray(categories) ? categories : []).map((node) => (node.textContent ?? '').trim());
|
|
315
|
+
return codes.length > 0 && codes.every((code) => code === 'AE');
|
|
316
|
+
}
|
|
317
|
+
|
|
304
318
|
/**
|
|
305
319
|
* Reads the preceding invoice references (BG-3): the number (BT-25) and,
|
|
306
320
|
* when stated, the issue date (BT-26) of each.
|
|
@@ -1,8 +1,10 @@
|
|
|
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';
|
|
7
|
+
import { getVatCategory, getVatExemption, type IVatExemption, type TVatCategoryCode } from './vat.category.js';
|
|
6
8
|
|
|
7
9
|
/** An item amount that is no finite number, so no total can be computed from it */
|
|
8
10
|
export interface IInvalidItemAmount {
|
|
@@ -61,13 +63,17 @@ export const getDocumentCalculator = (currency: TAccountingDoc['currency']): Dec
|
|
|
61
63
|
new DecimalCurrencyCalculator(currency, 'HALF_UP', { maxDecimals: EN16931_MAX_AMOUNT_DECIMALS });
|
|
62
64
|
|
|
63
65
|
/**
|
|
64
|
-
* The VAT breakdown of one rate (BG-23): the
|
|
65
|
-
*
|
|
66
|
+
* The VAT breakdown of one VAT category and rate (BG-23): the category
|
|
67
|
+
* (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).
|
|
66
70
|
*/
|
|
67
71
|
export interface IDocumentVatGroup {
|
|
72
|
+
category: TVatCategoryCode;
|
|
68
73
|
rate: number;
|
|
69
74
|
taxableAmount: Decimal;
|
|
70
75
|
taxAmount: Decimal;
|
|
76
|
+
exemption?: IVatExemption;
|
|
71
77
|
}
|
|
72
78
|
|
|
73
79
|
/**
|
|
@@ -80,11 +86,13 @@ export interface IDocumentTotals {
|
|
|
80
86
|
minorUnits: number;
|
|
81
87
|
/** the Invoice line net amount (BT-131) of each item, in item order: quantity × net price, rounded */
|
|
82
88
|
lineNetAmounts: Decimal[];
|
|
89
|
+
/** the VAT category (BT-151) of each item, in item order */
|
|
90
|
+
lineVatCategories: TVatCategoryCode[];
|
|
83
91
|
/** Sum of Invoice line net amount (BT-106) */
|
|
84
92
|
lineTotal: Decimal;
|
|
85
93
|
/** Invoice total amount without VAT (BT-109): BT-106, as the envelope has no document level allowances or charges */
|
|
86
94
|
taxBasisTotal: Decimal;
|
|
87
|
-
/** VAT breakdown (BG-23), in the order
|
|
95
|
+
/** VAT breakdown (BG-23), one per VAT category and rate, in the order they first appear */
|
|
88
96
|
vatGroups: IDocumentVatGroup[];
|
|
89
97
|
/** Invoice total VAT amount (BT-110): the sum of the rounded VAT category tax amounts */
|
|
90
98
|
taxTotal: Decimal;
|
|
@@ -100,16 +108,20 @@ export interface IDocumentTotals {
|
|
|
100
108
|
* decimals (BR-DEC-*): each line net amount (BT-131) is quantity × net price
|
|
101
109
|
* rounded; the sum of line net amounts (BT-106) is their sum (BR-CO-10);
|
|
102
110
|
* each VAT category taxable amount (BT-116) is the sum of the line net amounts
|
|
103
|
-
*
|
|
104
|
-
* (BR-CO-17); the total VAT (BT-110) is the sum of the category tax amounts
|
|
111
|
+
* of that category and rate, and its tax amount (BT-117) is BT-116 × rate / 100
|
|
112
|
+
* rounded (BR-CO-17), 0 for reverse charge (BR-AE-09); the total VAT (BT-110) is the sum of the category tax amounts
|
|
105
113
|
* (BR-CO-14); the total with VAT (BT-112) is BT-109 + BT-110 (BR-CO-15).
|
|
106
114
|
* @param accountingDoc The document
|
|
107
115
|
*/
|
|
108
|
-
export const computeDocumentTotals = (
|
|
116
|
+
export const computeDocumentTotals = (
|
|
117
|
+
accountingDoc: Pick<TAccountingDoc, 'currency' | 'items'> & { reverseCharge?: boolean; language?: string },
|
|
118
|
+
): IDocumentTotals => {
|
|
109
119
|
const calculator = getDocumentCalculator(accountingDoc.currency);
|
|
110
120
|
const minorUnits = calculator.getCurrencyInfo().minorUnits;
|
|
121
|
+
const category = getVatCategory(accountingDoc);
|
|
111
122
|
const lineNetAmounts: Decimal[] = [];
|
|
112
|
-
const
|
|
123
|
+
const lineVatCategories: TVatCategoryCode[] = [];
|
|
124
|
+
const taxableByGroup = new Map<string, { category: TVatCategoryCode; rate: number; taxable: Decimal }>();
|
|
113
125
|
// an amount that is no number cannot be computed with; it is refused, not treated as 0
|
|
114
126
|
const [firstInvalid] = findInvalidItemAmounts(accountingDoc.items);
|
|
115
127
|
if (firstInvalid) {
|
|
@@ -119,21 +131,38 @@ export const computeDocumentTotals = (accountingDoc: Pick<TAccountingDoc, 'curre
|
|
|
119
131
|
);
|
|
120
132
|
}
|
|
121
133
|
for (const item of accountingDoc.items ?? []) {
|
|
122
|
-
|
|
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
|
+
);
|
|
123
140
|
lineNetAmounts.push(lineNet);
|
|
124
|
-
|
|
141
|
+
lineVatCategories.push(category);
|
|
142
|
+
// grouped by VAT category and rate: the same rate under two categories is two groups
|
|
143
|
+
const key = `${category}|${item.vatPercentage}`;
|
|
144
|
+
const group = taxableByGroup.get(key) ?? { category, rate: item.vatPercentage, taxable: Decimal.ZERO };
|
|
145
|
+
group.taxable = group.taxable.add(lineNet);
|
|
146
|
+
taxableByGroup.set(key, group);
|
|
125
147
|
}
|
|
126
|
-
const vatGroups: IDocumentVatGroup[] = [...
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
148
|
+
const vatGroups: IDocumentVatGroup[] = [...taxableByGroup.values()].map((group) => {
|
|
149
|
+
const exemption = getVatExemption(group.category, accountingDoc.language);
|
|
150
|
+
return {
|
|
151
|
+
category: group.category,
|
|
152
|
+
rate: group.rate,
|
|
153
|
+
taxableAmount: calculator.round(group.taxable),
|
|
154
|
+
// reverse charge states no tax: the recipient owes it (BR-AE-09)
|
|
155
|
+
taxAmount: group.category === 'AE' ? calculator.round(Decimal.ZERO) : calculator.calculateVAT(group.taxable, group.rate),
|
|
156
|
+
...(exemption ? { exemption } : {}),
|
|
157
|
+
};
|
|
158
|
+
});
|
|
131
159
|
const lineTotal = calculator.round(Decimal.sum(lineNetAmounts));
|
|
132
160
|
const taxTotal = calculator.round(Decimal.sum(vatGroups.map((group) => group.taxAmount)));
|
|
133
161
|
const grandTotal = calculator.round(lineTotal.add(taxTotal));
|
|
134
162
|
return {
|
|
135
163
|
minorUnits,
|
|
136
164
|
lineNetAmounts,
|
|
165
|
+
lineVatCategories,
|
|
137
166
|
lineTotal,
|
|
138
167
|
taxBasisTotal: lineTotal,
|
|
139
168
|
vatGroups,
|
|
@@ -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
|
+
};
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import type { TAccountingDoc } from '../../interfaces/common.js';
|
|
2
|
+
import { EInvoiceFormatError } from '../../errors.js';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* The VAT category codes (UNTDID 5305, EN 16931 BT-118/BT-151) the encoders
|
|
6
|
+
* write: standard rated, or reverse charge. The envelope states reverse charge
|
|
7
|
+
* for the whole document (`reverseCharge`); an item carries no category of its
|
|
8
|
+
* own, so a document cannot mix reverse charge lines with standard rated ones.
|
|
9
|
+
*/
|
|
10
|
+
export type TVatCategoryCode = 'S' | 'AE';
|
|
11
|
+
|
|
12
|
+
/** The VAT exemption reason (BT-121 code, BT-120 text) a VAT category states */
|
|
13
|
+
export interface IVatExemption {
|
|
14
|
+
code: string;
|
|
15
|
+
reason: string;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* The VAT category of every line of a document: reverse charge (`AE`) when the
|
|
20
|
+
* document states it, standard rated (`S`) otherwise.
|
|
21
|
+
* @param accountingDoc The document
|
|
22
|
+
*/
|
|
23
|
+
export const getVatCategory = (accountingDoc: { reverseCharge?: boolean }): TVatCategoryCode =>
|
|
24
|
+
accountingDoc.reverseCharge ? 'AE' : 'S';
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* The exemption reason a reverse charge VAT breakdown states (BR-AE-10): the
|
|
28
|
+
* code `VATEX-EU-AE` and the wording the law prescribes, "Steuerschuldnerschaft
|
|
29
|
+
* des Leistungsempfängers" (§ 14a Abs. 1 and 5 UStG). A document in another
|
|
30
|
+
* language may use the wording of Article 226 Nr. 11a of the VAT Directive in
|
|
31
|
+
* that language, "Reverse charge" in English (Abschnitt 14a.1 Abs. 6 Satz 2
|
|
32
|
+
* UStAE). Other categories state none.
|
|
33
|
+
* @param category The VAT category
|
|
34
|
+
* @param language The document language
|
|
35
|
+
*/
|
|
36
|
+
export const getVatExemption = (category: TVatCategoryCode, language: string | undefined): IVatExemption | undefined =>
|
|
37
|
+
category === 'AE'
|
|
38
|
+
? {
|
|
39
|
+
code: 'VATEX-EU-AE',
|
|
40
|
+
reason: (language ?? '').toLowerCase().startsWith('de')
|
|
41
|
+
? 'Steuerschuldnerschaft des Leistungsempfängers'
|
|
42
|
+
: 'Reverse charge',
|
|
43
|
+
}
|
|
44
|
+
: undefined;
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Refuses a reverse charge document that lacks what EN 16931 requires of one,
|
|
48
|
+
* naming the rule; no value is put in place of a missing one.
|
|
49
|
+
* - BR-AE-05: every line of a reverse charge document has the VAT rate 0; the
|
|
50
|
+
* recipient owes the tax, and the invoice states none (§ 14a Abs. 5 Satz 2
|
|
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.
|
|
60
|
+
* @param accountingDoc The document
|
|
61
|
+
* @param targetFormat The format being written
|
|
62
|
+
*/
|
|
63
|
+
export const assertVatCategoryWritable = (accountingDoc: TAccountingDoc, targetFormat: string): void => {
|
|
64
|
+
if (!accountingDoc.reverseCharge) {
|
|
65
|
+
return;
|
|
66
|
+
}
|
|
67
|
+
const refuse = (rule: string, message: string): never => {
|
|
68
|
+
throw new EInvoiceFormatError(`${rule}: ${message}`, { targetFormat, unsupportedFeatures: [rule] });
|
|
69
|
+
};
|
|
70
|
+
for (const [index, item] of (accountingDoc.items ?? []).entries()) {
|
|
71
|
+
if (item.vatPercentage !== 0) {
|
|
72
|
+
refuse('BR-AE-05', `a reverse charge line has the VAT rate 0, items[${index}].vatPercentage is ${String(item.vatPercentage)}`);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
const seller = accountingDoc.from?.registrationDetails;
|
|
76
|
+
if (!seller?.vatId) {
|
|
77
|
+
refuse(
|
|
78
|
+
'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',
|
|
80
|
+
);
|
|
81
|
+
}
|
|
82
|
+
const buyer = accountingDoc.to?.registrationDetails;
|
|
83
|
+
if (!buyer?.vatId && !buyer?.registrationId) {
|
|
84
|
+
refuse(
|
|
85
|
+
'BR-AE-02',
|
|
86
|
+
'a reverse charge document states the buyer VAT identifier (BT-48) or legal registration identifier (BT-47), to.registrationDetails.vatId and .registrationId are missing',
|
|
87
|
+
);
|
|
88
|
+
}
|
|
89
|
+
};
|