@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.
- package/dist_ts/00_commitinfo_data.js +1 -1
- package/dist_ts/formats/cii/cii.encoder.d.ts +10 -0
- package/dist_ts/formats/cii/cii.encoder.js +39 -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 +7 -8
- package/dist_ts/formats/cii/facturx/facturx.encoder.d.ts +1 -0
- package/dist_ts/formats/cii/facturx/facturx.encoder.js +23 -6
- package/dist_ts/formats/cii/zugferd/zugferd.decoder.js +7 -8
- package/dist_ts/formats/cii/zugferd/zugferd.encoder.d.ts +1 -0
- package/dist_ts/formats/cii/zugferd/zugferd.encoder.js +51 -33
- package/dist_ts/formats/cii/zugferd/zugferd.v1.decoder.js +8 -8
- package/dist_ts/formats/ubl/generic/ubl.encoder.js +16 -3
- package/dist_ts/formats/ubl/xrechnung/xrechnung.decoder.js +19 -5
- package/dist_ts/formats/ubl/xrechnung.validator.js +14 -11
- package/dist_ts/formats/utils/document.totals.js +5 -2
- package/dist_ts/formats/utils/eu.memberstates.d.ts +11 -0
- package/dist_ts/formats/utils/eu.memberstates.js +16 -0
- 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 +13 -8
- package/dist_ts/formats/utils/vat.category.js +16 -11
- package/dist_ts/formats/validation/vat-categories.validator.js +3 -7
- package/package.json +3 -3
- package/readme.md +14 -6
- package/ts/00_commitinfo_data.ts +1 -1
- package/ts/formats/cii/cii.encoder.ts +39 -1
- package/ts/formats/cii/cii.types.ts +41 -0
- package/ts/formats/cii/facturx/facturx.decoder.ts +7 -8
- package/ts/formats/cii/facturx/facturx.encoder.ts +25 -5
- package/ts/formats/cii/zugferd/zugferd.decoder.ts +7 -8
- package/ts/formats/cii/zugferd/zugferd.encoder.ts +57 -36
- package/ts/formats/cii/zugferd/zugferd.v1.decoder.ts +7 -8
- package/ts/formats/ubl/generic/ubl.encoder.ts +18 -2
- package/ts/formats/ubl/xrechnung/xrechnung.decoder.ts +17 -4
- package/ts/formats/ubl/xrechnung.validator.ts +21 -17
- package/ts/formats/utils/document.totals.ts +7 -1
- package/ts/formats/utils/eu.memberstates.ts +16 -0
- package/ts/formats/utils/number.text.ts +25 -0
- package/ts/formats/utils/vat.category.ts +15 -10
- 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
|
-
//
|
|
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);
|
|
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
|
-
|
|
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
|
|
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
|
|
234
|
-
// the
|
|
235
|
-
//
|
|
236
|
-
const registrationId =
|
|
237
|
-
|
|
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
|
|
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
|
-
|
|
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)
|
|
417
|
-
|
|
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-
|
|
57
|
-
|
|
58
|
-
|
|
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-
|
|
61
|
-
'
|
|
62
|
-
'//cac:AccountingSupplierParty
|
|
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
|
-
|
|
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)
|
|
53
|
-
*
|
|
54
|
-
*
|
|
55
|
-
*
|
|
56
|
-
*
|
|
57
|
-
*
|
|
58
|
-
*
|
|
59
|
-
*
|
|
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
|
|
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
|
-
|
|
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 {
|