@fin.cx/einvoice 8.2.4 → 8.3.1
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/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 +19 -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 +21 -10
- package/dist_ts/formats/cii/zugferd/zugferd.v1.decoder.js +8 -8
- package/dist_ts/formats/ubl/generic/ubl.encoder.d.ts +0 -7
- package/dist_ts/formats/ubl/generic/ubl.encoder.js +20 -23
- package/dist_ts/formats/ubl/xrechnung/xrechnung.decoder.js +19 -5
- package/dist_ts/formats/ubl/xrechnung/xrechnung.encoder.d.ts +0 -5
- package/dist_ts/formats/ubl/xrechnung/xrechnung.encoder.js +1 -23
- package/dist_ts/formats/ubl/xrechnung.validator.js +14 -11
- package/dist_ts/formats/utils/country.code.d.ts +15 -0
- package/dist_ts/formats/utils/country.code.js +41 -0
- 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/vat.category.d.ts +13 -8
- package/dist_ts/formats/utils/vat.category.js +16 -11
- package/dist_ts/formats/validation/codelist.validator.js +2 -2
- package/dist_ts/formats/validation/validation.types.js +23 -6
- package/dist_ts/formats/validation/vat-categories.validator.js +3 -7
- package/package.json +3 -3
- package/readme.md +15 -6
- package/ts/00_commitinfo_data.ts +1 -1
- package/ts/formats/cii/facturx/facturx.decoder.ts +7 -8
- package/ts/formats/cii/facturx/facturx.encoder.ts +19 -5
- package/ts/formats/cii/zugferd/zugferd.decoder.ts +7 -8
- package/ts/formats/cii/zugferd/zugferd.encoder.ts +21 -9
- package/ts/formats/cii/zugferd/zugferd.v1.decoder.ts +7 -8
- package/ts/formats/ubl/generic/ubl.encoder.ts +27 -24
- package/ts/formats/ubl/xrechnung/xrechnung.decoder.ts +17 -4
- package/ts/formats/ubl/xrechnung/xrechnung.encoder.ts +0 -23
- package/ts/formats/ubl/xrechnung.validator.ts +21 -17
- package/ts/formats/utils/country.code.ts +53 -0
- package/ts/formats/utils/eu.memberstates.ts +16 -0
- package/ts/formats/utils/vat.category.ts +15 -10
- package/ts/formats/validation/codelist.validator.ts +2 -2
- package/ts/formats/validation/validation.types.ts +22 -5
- package/ts/formats/validation/vat-categories.validator.ts +2 -6
|
@@ -5,6 +5,7 @@ import { DOMParser, XMLSerializer } from '../../../plugins.js';
|
|
|
5
5
|
import { getDocumentTypeCode } from '../../utils/document.typecode.js';
|
|
6
6
|
import { computeDocumentTotals } from '../../utils/document.totals.js';
|
|
7
7
|
import { toPlainDecimalString } from '../../utils/number.text.js';
|
|
8
|
+
import { resolveCountryCode } from '../../utils/country.code.js';
|
|
8
9
|
import { getWritableDate, getWritableDueDate } from '../../utils/date.value.js';
|
|
9
10
|
|
|
10
11
|
/**
|
|
@@ -209,12 +210,12 @@ export class FacturXEncoder extends CIIBaseEncoder {
|
|
|
209
210
|
|
|
210
211
|
// Add seller
|
|
211
212
|
const sellerElement = doc.createElement('ram:SellerTradeParty');
|
|
212
|
-
this.addPartyInfo(doc, sellerElement, invoice.from);
|
|
213
|
+
this.addPartyInfo(doc, sellerElement, invoice.from, 'seller');
|
|
213
214
|
agreementElement.appendChild(sellerElement);
|
|
214
215
|
|
|
215
216
|
// Add buyer
|
|
216
217
|
const buyerElement = doc.createElement('ram:BuyerTradeParty');
|
|
217
|
-
this.addPartyInfo(doc, buyerElement, invoice.to);
|
|
218
|
+
this.addPartyInfo(doc, buyerElement, invoice.to, 'buyer');
|
|
218
219
|
agreementElement.appendChild(buyerElement);
|
|
219
220
|
}
|
|
220
221
|
|
|
@@ -223,8 +224,9 @@ export class FacturXEncoder extends CIIBaseEncoder {
|
|
|
223
224
|
* @param doc XML document
|
|
224
225
|
* @param partyElement Party element
|
|
225
226
|
* @param party Party data
|
|
227
|
+
* @param partyRole Whether the party is the seller or the buyer
|
|
226
228
|
*/
|
|
227
|
-
private addPartyInfo(doc: Document, partyElement: Element, party: any): void {
|
|
229
|
+
private addPartyInfo(doc: Document, partyElement: Element, party: any, partyRole: 'seller' | 'buyer'): void {
|
|
228
230
|
// Add name
|
|
229
231
|
const nameElement = doc.createElement('ram:Name');
|
|
230
232
|
nameElement.textContent = party.name;
|
|
@@ -264,9 +266,10 @@ export class FacturXEncoder extends CIIBaseEncoder {
|
|
|
264
266
|
cityElement.textContent = party.address.city;
|
|
265
267
|
addressElement.appendChild(cityElement);
|
|
266
268
|
|
|
267
|
-
//
|
|
269
|
+
// Country code (BT-40, BT-55), mandatory (BR-09, BR-11): an ISO 3166-1 alpha-2 code, never
|
|
270
|
+
// derived from a name (BR-CL-14)
|
|
268
271
|
const countryElement = doc.createElement('ram:CountryID');
|
|
269
|
-
countryElement.textContent = party.address
|
|
272
|
+
countryElement.textContent = resolveCountryCode(party.address, partyRole, 'cii');
|
|
270
273
|
addressElement.appendChild(countryElement);
|
|
271
274
|
|
|
272
275
|
partyElement.appendChild(addressElement);
|
|
@@ -281,6 +284,17 @@ export class FacturXEncoder extends CIIBaseEncoder {
|
|
|
281
284
|
partyElement.appendChild(taxRegistrationElement);
|
|
282
285
|
}
|
|
283
286
|
|
|
287
|
+
// Seller tax registration identifier (BT-32), the Steuernummer: scheme FC, after the VAT
|
|
288
|
+
// identifier. EN 16931 has no such element for the buyer, so a buyer's tax number is not written.
|
|
289
|
+
if (partyRole === 'seller' && party.registrationDetails?.taxNumber) {
|
|
290
|
+
const taxRegistrationElement = doc.createElement('ram:SpecifiedTaxRegistration');
|
|
291
|
+
const taxIdElement = doc.createElement('ram:ID');
|
|
292
|
+
taxIdElement.setAttribute('schemeID', 'FC');
|
|
293
|
+
taxIdElement.textContent = party.registrationDetails.taxNumber;
|
|
294
|
+
taxRegistrationElement.appendChild(taxIdElement);
|
|
295
|
+
partyElement.appendChild(taxRegistrationElement);
|
|
296
|
+
}
|
|
297
|
+
|
|
284
298
|
}
|
|
285
299
|
|
|
286
300
|
/**
|
|
@@ -160,13 +160,11 @@ export class ZUGFeRDDecoder extends CIIBaseDecoder {
|
|
|
160
160
|
// Extract VAT ID
|
|
161
161
|
const vatId = this.getText(`${partyXPath}/ram:SpecifiedTaxRegistration/ram:ID[@schemeID="VA"]`) || '';
|
|
162
162
|
|
|
163
|
-
//
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
this.getText(`${partyXPath}/ram:SpecifiedTaxRegistration/ram:ID[@schemeID="FC"]`) ||
|
|
169
|
-
'';
|
|
163
|
+
// the legal registration identifier (BT-30, BT-47)
|
|
164
|
+
const registrationId = this.getText(`${partyXPath}/ram:SpecifiedLegalOrganization/ram:ID`) || '';
|
|
165
|
+
|
|
166
|
+
// the tax registration identifier (BT-32 for the seller), the Steuernummer: scheme FC
|
|
167
|
+
const taxNumber = this.getText(`${partyXPath}/ram:SpecifiedTaxRegistration/ram:ID[@schemeID="FC"]`) || '';
|
|
170
168
|
|
|
171
169
|
// Create contact object
|
|
172
170
|
return {
|
|
@@ -179,7 +177,8 @@ export class ZUGFeRDDecoder extends CIIBaseDecoder {
|
|
|
179
177
|
registrationDetails: {
|
|
180
178
|
vatId: vatId,
|
|
181
179
|
registrationId: registrationId,
|
|
182
|
-
registrationName: ''
|
|
180
|
+
registrationName: '',
|
|
181
|
+
...(taxNumber ? { taxNumber } : {})
|
|
183
182
|
}
|
|
184
183
|
} as business.TContact;
|
|
185
184
|
}
|
|
@@ -8,6 +8,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
10
|
import { toPlainDecimalString } from '../../utils/number.text.js';
|
|
11
|
+
import { resolveCountryCode } from '../../utils/country.code.js';
|
|
11
12
|
|
|
12
13
|
/**
|
|
13
14
|
* Encoder for ZUGFeRD invoice format
|
|
@@ -224,7 +225,7 @@ export class ZUGFeRDEncoder extends CIIBaseEncoder {
|
|
|
224
225
|
|
|
225
226
|
// Add seller
|
|
226
227
|
const sellerElement = doc.createElement('ram:SellerTradeParty');
|
|
227
|
-
this.addPartyInfo(doc, sellerElement, invoice.from);
|
|
228
|
+
this.addPartyInfo(doc, sellerElement, invoice.from, 'seller');
|
|
228
229
|
|
|
229
230
|
// Seller electronic address (BT-34): ram:URIUniversalCommunication/ram:URIID of the party; a
|
|
230
231
|
// trade contact (ram:DefinedTradeContact) has no URIID
|
|
@@ -241,7 +242,7 @@ export class ZUGFeRDEncoder extends CIIBaseEncoder {
|
|
|
241
242
|
|
|
242
243
|
// Add buyer
|
|
243
244
|
const buyerElement = doc.createElement('ram:BuyerTradeParty');
|
|
244
|
-
this.addPartyInfo(doc, buyerElement, invoice.to);
|
|
245
|
+
this.addPartyInfo(doc, buyerElement, invoice.to, 'buyer');
|
|
245
246
|
agreementElement.appendChild(buyerElement);
|
|
246
247
|
}
|
|
247
248
|
|
|
@@ -250,8 +251,9 @@ export class ZUGFeRDEncoder extends CIIBaseEncoder {
|
|
|
250
251
|
* @param doc XML document
|
|
251
252
|
* @param partyElement Party element
|
|
252
253
|
* @param party Party data
|
|
254
|
+
* @param partyRole Whether the party is the seller or the buyer
|
|
253
255
|
*/
|
|
254
|
-
private addPartyInfo(doc: Document, partyElement: Element, party: any): void {
|
|
256
|
+
private addPartyInfo(doc: Document, partyElement: Element, party: any, partyRole: 'seller' | 'buyer'): void {
|
|
255
257
|
// Add name
|
|
256
258
|
const nameElement = doc.createElement('ram:Name');
|
|
257
259
|
nameElement.textContent = party.name;
|
|
@@ -299,12 +301,11 @@ export class ZUGFeRDEncoder extends CIIBaseEncoder {
|
|
|
299
301
|
addressElement.appendChild(cityElement);
|
|
300
302
|
}
|
|
301
303
|
|
|
302
|
-
//
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
}
|
|
304
|
+
// Country code (BT-40, BT-55), mandatory (BR-09, BR-11): an ISO 3166-1 alpha-2 code, never
|
|
305
|
+
// derived from a name (BR-CL-14)
|
|
306
|
+
const countryElement = doc.createElement('ram:CountryID');
|
|
307
|
+
countryElement.textContent = resolveCountryCode(party.address, partyRole, 'cii');
|
|
308
|
+
addressElement.appendChild(countryElement);
|
|
308
309
|
|
|
309
310
|
partyElement.appendChild(addressElement);
|
|
310
311
|
|
|
@@ -318,6 +319,17 @@ export class ZUGFeRDEncoder extends CIIBaseEncoder {
|
|
|
318
319
|
partyElement.appendChild(taxRegistrationElement);
|
|
319
320
|
}
|
|
320
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
|
+
|
|
321
333
|
}
|
|
322
334
|
|
|
323
335
|
/**
|
|
@@ -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
|
}
|
|
@@ -8,6 +8,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
10
|
import { toPlainDecimalString } from '../../utils/number.text.js';
|
|
11
|
+
import { resolveCountryCode } from '../../utils/country.code.js';
|
|
11
12
|
|
|
12
13
|
/**
|
|
13
14
|
* UBL Encoder implementation
|
|
@@ -253,17 +254,18 @@ export class UBLEncoder extends UBLBaseEncoder {
|
|
|
253
254
|
this.appendElement(doc, postalAddressNode, 'cbc:PostalZone', party.address.postalCode);
|
|
254
255
|
}
|
|
255
256
|
|
|
256
|
-
// Country
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
257
|
+
// Country code (BT-40, BT-55), mandatory (BR-09, BR-11): an ISO 3166-1 alpha-2 code, never
|
|
258
|
+
// derived from a name (BR-CL-14)
|
|
259
|
+
const countryNode = doc.createElement('cac:Country');
|
|
260
|
+
postalAddressNode.appendChild(countryNode);
|
|
261
|
+
this.appendElement(
|
|
262
|
+
doc,
|
|
263
|
+
countryNode,
|
|
264
|
+
'cbc:IdentificationCode',
|
|
265
|
+
resolveCountryCode(party.address, elementName === 'cac:AccountingSupplierParty' ? 'seller' : 'buyer', 'ubl'),
|
|
266
|
+
);
|
|
267
|
+
if (party.address.country) {
|
|
268
|
+
this.appendElement(doc, countryNode, 'cbc:Name', party.address.country);
|
|
267
269
|
}
|
|
268
270
|
|
|
269
271
|
// Party tax scheme (VAT ID)
|
|
@@ -277,6 +279,20 @@ export class UBLEncoder extends UBLBaseEncoder {
|
|
|
277
279
|
partyTaxSchemeNode.appendChild(taxSchemeNode);
|
|
278
280
|
this.appendElement(doc, taxSchemeNode, 'cbc:ID', 'VAT');
|
|
279
281
|
}
|
|
282
|
+
|
|
283
|
+
// Seller tax registration identifier (BT-32), the Steuernummer: a party tax scheme other than
|
|
284
|
+
// VAT, after the VAT one; FC as in CII and the German reference examples. EN 16931 has no
|
|
285
|
+
// such element for the buyer, so a buyer's tax number is not written.
|
|
286
|
+
if (elementName === 'cac:AccountingSupplierParty' && party.registrationDetails?.taxNumber) {
|
|
287
|
+
const partyTaxSchemeNode = doc.createElement('cac:PartyTaxScheme');
|
|
288
|
+
partyNode.appendChild(partyTaxSchemeNode);
|
|
289
|
+
|
|
290
|
+
this.appendElement(doc, partyTaxSchemeNode, 'cbc:CompanyID', party.registrationDetails.taxNumber);
|
|
291
|
+
|
|
292
|
+
const taxSchemeNode = doc.createElement('cac:TaxScheme');
|
|
293
|
+
partyTaxSchemeNode.appendChild(taxSchemeNode);
|
|
294
|
+
this.appendElement(doc, taxSchemeNode, 'cbc:ID', 'FC');
|
|
295
|
+
}
|
|
280
296
|
|
|
281
297
|
// Party legal entity (registration information)
|
|
282
298
|
if (party.registrationDetails) {
|
|
@@ -562,19 +578,6 @@ export class UBLEncoder extends UBLBaseEncoder {
|
|
|
562
578
|
parentElement.appendChild(element);
|
|
563
579
|
}
|
|
564
580
|
|
|
565
|
-
/**
|
|
566
|
-
* Helper method to get country code from country name
|
|
567
|
-
* Simple implementation that assumes the country name is already a code
|
|
568
|
-
* @param countryName Country name
|
|
569
|
-
* @returns Country code (2-letter ISO code)
|
|
570
|
-
*/
|
|
571
|
-
private getCountryCode(countryName: string): string {
|
|
572
|
-
// In a real implementation, this would map country names to ISO codes
|
|
573
|
-
// For now, just return the first 2 characters or "XX" as fallback
|
|
574
|
-
if (!countryName) return 'XX';
|
|
575
|
-
return countryName.length >= 2 ? countryName.substring(0, 2).toUpperCase() : 'XX';
|
|
576
|
-
}
|
|
577
|
-
|
|
578
581
|
/**
|
|
579
582
|
* Preserves metadata from invoice to enhance UBL XML output
|
|
580
583
|
* @param doc XML document
|
|
@@ -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
|
|
|
@@ -89,9 +89,6 @@ export class XRechnungEncoder extends UBLEncoder {
|
|
|
89
89
|
}
|
|
90
90
|
}
|
|
91
91
|
|
|
92
|
-
// Add country code handling for German addresses
|
|
93
|
-
this.fixGermanCountryCodes(doc);
|
|
94
|
-
|
|
95
92
|
// Preserve business references from metadata
|
|
96
93
|
this.addBusinessReferences(doc, metadata?.businessReferences);
|
|
97
94
|
|
|
@@ -207,26 +204,6 @@ export class XRechnungEncoder extends UBLEncoder {
|
|
|
207
204
|
}
|
|
208
205
|
}
|
|
209
206
|
|
|
210
|
-
/**
|
|
211
|
-
* Fixes German country codes in the document
|
|
212
|
-
* @param doc XML document
|
|
213
|
-
*/
|
|
214
|
-
private fixGermanCountryCodes(doc: Document): void {
|
|
215
|
-
const countryNodes = doc.getElementsByTagName('cbc:IdentificationCode');
|
|
216
|
-
for (let i = 0; i < countryNodes.length; i++) {
|
|
217
|
-
const node = countryNodes[i];
|
|
218
|
-
if (node.textContent) {
|
|
219
|
-
const text = node.textContent.toLowerCase();
|
|
220
|
-
if (text === 'germany' || text === 'deutschland' || text === 'de') {
|
|
221
|
-
node.textContent = 'DE';
|
|
222
|
-
} else if (text.length > 2) {
|
|
223
|
-
// Try to use first 2 characters as country code
|
|
224
|
-
node.textContent = text.substring(0, 2).toUpperCase();
|
|
225
|
-
}
|
|
226
|
-
}
|
|
227
|
-
}
|
|
228
|
-
}
|
|
229
|
-
|
|
230
207
|
/**
|
|
231
208
|
* Adds business references from metadata to the document
|
|
232
209
|
* @param doc XML document
|
|
@@ -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
|
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import type { business } from '@tsclass/tsclass';
|
|
2
|
+
import { EInvoiceFormatError } from '../../errors.js';
|
|
3
|
+
import { CodeLists } from '../validation/validation.types.js';
|
|
4
|
+
|
|
5
|
+
/** The party whose address a country code is written for */
|
|
6
|
+
export type TCountryCodeParty = 'seller' | 'buyer';
|
|
7
|
+
|
|
8
|
+
const partyFields: Record<TCountryCodeParty, { path: string; term: string; presenceRule: string }> = {
|
|
9
|
+
// BR-09: the seller postal address (BG-5) states the seller country code (BT-40)
|
|
10
|
+
seller: { path: 'from.address', term: 'BT-40', presenceRule: 'BR-09' },
|
|
11
|
+
// BR-11: the buyer postal address (BG-8) states the buyer country code (BT-55)
|
|
12
|
+
buyer: { path: 'to.address', term: 'BT-55', presenceRule: 'BR-11' },
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* The country code an encoder writes for a party's address: an ISO 3166-1
|
|
17
|
+
* alpha-2 code EN 16931 accepts (BR-CL-14, `CodeLists.ISO3166`), taken from
|
|
18
|
+
* `countryCode`, or from `country` when that is already such a code; letter
|
|
19
|
+
* case does not matter. A code is never derived from a country name, so a
|
|
20
|
+
* name, an unknown code or a missing country is refused with an
|
|
21
|
+
* `EInvoiceFormatError` naming the field and the business term.
|
|
22
|
+
* @param address The party's address
|
|
23
|
+
* @param party Whether it is the seller's or the buyer's
|
|
24
|
+
* @param targetFormat The format being written
|
|
25
|
+
*/
|
|
26
|
+
export const resolveCountryCode = (
|
|
27
|
+
address: Partial<business.IAddress> | undefined,
|
|
28
|
+
party: TCountryCodeParty,
|
|
29
|
+
targetFormat: string,
|
|
30
|
+
): string => {
|
|
31
|
+
const { path, term, presenceRule } = partyFields[party];
|
|
32
|
+
const refuse = (rule: string, message: string): never => {
|
|
33
|
+
throw new EInvoiceFormatError(`${rule}: ${message}`, { targetFormat, unsupportedFeatures: [rule] });
|
|
34
|
+
};
|
|
35
|
+
const countryCode = (address?.countryCode ?? '').trim();
|
|
36
|
+
if (countryCode) {
|
|
37
|
+
if (!CodeLists.ISO3166.codes.has(countryCode.toUpperCase())) {
|
|
38
|
+
refuse('BR-CL-14', `the ${party} country code (${term}) is an ISO 3166-1 alpha-2 code, ${path}.countryCode is "${countryCode}"`);
|
|
39
|
+
}
|
|
40
|
+
return countryCode.toUpperCase();
|
|
41
|
+
}
|
|
42
|
+
const country = (address?.country ?? '').trim();
|
|
43
|
+
if (!country) {
|
|
44
|
+
refuse(presenceRule, `the ${party} postal address states a country code (${term}), ${path}.countryCode and ${path}.country are missing`);
|
|
45
|
+
}
|
|
46
|
+
if (!CodeLists.ISO3166.codes.has(country.toUpperCase())) {
|
|
47
|
+
refuse(
|
|
48
|
+
'BR-CL-14',
|
|
49
|
+
`the ${party} country code (${term}) is an ISO 3166-1 alpha-2 code, ${path}.countryCode is missing and ${path}.country is "${country}", which is not one; a code is not derived from a country name`,
|
|
50
|
+
);
|
|
51
|
+
}
|
|
52
|
+
return country.toUpperCase();
|
|
53
|
+
};
|
|
@@ -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);
|
|
@@ -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;
|
|
@@ -96,7 +96,7 @@ export class CodeListValidator {
|
|
|
96
96
|
const buyerCountry = invoice.to?.address?.countryCode;
|
|
97
97
|
if (buyerCountry && !CodeLists.ISO3166.codes.has(buyerCountry.toUpperCase())) {
|
|
98
98
|
this.addError(
|
|
99
|
-
'BR-CL-
|
|
99
|
+
'BR-CL-14',
|
|
100
100
|
`Invalid buyer country code: ${buyerCountry}. Must be ISO 3166-1 alpha-2`,
|
|
101
101
|
'EN16931',
|
|
102
102
|
'to.address.countryCode',
|
|
@@ -110,7 +110,7 @@ export class CodeListValidator {
|
|
|
110
110
|
const deliveryCountry = invoice.metadata?.deliveryAddress?.countryCode;
|
|
111
111
|
if (deliveryCountry && !CodeLists.ISO3166.codes.has(deliveryCountry.toUpperCase())) {
|
|
112
112
|
this.addError(
|
|
113
|
-
'BR-CL-
|
|
113
|
+
'BR-CL-14',
|
|
114
114
|
`Invalid delivery country code: ${deliveryCountry}. Must be ISO 3166-1 alpha-2`,
|
|
115
115
|
'EN16931',
|
|
116
116
|
'metadata.deliveryAddress.countryCode',
|
|
@@ -96,13 +96,30 @@ export const CodeLists = {
|
|
|
96
96
|
])
|
|
97
97
|
},
|
|
98
98
|
|
|
99
|
-
// ISO 3166-1 alpha-2
|
|
99
|
+
// ISO 3166-1 alpha-2 country codes, as EN 16931 accepts them (BR-CL-14, BR-CL-15): the code list
|
|
100
|
+
// of the CEN/TC 434 validation artefacts 1.3.16 for UBL, which adds 1A (Kosovo) and XI (Northern
|
|
101
|
+
// Ireland) to the codes ISO 3166-1 assigns
|
|
100
102
|
ISO3166: {
|
|
101
|
-
version: '
|
|
103
|
+
version: 'EN 16931 validation artefacts 1.3.16',
|
|
102
104
|
codes: new Set([
|
|
103
|
-
'
|
|
104
|
-
'
|
|
105
|
-
'
|
|
105
|
+
'1A', 'AD', 'AE', 'AF', 'AG', 'AI', 'AL', 'AM', 'AO', 'AQ', 'AR', 'AS', 'AT', 'AU',
|
|
106
|
+
'AW', 'AX', 'AZ', 'BA', 'BB', 'BD', 'BE', 'BF', 'BG', 'BH', 'BI', 'BJ', 'BL', 'BM',
|
|
107
|
+
'BN', 'BO', 'BQ', 'BR', 'BS', 'BT', 'BV', 'BW', 'BY', 'BZ', 'CA', 'CC', 'CD', 'CF',
|
|
108
|
+
'CG', 'CH', 'CI', 'CK', 'CL', 'CM', 'CN', 'CO', 'CR', 'CU', 'CV', 'CW', 'CX', 'CY',
|
|
109
|
+
'CZ', 'DE', 'DJ', 'DK', 'DM', 'DO', 'DZ', 'EC', 'EE', 'EG', 'EH', 'ER', 'ES', 'ET',
|
|
110
|
+
'FI', 'FJ', 'FK', 'FM', 'FO', 'FR', 'GA', 'GB', 'GD', 'GE', 'GF', 'GG', 'GH', 'GI',
|
|
111
|
+
'GL', 'GM', 'GN', 'GP', 'GQ', 'GR', 'GS', 'GT', 'GU', 'GW', 'GY', 'HK', 'HM', 'HN',
|
|
112
|
+
'HR', 'HT', 'HU', 'ID', 'IE', 'IL', 'IM', 'IN', 'IO', 'IQ', 'IR', 'IS', 'IT', 'JE',
|
|
113
|
+
'JM', 'JO', 'JP', 'KE', 'KG', 'KH', 'KI', 'KM', 'KN', 'KP', 'KR', 'KW', 'KY', 'KZ',
|
|
114
|
+
'LA', 'LB', 'LC', 'LI', 'LK', 'LR', 'LS', 'LT', 'LU', 'LV', 'LY', 'MA', 'MC', 'MD',
|
|
115
|
+
'ME', 'MF', 'MG', 'MH', 'MK', 'ML', 'MM', 'MN', 'MO', 'MP', 'MQ', 'MR', 'MS', 'MT',
|
|
116
|
+
'MU', 'MV', 'MW', 'MX', 'MY', 'MZ', 'NA', 'NC', 'NE', 'NF', 'NG', 'NI', 'NL', 'NO',
|
|
117
|
+
'NP', 'NR', 'NU', 'NZ', 'OM', 'PA', 'PE', 'PF', 'PG', 'PH', 'PK', 'PL', 'PM', 'PN',
|
|
118
|
+
'PR', 'PS', 'PT', 'PW', 'PY', 'QA', 'RE', 'RO', 'RS', 'RU', 'RW', 'SA', 'SB', 'SC',
|
|
119
|
+
'SD', 'SE', 'SG', 'SH', 'SI', 'SJ', 'SK', 'SL', 'SM', 'SN', 'SO', 'SR', 'SS', 'ST',
|
|
120
|
+
'SV', 'SX', 'SY', 'SZ', 'TC', 'TD', 'TF', 'TG', 'TH', 'TJ', 'TK', 'TL', 'TM', 'TN',
|
|
121
|
+
'TO', 'TR', 'TT', 'TV', 'TW', 'TZ', 'UA', 'UG', 'UM', 'US', 'UY', 'UZ', 'VA', 'VC',
|
|
122
|
+
'VE', 'VG', 'VI', 'VN', 'VU', 'WF', 'WS', 'XI', 'YE', 'YT', 'ZA', 'ZM', 'ZW',
|
|
106
123
|
])
|
|
107
124
|
},
|
|
108
125
|
|
|
@@ -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 {
|