@fin.cx/einvoice 9.0.0 → 10.0.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 +2 -2
- package/dist_ts/einvoice.d.ts +12 -0
- package/dist_ts/einvoice.js +45 -3
- package/dist_ts/formats/base/base.decoder.d.ts +27 -2
- package/dist_ts/formats/base/base.decoder.js +52 -27
- package/dist_ts/formats/cii/cii.decoder.d.ts +89 -6
- package/dist_ts/formats/cii/cii.decoder.js +125 -30
- package/dist_ts/formats/cii/cii.encoder.d.ts +71 -1
- package/dist_ts/formats/cii/cii.encoder.js +191 -12
- package/dist_ts/formats/cii/cii.types.d.ts +2 -5
- package/dist_ts/formats/cii/cii.types.js +7 -9
- package/dist_ts/formats/cii/cii.validator.d.ts +13 -6
- package/dist_ts/formats/cii/cii.validator.js +29 -30
- package/dist_ts/formats/cii/facturx/facturx.decoder.js +15 -3
- package/dist_ts/formats/cii/facturx/facturx.encoder.d.ts +0 -6
- package/dist_ts/formats/cii/facturx/facturx.encoder.js +11 -25
- package/dist_ts/formats/cii/facturx/facturx.types.d.ts +0 -5
- package/dist_ts/formats/cii/facturx/facturx.types.js +1 -8
- package/dist_ts/formats/cii/facturx/facturx.validator.d.ts +0 -5
- package/dist_ts/formats/cii/facturx/facturx.validator.js +5 -29
- package/dist_ts/formats/cii/zugferd/zugferd.decoder.js +15 -2
- package/dist_ts/formats/cii/zugferd/zugferd.encoder.d.ts +0 -7
- package/dist_ts/formats/cii/zugferd/zugferd.encoder.js +9 -67
- package/dist_ts/formats/cii/zugferd/zugferd.types.d.ts +0 -5
- package/dist_ts/formats/cii/zugferd/zugferd.types.js +1 -8
- package/dist_ts/formats/cii/zugferd/zugferd.v1.decoder.d.ts +26 -0
- package/dist_ts/formats/cii/zugferd/zugferd.v1.decoder.js +43 -1
- package/dist_ts/formats/cii/zugferd/zugferd.validator.js +6 -2
- package/dist_ts/formats/semantic/semantic.adapter.js +20 -18
- package/dist_ts/formats/ubl/en16931.ubl.validator.d.ts +0 -4
- package/dist_ts/formats/ubl/en16931.ubl.validator.js +5 -42
- package/dist_ts/formats/ubl/generic/ubl.encoder.js +18 -24
- package/dist_ts/formats/ubl/ubl.decoder.d.ts +4 -0
- package/dist_ts/formats/ubl/ubl.decoder.js +9 -1
- package/dist_ts/formats/ubl/ubl.encoder.js +9 -5
- package/dist_ts/formats/ubl/ubl.validator.d.ts +13 -0
- package/dist_ts/formats/ubl/ubl.validator.js +28 -1
- package/dist_ts/formats/ubl/xrechnung/xrechnung.decoder.js +10 -1
- package/dist_ts/formats/ubl/xrechnung/xrechnung.encoder.d.ts +0 -7
- package/dist_ts/formats/ubl/xrechnung/xrechnung.encoder.js +30 -45
- package/dist_ts/formats/ubl/xrechnung.validator.d.ts +5 -4
- package/dist_ts/formats/ubl/xrechnung.validator.js +71 -68
- package/dist_ts/formats/utils/date.value.d.ts +35 -0
- package/dist_ts/formats/utils/date.value.js +98 -1
- package/dist_ts/formats/utils/format.detector.js +16 -10
- package/dist_ts/formats/utils/party.contact.d.ts +16 -0
- package/dist_ts/formats/utils/party.contact.js +16 -0
- package/dist_ts/formats/utils/party.identifier.d.ts +28 -0
- package/dist_ts/formats/utils/party.identifier.js +49 -0
- package/dist_ts/formats/utils/peppol.profile.d.ts +10 -0
- package/dist_ts/formats/utils/peppol.profile.js +12 -0
- package/dist_ts/formats/utils/seller.identifier.d.ts +46 -0
- package/dist_ts/formats/utils/seller.identifier.js +78 -0
- package/dist_ts/formats/utils/stated.values.d.ts +80 -0
- package/dist_ts/formats/utils/stated.values.js +418 -0
- package/dist_ts/formats/utils/vat.category.d.ts +30 -2
- package/dist_ts/formats/utils/vat.category.js +36 -6
- package/dist_ts/formats/utils/vat.id.d.ts +18 -0
- package/dist_ts/formats/utils/vat.id.js +22 -0
- package/dist_ts/formats/validation/conformance.harness.js +6 -6
- package/dist_ts/formats/validation/en16931.business-rules.validator.js +4 -21
- package/dist_ts/formats/validation/facturx.validator.js +6 -6
- package/dist_ts/formats/validation/integrated.validator.js +4 -13
- package/dist_ts/formats/validation/peppol.validator.js +6 -13
- package/dist_ts/formats/validation/validation.types.d.ts +5 -0
- package/dist_ts/formats/validation/validation.types.js +6 -1
- package/dist_ts/formats/validation/vat-categories.validator.d.ts +21 -42
- package/dist_ts/formats/validation/vat-categories.validator.js +137 -431
- package/dist_ts/formats/validation/xrechnung.validator.d.ts +11 -58
- package/dist_ts/formats/validation/xrechnung.validator.js +58 -324
- package/dist_ts/index.d.ts +1 -0
- package/dist_ts/index.js +1 -1
- package/dist_ts/interfaces/en16931-metadata.d.ts +0 -4
- package/dist_ts/interfaces/stated.values.d.ts +93 -0
- package/dist_ts/interfaces/stated.values.js +2 -0
- package/package.json +4 -4
- package/readme.md +152 -7
- package/ts/00_commitinfo_data.ts +1 -1
- package/ts/einvoice.ts +52 -2
- package/ts/formats/base/base.decoder.ts +55 -45
- package/ts/formats/cii/cii.decoder.ts +159 -32
- package/ts/formats/cii/cii.encoder.ts +206 -14
- package/ts/formats/cii/cii.types.ts +7 -9
- package/ts/formats/cii/cii.validator.ts +30 -32
- package/ts/formats/cii/facturx/facturx.decoder.ts +16 -2
- package/ts/formats/cii/facturx/facturx.encoder.ts +13 -25
- package/ts/formats/cii/facturx/facturx.types.ts +0 -9
- package/ts/formats/cii/facturx/facturx.validator.ts +5 -43
- package/ts/formats/cii/zugferd/zugferd.decoder.ts +16 -1
- package/ts/formats/cii/zugferd/zugferd.encoder.ts +10 -72
- package/ts/formats/cii/zugferd/zugferd.types.ts +0 -9
- package/ts/formats/cii/zugferd/zugferd.v1.decoder.ts +56 -0
- package/ts/formats/cii/zugferd/zugferd.validator.ts +5 -1
- package/ts/formats/semantic/semantic.adapter.ts +19 -17
- package/ts/formats/ubl/en16931.ubl.validator.ts +5 -64
- package/ts/formats/ubl/generic/ubl.encoder.ts +19 -23
- package/ts/formats/ubl/ubl.decoder.ts +12 -0
- package/ts/formats/ubl/ubl.encoder.ts +8 -4
- package/ts/formats/ubl/ubl.validator.ts +29 -0
- package/ts/formats/ubl/xrechnung/xrechnung.decoder.ts +8 -0
- package/ts/formats/ubl/xrechnung/xrechnung.encoder.ts +33 -45
- package/ts/formats/ubl/xrechnung.validator.ts +75 -127
- package/ts/formats/utils/date.value.ts +106 -0
- package/ts/formats/utils/format.detector.ts +15 -9
- package/ts/formats/utils/party.contact.ts +30 -0
- package/ts/formats/utils/party.identifier.ts +61 -0
- package/ts/formats/utils/peppol.profile.ts +13 -0
- package/ts/formats/utils/seller.identifier.ts +102 -0
- package/ts/formats/utils/stated.values.ts +499 -0
- package/ts/formats/utils/vat.category.ts +47 -5
- package/ts/formats/utils/vat.id.ts +24 -0
- package/ts/formats/validation/conformance.harness.ts +5 -5
- package/ts/formats/validation/en16931.business-rules.validator.ts +3 -28
- package/ts/formats/validation/facturx.validator.ts +5 -5
- package/ts/formats/validation/integrated.validator.ts +3 -16
- package/ts/formats/validation/peppol.validator.ts +5 -16
- package/ts/formats/validation/validation.types.ts +7 -1
- package/ts/formats/validation/vat-categories.validator.ts +179 -761
- package/ts/formats/validation/xrechnung.validator.ts +61 -382
- package/ts/index.ts +9 -0
- package/ts/interfaces/en16931-metadata.ts +3 -7
- package/ts/interfaces/stated.values.ts +94 -0
- package/ts/readme.md +1 -1
- package/dist_ts/formats/utils/eu.memberstates.d.ts +0 -11
- package/dist_ts/formats/utils/eu.memberstates.js +0 -16
- package/ts/formats/utils/eu.memberstates.ts +0 -16
|
@@ -10,6 +10,7 @@ import { computeDocumentTotals } from '../../utils/document.totals.js';
|
|
|
10
10
|
import { getPaymentTotals } from '../../utils/paid.amount.js';
|
|
11
11
|
import { toPlainDecimalString } from '../../utils/number.text.js';
|
|
12
12
|
import { resolveCountryCode } from '../../utils/country.code.js';
|
|
13
|
+
import { getPartyIdentifiers } from '../../utils/party.identifier.js';
|
|
13
14
|
|
|
14
15
|
/**
|
|
15
16
|
* UBL Encoder implementation
|
|
@@ -114,6 +115,12 @@ export class UBLEncoder extends UBLBaseEncoder {
|
|
|
114
115
|
// Document Currency Code
|
|
115
116
|
this.appendElement(doc, root, 'cbc:DocumentCurrencyCode', invoice.currency);
|
|
116
117
|
|
|
118
|
+
// Buyer reference (BT-10), as the buyer gave it
|
|
119
|
+
const buyerReference = invoice.buyerReference?.trim();
|
|
120
|
+
if (buyerReference) {
|
|
121
|
+
this.appendElement(doc, root, 'cbc:BuyerReference', buyerReference);
|
|
122
|
+
}
|
|
123
|
+
|
|
117
124
|
// Invoicing period (BG-14), in schema order before the document references
|
|
118
125
|
this.addInvoicePeriod(doc, root, invoice);
|
|
119
126
|
|
|
@@ -229,6 +236,18 @@ export class UBLEncoder extends UBLBaseEncoder {
|
|
|
229
236
|
|
|
230
237
|
const partyNode = doc.createElement('cac:Party');
|
|
231
238
|
partyElement.appendChild(partyNode);
|
|
239
|
+
|
|
240
|
+
// Party identifiers (BT-29 seller, BT-46 buyer), before the name in the schema order
|
|
241
|
+
for (const identifier of getPartyIdentifiers(party)) {
|
|
242
|
+
const partyIdentificationNode = doc.createElement('cac:PartyIdentification');
|
|
243
|
+
partyNode.appendChild(partyIdentificationNode);
|
|
244
|
+
const idNode = doc.createElement('cbc:ID');
|
|
245
|
+
if (identifier.scheme) {
|
|
246
|
+
idNode.setAttribute('schemeID', identifier.scheme);
|
|
247
|
+
}
|
|
248
|
+
idNode.textContent = identifier.value;
|
|
249
|
+
partyIdentificationNode.appendChild(idNode);
|
|
250
|
+
}
|
|
232
251
|
|
|
233
252
|
// Party name
|
|
234
253
|
const partyNameNode = doc.createElement('cac:PartyName');
|
|
@@ -902,29 +921,6 @@ export class UBLEncoder extends UBLBaseEncoder {
|
|
|
902
921
|
}
|
|
903
922
|
}
|
|
904
923
|
|
|
905
|
-
// Add additional identifiers
|
|
906
|
-
if (partyData.additionalIdentifiers && Array.isArray(partyData.additionalIdentifiers)) {
|
|
907
|
-
for (const identifier of partyData.additionalIdentifiers) {
|
|
908
|
-
const partyId = doc.createElement('cac:PartyIdentification');
|
|
909
|
-
const id = doc.createElement('cbc:ID');
|
|
910
|
-
if (identifier.scheme) {
|
|
911
|
-
id.setAttribute('schemeID', identifier.scheme);
|
|
912
|
-
}
|
|
913
|
-
id.textContent = identifier.value;
|
|
914
|
-
partyId.appendChild(id);
|
|
915
|
-
|
|
916
|
-
// Insert after EndpointID or at beginning
|
|
917
|
-
const endpoint = party.getElementsByTagName('cbc:EndpointID')[0];
|
|
918
|
-
if (endpoint && endpoint.nextSibling) {
|
|
919
|
-
party.insertBefore(partyId, endpoint.nextSibling);
|
|
920
|
-
} else if (party.firstChild) {
|
|
921
|
-
party.insertBefore(partyId, party.firstChild);
|
|
922
|
-
} else {
|
|
923
|
-
party.appendChild(partyId);
|
|
924
|
-
}
|
|
925
|
-
}
|
|
926
|
-
}
|
|
927
|
-
|
|
928
924
|
// Add contact information from metadata if not already present
|
|
929
925
|
const contactInfo = partyData.metadata?.contactInformation;
|
|
930
926
|
if (contactInfo) {
|
|
@@ -4,6 +4,8 @@ import { UBLDocumentType, UBL_NAMESPACES } from './ubl.types.js';
|
|
|
4
4
|
import { DOMParser, xpath } from '../../plugins.js';
|
|
5
5
|
import { EInvoiceParsingError } from '../../errors.js';
|
|
6
6
|
import { getAccountingDocType } from '../utils/document.typecode.js';
|
|
7
|
+
import { readUblStatedValues } from '../utils/stated.values.js';
|
|
8
|
+
import type { IEInvoiceStatedValues } from '../../interfaces/stated.values.js';
|
|
7
9
|
|
|
8
10
|
const ublParser = new DOMParser();
|
|
9
11
|
const ublNamespaces = {
|
|
@@ -37,11 +39,21 @@ export abstract class UBLBaseDecoder extends BaseDecoder {
|
|
|
37
39
|
return this.doc;
|
|
38
40
|
}
|
|
39
41
|
|
|
42
|
+
/** The values the document states, read by `decode` */
|
|
43
|
+
private statedValues?: IEInvoiceStatedValues;
|
|
44
|
+
|
|
45
|
+
public override getStatedValues(): IEInvoiceStatedValues | undefined {
|
|
46
|
+
return this.statedValues;
|
|
47
|
+
}
|
|
48
|
+
|
|
40
49
|
/**
|
|
41
50
|
* Decodes UBL XML into an accounting document
|
|
42
51
|
* @returns Promise resolving to the accounting document
|
|
43
52
|
*/
|
|
44
53
|
public async decode(): Promise<TAccountingDoc> {
|
|
54
|
+
// the values the document states, read first: an amount that is no number is refused
|
|
55
|
+
this.statedValues = this.assertStatedValuesReadable(readUblStatedValues(this.doc), 'ubl');
|
|
56
|
+
|
|
45
57
|
// a CreditNote root is a credit note whatever its type code says
|
|
46
58
|
if (this.getDocumentType() === UBLDocumentType.CREDIT_NOTE) {
|
|
47
59
|
return this.decodeCreditNote();
|
|
@@ -3,6 +3,8 @@ import type { TAccountingDoc, TCreditNote, TInvoiceDocument } from '../../interf
|
|
|
3
3
|
import { UBLDocumentType, UBL_NAMESPACES } from './ubl.types.js';
|
|
4
4
|
import { getWritableDate } from '../utils/date.value.js';
|
|
5
5
|
import { assertVatCategoryWritable } from '../utils/vat.category.js';
|
|
6
|
+
import { assertSellerIdentified } from '../utils/seller.identifier.js';
|
|
7
|
+
import { assertPartyIdentifiersWritable } from '../utils/party.identifier.js';
|
|
6
8
|
import { computeDocumentTotals } from '../utils/document.totals.js';
|
|
7
9
|
import { getPaymentTotals } from '../utils/paid.amount.js';
|
|
8
10
|
|
|
@@ -53,13 +55,15 @@ export abstract class UBLBaseEncoder extends BaseEncoder {
|
|
|
53
55
|
public async encode(invoice: TAccountingDoc): Promise<string> {
|
|
54
56
|
// a reverse charge document that lacks what EN 16931 requires of one is refused (BR-AE-02, BR-AE-05)
|
|
55
57
|
assertVatCategoryWritable(invoice, 'ubl');
|
|
58
|
+
// a buyer identifier with the SEPA scheme is refused (BR-CL-10)
|
|
59
|
+
assertPartyIdentifiersWritable(invoice, 'ubl');
|
|
56
60
|
// a paid amount the advance payments do not add up to, or with more decimals than the currency has, is refused
|
|
57
61
|
getPaymentTotals(invoice, computeDocumentTotals(invoice));
|
|
58
62
|
// 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
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
return
|
|
63
|
+
const xml = invoice.accountingDocType === 'creditnote' ? await this.encodeCreditNote(invoice) : await this.encodeInvoice(invoice);
|
|
64
|
+
// a seller the written document does not identify is refused (BR-CO-26), never written silently
|
|
65
|
+
assertSellerIdentified(xml, 'ubl', 'ubl');
|
|
66
|
+
return xml;
|
|
63
67
|
}
|
|
64
68
|
|
|
65
69
|
/**
|
|
@@ -4,6 +4,8 @@ import type { ValidationResult } from '../../interfaces/common.js';
|
|
|
4
4
|
import { UBLDocumentType, UBL_NAMESPACES } from './ubl.types.js';
|
|
5
5
|
import { DOMParser, xpath } from '../../plugins.js';
|
|
6
6
|
import { meetsUnitCodeRule } from '../utils/unit.codes.js';
|
|
7
|
+
import { BR_CO_26_TEXT, ublSellerMeetsBrCo26 } from '../utils/seller.identifier.js';
|
|
8
|
+
import { findStatedValueDisagreements, readUblStatedValues } from '../utils/stated.values.js';
|
|
7
9
|
|
|
8
10
|
const ublValidatorParser = new DOMParser();
|
|
9
11
|
const ublValidatorNamespaces = {
|
|
@@ -105,6 +107,33 @@ export abstract class UBLBaseValidator extends BaseValidator {
|
|
|
105
107
|
*/
|
|
106
108
|
protected abstract validateStructure(): boolean;
|
|
107
109
|
|
|
110
|
+
/**
|
|
111
|
+
* BR-CO-10 to BR-CO-17: the document totals, the VAT breakdown, the line
|
|
112
|
+
* amounts and the document level allowances and charges the document states
|
|
113
|
+
* agree with each other, as the CEN artefacts check them
|
|
114
|
+
* @returns True if they do
|
|
115
|
+
*/
|
|
116
|
+
protected validateStatedTotals(): boolean {
|
|
117
|
+
const findings = findStatedValueDisagreements(readUblStatedValues(this.doc), 'ubl');
|
|
118
|
+
for (const finding of findings) {
|
|
119
|
+
this.addError(finding.ruleId, finding.message, finding.location);
|
|
120
|
+
}
|
|
121
|
+
return findings.length === 0;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* BR-CO-26: the seller states a seller identifier (BT-29), a legal
|
|
126
|
+
* registration identifier (BT-30) or a VAT identifier (BT-31).
|
|
127
|
+
* @returns True if it does
|
|
128
|
+
*/
|
|
129
|
+
protected validateSellerIdentifier(): boolean {
|
|
130
|
+
if (ublSellerMeetsBrCo26(this.doc)) {
|
|
131
|
+
return true;
|
|
132
|
+
}
|
|
133
|
+
this.addError('BR-CO-26', BR_CO_26_TEXT, '/*/cac:AccountingSupplierParty');
|
|
134
|
+
return false;
|
|
135
|
+
}
|
|
136
|
+
|
|
108
137
|
/**
|
|
109
138
|
* BR-23: every invoice or credit note line has a unit code on its invoiced or credited quantity.
|
|
110
139
|
* @returns True if every line has one
|
|
@@ -271,6 +271,8 @@ export class XRechnungDecoder extends UBLBaseDecoder {
|
|
|
271
271
|
currency: currencyCode as finance.TCurrency,
|
|
272
272
|
notes: notes,
|
|
273
273
|
objectActions: [],
|
|
274
|
+
// the buyer reference (BT-10), when stated
|
|
275
|
+
...(buyerReference.trim() ? { buyerReference: buyerReference.trim() } : {}),
|
|
274
276
|
// the day and the period of supply as the document states them (BT-72, BG-14)
|
|
275
277
|
...(deliveryDate ? { deliveryDate: this.parseRequiredUblDate(deliveryDate) } : {}),
|
|
276
278
|
...(periodStart && periodEnd
|
|
@@ -285,16 +287,22 @@ export class XRechnungDecoder extends UBLBaseDecoder {
|
|
|
285
287
|
...(paidAmount === undefined ? {} : { paidAmount }),
|
|
286
288
|
};
|
|
287
289
|
|
|
290
|
+
// the specification identifier (BT-24), which the validators read to tell an XRechnung, a
|
|
291
|
+
// PEPPOL or a Factur-X document
|
|
292
|
+
const customizationId = this.getText('/*/cbc:CustomizationID', this.doc).trim();
|
|
288
293
|
if (hasBusinessReferences || hasPaymentInformation || hasDateInformation) {
|
|
289
294
|
invoiceData.metadata = {
|
|
290
295
|
format: 'xrechnung' as any,
|
|
291
296
|
version: '1.0.0',
|
|
297
|
+
...(customizationId ? { customizationId } : {}),
|
|
292
298
|
extensions: {
|
|
293
299
|
...(hasBusinessReferences ? { businessReferences } : {}),
|
|
294
300
|
...(hasPaymentInformation ? { paymentInformation } : {}),
|
|
295
301
|
...(hasDateInformation ? { dateInformation } : {}),
|
|
296
302
|
}
|
|
297
303
|
};
|
|
304
|
+
} else if (customizationId) {
|
|
305
|
+
invoiceData.metadata = { customizationId };
|
|
298
306
|
}
|
|
299
307
|
|
|
300
308
|
// Validate mandatory EN16931 fields unless validation is skipped
|
|
@@ -2,6 +2,32 @@ import { UBLEncoder } from '../generic/ubl.encoder.js';
|
|
|
2
2
|
import type { TAccountingDoc, TCreditNote, TInvoiceDocument } from '../../../interfaces/common.js';
|
|
3
3
|
import { DOMParser, XMLSerializer } from '../../../plugins.js';
|
|
4
4
|
import { UBL_CUSTOMIZATION_IDS } from '../ubl.types.js';
|
|
5
|
+
import { getPartyContact } from '../../utils/party.contact.js';
|
|
6
|
+
import { EInvoiceFormatError } from '../../../errors.js';
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* The buyer reference (BT-10) an XRechnung document states: the envelope's
|
|
10
|
+
* `buyerReference`, or the one a decoded UBL document keeps in
|
|
11
|
+
* `metadata.extensions.businessReferences`. XRechnung requires it (BR-DE-15,
|
|
12
|
+
* "Das Element \"Buyer reference\" (BT-10) muss übermittelt werden."), and it is
|
|
13
|
+
* the buyer's: the buyer gives the seller an identifier of its choice, a
|
|
14
|
+
* Leitweg-ID for a public buyer. Without one the export is refused; a document
|
|
15
|
+
* for a buyer that gave none can be written as EN 16931 UBL (`ubl`), where
|
|
16
|
+
* BT-10 is optional.
|
|
17
|
+
* @param invoice The document
|
|
18
|
+
*/
|
|
19
|
+
const getXRechnungBuyerReference = (invoice: TAccountingDoc): string => {
|
|
20
|
+
const kept: unknown = (invoice as { metadata?: { extensions?: { businessReferences?: { buyerReference?: unknown } } } }).metadata
|
|
21
|
+
?.extensions?.businessReferences?.buyerReference;
|
|
22
|
+
const buyerReference = invoice.buyerReference?.trim() || (typeof kept === 'string' ? kept.trim() : '');
|
|
23
|
+
if (!buyerReference) {
|
|
24
|
+
throw new EInvoiceFormatError(
|
|
25
|
+
"BR-DE-15: an XRechnung document states the buyer reference (BT-10), buyerReference is missing. It is the identifier the buyer gave for routing the invoice (a Leitweg-ID for a public buyer) and cannot be put in place by the seller; without one, export the document as EN 16931 UBL ('ubl'), where BT-10 is optional",
|
|
26
|
+
{ targetFormat: 'xrechnung', unsupportedFeatures: ['BR-DE-15'] },
|
|
27
|
+
);
|
|
28
|
+
}
|
|
29
|
+
return buyerReference;
|
|
30
|
+
};
|
|
5
31
|
|
|
6
32
|
/**
|
|
7
33
|
* Encoder for XRechnung (UBL) format
|
|
@@ -59,9 +85,11 @@ export class XRechnungEncoder extends UBLEncoder {
|
|
|
59
85
|
customizationId.textContent = UBL_CUSTOMIZATION_IDS.XRECHNUNG;
|
|
60
86
|
}
|
|
61
87
|
|
|
62
|
-
//
|
|
88
|
+
// The buyer reference (BT-10), which XRechnung requires (BR-DE-15): the identifier the buyer
|
|
89
|
+
// gave the seller for routing the invoice, a Leitweg-ID for a public buyer. It comes from the
|
|
90
|
+
// buyer, so there is none to put in place of a missing one.
|
|
91
|
+
const buyerReferenceValue = getXRechnungBuyerReference(invoice);
|
|
63
92
|
let buyerRef = root.getElementsByTagName('cbc:BuyerReference')[0];
|
|
64
|
-
const buyerReferenceValue = (invoice as any).buyerReference || metadata?.businessReferences?.buyerReference || invoice.id;
|
|
65
93
|
if (!buyerRef) {
|
|
66
94
|
// Find where to insert it (after DocumentCurrencyCode)
|
|
67
95
|
const currencyCode = root.getElementsByTagName('cbc:DocumentCurrencyCode')[0];
|
|
@@ -70,7 +98,7 @@ export class XRechnungEncoder extends UBLEncoder {
|
|
|
70
98
|
buyerRef.textContent = buyerReferenceValue;
|
|
71
99
|
currencyCode.parentNode!.insertBefore(buyerRef, currencyCode.nextSibling);
|
|
72
100
|
}
|
|
73
|
-
} else
|
|
101
|
+
} else {
|
|
74
102
|
buyerRef.textContent = buyerReferenceValue;
|
|
75
103
|
}
|
|
76
104
|
|
|
@@ -151,29 +179,6 @@ export class XRechnungEncoder extends UBLEncoder {
|
|
|
151
179
|
}
|
|
152
180
|
}
|
|
153
181
|
|
|
154
|
-
// Add PartyIdentification for additional identifiers
|
|
155
|
-
if (party.additionalIdentifiers) {
|
|
156
|
-
for (const identifier of party.additionalIdentifiers) {
|
|
157
|
-
const partyId = doc.createElement('cac:PartyIdentification');
|
|
158
|
-
const id = doc.createElement('cbc:ID');
|
|
159
|
-
if (identifier.scheme) {
|
|
160
|
-
id.setAttribute('schemeID', identifier.scheme);
|
|
161
|
-
}
|
|
162
|
-
id.textContent = identifier.value;
|
|
163
|
-
partyId.appendChild(id);
|
|
164
|
-
|
|
165
|
-
// Insert after EndpointID or at beginning
|
|
166
|
-
const endpoint = partyElement.getElementsByTagName('cbc:EndpointID')[0];
|
|
167
|
-
if (endpoint && endpoint.nextSibling) {
|
|
168
|
-
partyElement.insertBefore(partyId, endpoint.nextSibling);
|
|
169
|
-
} else if (partyElement.firstChild) {
|
|
170
|
-
partyElement.insertBefore(partyId, partyElement.firstChild);
|
|
171
|
-
} else {
|
|
172
|
-
partyElement.appendChild(partyId);
|
|
173
|
-
}
|
|
174
|
-
}
|
|
175
|
-
}
|
|
176
|
-
|
|
177
182
|
// Add company registration number to PartyLegalEntity
|
|
178
183
|
if (party.registrationDetails?.registrationId) {
|
|
179
184
|
let legalEntity = partyElement.getElementsByTagName('cac:PartyLegalEntity')[0];
|
|
@@ -444,27 +449,10 @@ export class XRechnungEncoder extends UBLEncoder {
|
|
|
444
449
|
*/
|
|
445
450
|
private enhancePartyInformation(doc: Document, invoice: TAccountingDoc): void {
|
|
446
451
|
// Enhance supplier party
|
|
447
|
-
this.addContactToParty(doc, 'cac:AccountingSupplierParty',
|
|
452
|
+
this.addContactToParty(doc, 'cac:AccountingSupplierParty', getPartyContact(invoice.from));
|
|
448
453
|
|
|
449
454
|
// Enhance customer party
|
|
450
|
-
this.addContactToParty(doc, 'cac:AccountingCustomerParty',
|
|
451
|
-
}
|
|
452
|
-
|
|
453
|
-
/**
|
|
454
|
-
* The contact block of a party: explicit contact metadata first, otherwise
|
|
455
|
-
* the party's own email and phone (BR-DE-11 and BR-DE-12 require both for
|
|
456
|
-
* the seller).
|
|
457
|
-
* @param party Party data
|
|
458
|
-
*/
|
|
459
|
-
private contactInformationOf(party: any): { name?: string; phone?: string; email?: string } | undefined {
|
|
460
|
-
const explicit = party?.metadata?.contactInformation;
|
|
461
|
-
if (explicit && (explicit.name || explicit.phone || explicit.email)) {
|
|
462
|
-
return explicit;
|
|
463
|
-
}
|
|
464
|
-
if (party?.email || party?.phone) {
|
|
465
|
-
return { name: party.name, phone: party.phone, email: party.email };
|
|
466
|
-
}
|
|
467
|
-
return undefined;
|
|
455
|
+
this.addContactToParty(doc, 'cac:AccountingCustomerParty', getPartyContact(invoice.to));
|
|
468
456
|
}
|
|
469
457
|
|
|
470
458
|
/**
|
|
@@ -1,31 +1,11 @@
|
|
|
1
1
|
import { EN16931UBLValidator } from './en16931.ubl.validator.js';
|
|
2
|
+
import { GERMAN_VAT_ID_FORMAT_CODE, germanVatIdFormatMessage, isMalformedGermanVatId } from '../utils/vat.id.js';
|
|
2
3
|
|
|
3
4
|
/**
|
|
4
5
|
* XRechnung-specific validator that extends EN16931 validation
|
|
5
6
|
* Implements additional German CIUS (Core Invoice Usage Specification) rules
|
|
6
7
|
*/
|
|
7
8
|
export class XRechnungValidator extends EN16931UBLValidator {
|
|
8
|
-
/**
|
|
9
|
-
* Validates XRechnung-specific structure requirements
|
|
10
|
-
*/
|
|
11
|
-
protected validateStructure(): boolean {
|
|
12
|
-
// First validate EN16931 structure
|
|
13
|
-
let valid = super.validateStructure();
|
|
14
|
-
|
|
15
|
-
// XRechnung-specific: Check for proper customization ID
|
|
16
|
-
const customizationID = this.getText('//cbc:CustomizationID');
|
|
17
|
-
if (!customizationID || !customizationID.includes('xrechnung')) {
|
|
18
|
-
this.addError(
|
|
19
|
-
'XRECH-STRUCT-1',
|
|
20
|
-
'XRechnung customization ID is missing or invalid',
|
|
21
|
-
'//cbc:CustomizationID'
|
|
22
|
-
);
|
|
23
|
-
valid = false;
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
return valid;
|
|
27
|
-
}
|
|
28
|
-
|
|
29
9
|
/**
|
|
30
10
|
* Validates XRechnung-specific business rules
|
|
31
11
|
*/
|
|
@@ -33,26 +13,6 @@ export class XRechnungValidator extends EN16931UBLValidator {
|
|
|
33
13
|
// First validate EN16931 business rules
|
|
34
14
|
let valid = super.validateBusinessRules();
|
|
35
15
|
|
|
36
|
-
// BR-DE-1: Payment terms (BT-20) or Payment due date (BT-9) shall be provided.
|
|
37
|
-
if (!this.exists('//cbc:PaymentDueDate') && !this.exists('//cac:PaymentTerms/cbc:Note')) {
|
|
38
|
-
this.addError(
|
|
39
|
-
'BR-DE-1',
|
|
40
|
-
'Payment terms or Payment due date shall be provided',
|
|
41
|
-
'//cac:PaymentTerms'
|
|
42
|
-
);
|
|
43
|
-
valid = false;
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
// BR-DE-2: The element "Buyer reference" (BT-10) shall be provided.
|
|
47
|
-
if (!this.exists('//cbc:BuyerReference')) {
|
|
48
|
-
this.addError(
|
|
49
|
-
'BR-DE-2',
|
|
50
|
-
'Buyer reference is required in XRechnung',
|
|
51
|
-
'//cbc:BuyerReference'
|
|
52
|
-
);
|
|
53
|
-
valid = false;
|
|
54
|
-
}
|
|
55
|
-
|
|
56
16
|
// BR-DE-16 (XRechnung): when a VAT category code S, Z, E, AE, K, G, L or M is used (BT-95,
|
|
57
17
|
// BT-102, BT-151), the seller states a VAT identifier (BT-31) or a tax registration
|
|
58
18
|
// identifier (BT-32), which is any party tax scheme of the seller, or a tax representative
|
|
@@ -79,111 +39,99 @@ export class XRechnungValidator extends EN16931UBLValidator {
|
|
|
79
39
|
valid = false;
|
|
80
40
|
}
|
|
81
41
|
|
|
82
|
-
//
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
)
|
|
89
|
-
|
|
90
|
-
|
|
42
|
+
// The rules below carry the IDs of the official rule sets and check what they check, in
|
|
43
|
+
// their context: KoSIT XRechnung Schematron 2.6.0 (BR-DE-*, and PEPPOL-EN16931-R010, which it
|
|
44
|
+
// includes) and the CEN EN 16931 validation artefacts 1.3.16 (BR-21, BR-49).
|
|
45
|
+
const supplierParty = '/*/cac:AccountingSupplierParty/cac:Party';
|
|
46
|
+
const customerParty = '/*/cac:AccountingCustomerParty/cac:Party';
|
|
47
|
+
const requireElement = (context: string, element: string, ruleId: string, message: string): void => {
|
|
48
|
+
for (const node of this.nodes(context)) {
|
|
49
|
+
if (!this.exists(`${element}[normalize-space(.)]`, node)) {
|
|
50
|
+
this.addError(ruleId, message, `${context}/${element}`);
|
|
51
|
+
valid = false;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
};
|
|
91
55
|
|
|
92
|
-
// BR-DE-
|
|
93
|
-
if (!this.exists('
|
|
94
|
-
this.addError(
|
|
95
|
-
'BR-DE-8',
|
|
96
|
-
'Seller post code is required',
|
|
97
|
-
'//cac:AccountingSupplierParty//cac:PostalAddress'
|
|
98
|
-
);
|
|
56
|
+
// BR-DE-1: an invoice states PAYMENT INSTRUCTIONS (BG-16)
|
|
57
|
+
if (!this.exists('/*/cac:PaymentMeans')) {
|
|
58
|
+
this.addError('BR-DE-1', 'An invoice states the payment instructions (BG-16)', '/*/cac:PaymentMeans');
|
|
99
59
|
valid = false;
|
|
100
60
|
}
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
'//cac:AccountingCustomerParty//cac:PostalAddress'
|
|
108
|
-
);
|
|
109
|
-
valid = false;
|
|
61
|
+
// BR-49 (EN 16931): a payment instruction (BG-16) states the payment means type code (BT-81)
|
|
62
|
+
for (const [index, paymentMeans] of this.nodes('/*/cac:PaymentMeans').entries()) {
|
|
63
|
+
if (!this.exists('cbc:PaymentMeansCode', paymentMeans)) {
|
|
64
|
+
this.addError('BR-49', 'A payment instruction (BG-16) states the payment means type code (BT-81)', `/*/cac:PaymentMeans[${index + 1}]/cbc:PaymentMeansCode`);
|
|
65
|
+
valid = false;
|
|
66
|
+
}
|
|
110
67
|
}
|
|
111
68
|
|
|
112
|
-
// BR-DE-
|
|
113
|
-
|
|
114
|
-
this.addError(
|
|
115
|
-
'BR-DE-10',
|
|
116
|
-
'Buyer post code is required',
|
|
117
|
-
'//cac:AccountingCustomerParty//cac:PostalAddress'
|
|
118
|
-
);
|
|
119
|
-
valid = false;
|
|
120
|
-
}
|
|
69
|
+
// BR-DE-15: the buyer reference (BT-10) is stated
|
|
70
|
+
requireElement('/*', 'cbc:BuyerReference', 'BR-DE-15', 'The buyer reference (BT-10) is stated');
|
|
121
71
|
|
|
122
|
-
// BR-DE-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
'Seller contact telephone number is required',
|
|
127
|
-
'//cac:AccountingSupplierParty//cac:Contact'
|
|
128
|
-
);
|
|
72
|
+
// BR-DE-2: the seller contact (BG-6) is stated; BR-DE-6 and BR-DE-7: with a telephone number
|
|
73
|
+
// (BT-42) and an email address (BT-43)
|
|
74
|
+
if (this.exists(supplierParty) && !this.exists(`${supplierParty}/cac:Contact`)) {
|
|
75
|
+
this.addError('BR-DE-2', 'The seller contact (BG-6) is stated', `${supplierParty}/cac:Contact`);
|
|
129
76
|
valid = false;
|
|
130
77
|
}
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
78
|
+
requireElement(`${supplierParty}/cac:Contact`, 'cbc:Telephone', 'BR-DE-6', 'The seller contact telephone number (BT-42) is stated');
|
|
79
|
+
requireElement(`${supplierParty}/cac:Contact`, 'cbc:ElectronicMail', 'BR-DE-7', 'The seller contact email address (BT-43) is stated');
|
|
80
|
+
|
|
81
|
+
// BR-DE-3, BR-DE-4: the seller city (BT-37) and post code (BT-38)
|
|
82
|
+
requireElement(`${supplierParty}/cac:PostalAddress`, 'cbc:CityName', 'BR-DE-3', 'The seller city (BT-37) is stated');
|
|
83
|
+
requireElement(`${supplierParty}/cac:PostalAddress`, 'cbc:PostalZone', 'BR-DE-4', 'The seller post code (BT-38) is stated');
|
|
84
|
+
// BR-DE-8, BR-DE-9: the buyer city (BT-52) and post code (BT-53)
|
|
85
|
+
requireElement(`${customerParty}/cac:PostalAddress`, 'cbc:CityName', 'BR-DE-8', 'The buyer city (BT-52) is stated');
|
|
86
|
+
requireElement(`${customerParty}/cac:PostalAddress`, 'cbc:PostalZone', 'BR-DE-9', 'The buyer post code (BT-53) is stated');
|
|
87
|
+
|
|
88
|
+
// PEPPOL-EN16931-R010, part of the XRechnung rules: the buyer electronic address (BT-49)
|
|
89
|
+
for (const party of this.nodes(customerParty)) {
|
|
90
|
+
if (!this.exists('cbc:EndpointID', party)) {
|
|
91
|
+
this.addError('PEPPOL-EN16931-R010', 'The buyer electronic address (BT-49) is stated', `${customerParty}/cbc:EndpointID`);
|
|
92
|
+
valid = false;
|
|
93
|
+
}
|
|
140
94
|
}
|
|
141
95
|
|
|
142
|
-
// BR-
|
|
143
|
-
|
|
144
|
-
this.
|
|
145
|
-
'BR-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
);
|
|
149
|
-
valid = false;
|
|
96
|
+
// BR-21 (EN 16931): each invoice line (BG-25) has an invoice line identifier (BT-126)
|
|
97
|
+
for (const [index, line] of this.nodes('/*/cac:InvoiceLine | /*/cac:CreditNoteLine').entries()) {
|
|
98
|
+
if (!this.exists('cbc:ID[normalize-space(.)]', line)) {
|
|
99
|
+
this.addError('BR-21', `Invoice line ${index + 1} has an invoice line identifier (BT-126)`, `${this.locationOf(line as Element)}/cbc:ID`);
|
|
100
|
+
valid = false;
|
|
101
|
+
}
|
|
150
102
|
}
|
|
151
103
|
|
|
152
|
-
// BR-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
)
|
|
104
|
+
// BR-CO-25 (EN 16931): while the amount due for payment (BT-115) is positive, the payment due
|
|
105
|
+
// date (BT-9) or the payment terms (BT-20) are stated. The CEN artefacts for UBL and CII do not
|
|
106
|
+
// check this rule; EN 16931 states it.
|
|
107
|
+
const amountDue = Number(this.getText('/*/cac:LegalMonetaryTotal/cbc:PayableAmount').trim());
|
|
108
|
+
if (
|
|
109
|
+
amountDue > 0 &&
|
|
110
|
+
!this.exists('/*/cbc:DueDate[normalize-space(.)] | /*/cac:PaymentMeans/cbc:PaymentDueDate[normalize-space(.)]') &&
|
|
111
|
+
!this.exists('/*/cac:PaymentTerms/cbc:Note[normalize-space(.)]')
|
|
112
|
+
) {
|
|
113
|
+
this.addError('BR-CO-25', 'While an amount is due (BT-115), the payment due date (BT-9) or the payment terms (BT-20) are stated', '/*/cac:PaymentTerms');
|
|
159
114
|
valid = false;
|
|
160
115
|
}
|
|
161
116
|
|
|
162
|
-
//
|
|
163
|
-
const
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
this.addError(
|
|
168
|
-
'BR-DE-15',
|
|
169
|
-
`Invoice line ${i + 1} is missing identifier`,
|
|
170
|
-
`//cac:InvoiceLine[${i + 1}]`
|
|
171
|
-
);
|
|
117
|
+
// The package's own check of the German VAT identification number format (no official rule)
|
|
118
|
+
for (const vatId of this.nodes('/*/cac:AccountingSupplierParty/cac:Party/cac:PartyTaxScheme[cac:TaxScheme/cbc:ID="VAT"]/cbc:CompanyID')) {
|
|
119
|
+
const value = (vatId.textContent ?? '').trim();
|
|
120
|
+
if (isMalformedGermanVatId(value)) {
|
|
121
|
+
this.addError(GERMAN_VAT_ID_FORMAT_CODE, germanVatIdFormatMessage(value), '/*/cac:AccountingSupplierParty/cac:Party/cac:PartyTaxScheme/cbc:CompanyID');
|
|
172
122
|
valid = false;
|
|
173
123
|
}
|
|
174
124
|
}
|
|
175
125
|
|
|
176
|
-
// German VAT ID format validation
|
|
177
|
-
const supplierVatId = this.getText('//cac:AccountingSupplierParty//cbc:CompanyID[../cac:TaxScheme/cbc:ID="VAT"]');
|
|
178
|
-
if (supplierVatId && supplierVatId.startsWith('DE') && !/^DE[0-9]{9}$/.test(supplierVatId)) {
|
|
179
|
-
this.addError(
|
|
180
|
-
'BR-DE-VAT',
|
|
181
|
-
'German VAT ID format is invalid (must be DE followed by 9 digits)',
|
|
182
|
-
'//cac:AccountingSupplierParty//cbc:CompanyID'
|
|
183
|
-
);
|
|
184
|
-
valid = false;
|
|
185
|
-
}
|
|
186
|
-
|
|
187
126
|
return valid;
|
|
188
127
|
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* The nodes an XPath expression selects
|
|
131
|
+
* @param xpathExpr XPath expression
|
|
132
|
+
*/
|
|
133
|
+
private nodes(xpathExpr: string): Node[] {
|
|
134
|
+
const result = this.select(xpathExpr, this.doc);
|
|
135
|
+
return Array.isArray(result) ? (result as Node[]) : [];
|
|
136
|
+
}
|
|
189
137
|
}
|