@fin.cx/einvoice 8.2.1 → 8.2.3
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/einvoice.js +12 -23
- package/dist_ts/formats/cii/cii.decoder.d.ts +6 -0
- package/dist_ts/formats/cii/cii.decoder.js +11 -1
- package/dist_ts/formats/cii/cii.encoder.js +4 -1
- package/dist_ts/formats/cii/facturx/facturx.decoder.js +8 -4
- package/dist_ts/formats/cii/facturx/facturx.encoder.js +57 -57
- package/dist_ts/formats/cii/zugferd/zugferd.decoder.js +8 -4
- package/dist_ts/formats/cii/zugferd/zugferd.encoder.js +55 -67
- package/dist_ts/formats/cii/zugferd/zugferd.v1.decoder.d.ts +5 -0
- package/dist_ts/formats/cii/zugferd/zugferd.v1.decoder.js +18 -5
- package/dist_ts/formats/semantic/semantic.validator.js +20 -1
- package/dist_ts/formats/ubl/generic/ubl.encoder.js +31 -57
- package/dist_ts/formats/ubl/ubl.encoder.js +4 -1
- package/dist_ts/formats/ubl/xrechnung/xrechnung.decoder.d.ts +4 -0
- package/dist_ts/formats/ubl/xrechnung/xrechnung.decoder.js +12 -2
- package/dist_ts/formats/utils/currency.calculator.decimal.d.ts +9 -1
- package/dist_ts/formats/utils/currency.calculator.decimal.js +10 -3
- package/dist_ts/formats/utils/document.totals.d.ts +89 -0
- package/dist_ts/formats/utils/document.totals.js +108 -0
- package/dist_ts/formats/utils/vat.category.d.ts +50 -0
- package/dist_ts/formats/utils/vat.category.js +64 -0
- package/dist_ts/formats/validation/codelist.validator.js +3 -11
- package/dist_ts/formats/validation/en16931.business-rules.validator.js +38 -44
- package/dist_ts/formats/validation/facturx.validator.js +19 -5
- package/dist_ts/formats/validation/vat-categories.validator.js +9 -1
- package/package.json +2 -2
- package/readme.md +25 -0
- package/ts/00_commitinfo_data.ts +1 -1
- package/ts/einvoice.ts +11 -25
- package/ts/formats/cii/cii.decoder.ts +14 -0
- package/ts/formats/cii/cii.encoder.ts +3 -0
- package/ts/formats/cii/facturx/facturx.decoder.ts +8 -3
- package/ts/formats/cii/facturx/facturx.encoder.ts +59 -65
- package/ts/formats/cii/zugferd/zugferd.decoder.ts +8 -3
- package/ts/formats/cii/zugferd/zugferd.encoder.ts +58 -77
- package/ts/formats/cii/zugferd/zugferd.v1.decoder.ts +22 -4
- package/ts/formats/semantic/semantic.validator.ts +20 -0
- package/ts/formats/ubl/generic/ubl.encoder.ts +36 -69
- package/ts/formats/ubl/ubl.encoder.ts +3 -0
- package/ts/formats/ubl/xrechnung/xrechnung.decoder.ts +15 -1
- package/ts/formats/utils/currency.calculator.decimal.ts +10 -2
- package/ts/formats/utils/document.totals.ts +167 -0
- package/ts/formats/utils/vat.category.ts +89 -0
- package/ts/formats/validation/codelist.validator.ts +3 -12
- package/ts/formats/validation/en16931.business-rules.validator.ts +44 -51
- package/ts/formats/validation/facturx.validator.ts +19 -4
- package/ts/formats/validation/vat-categories.validator.ts +9 -0
package/ts/einvoice.ts
CHANGED
|
@@ -46,6 +46,7 @@ import {
|
|
|
46
46
|
// Import enhanced validators
|
|
47
47
|
import { EN16931Validator } from './formats/validation/en16931.validator.js';
|
|
48
48
|
import { EN16931BusinessRulesValidator } from './formats/validation/en16931.business-rules.validator.js';
|
|
49
|
+
import { computeDocumentTotals } from './formats/utils/document.totals.js';
|
|
49
50
|
import { CodeListValidator } from './formats/validation/codelist.validator.js';
|
|
50
51
|
import type { ValidationOptions } from './formats/validation/validation.types.js';
|
|
51
52
|
|
|
@@ -211,7 +212,7 @@ export class EInvoice implements TAccountingDocEnvelope<TAccountingDocType, {}>
|
|
|
211
212
|
}
|
|
212
213
|
|
|
213
214
|
public get totalGross(): number {
|
|
214
|
-
return this.
|
|
215
|
+
return computeDocumentTotals(this).grandTotal.toNumber();
|
|
215
216
|
}
|
|
216
217
|
|
|
217
218
|
public get taxBreakdown(): Array<{ taxPercent: number; netAmount: number; taxAmount: number }> {
|
|
@@ -795,42 +796,27 @@ export class EInvoice implements TAccountingDocEnvelope<TAccountingDocType, {}>
|
|
|
795
796
|
* Calculates the total net amount
|
|
796
797
|
*/
|
|
797
798
|
private calculateTotalNet(): number {
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
}, 0);
|
|
799
|
+
// the sum of line net amounts (BT-106) the encoders write
|
|
800
|
+
return computeDocumentTotals(this).lineTotal.toNumber();
|
|
801
801
|
}
|
|
802
802
|
|
|
803
803
|
/**
|
|
804
804
|
* Calculates the total VAT amount
|
|
805
805
|
*/
|
|
806
806
|
private calculateTotalVat(): number {
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
return sum + (net * item.vatPercentage / 100);
|
|
810
|
-
}, 0);
|
|
807
|
+
// the total VAT (BT-110) the encoders write
|
|
808
|
+
return computeDocumentTotals(this).taxTotal.toNumber();
|
|
811
809
|
}
|
|
812
810
|
|
|
813
811
|
/**
|
|
814
812
|
* Calculates tax breakdown by rate
|
|
815
813
|
*/
|
|
816
814
|
private calculateTaxBreakdown(): Array<{ taxPercent: number; netAmount: number; taxAmount: number }> {
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
const current = breakdown.get(item.vatPercentage) || { net: 0, tax: 0 };
|
|
824
|
-
breakdown.set(item.vatPercentage, {
|
|
825
|
-
net: current.net + net,
|
|
826
|
-
tax: current.tax + tax
|
|
827
|
-
});
|
|
828
|
-
});
|
|
829
|
-
|
|
830
|
-
return Array.from(breakdown.entries()).map(([rate, amounts]) => ({
|
|
831
|
-
taxPercent: rate,
|
|
832
|
-
netAmount: amounts.net,
|
|
833
|
-
taxAmount: amounts.tax
|
|
815
|
+
// the VAT breakdown (BG-23) the encoders write
|
|
816
|
+
return computeDocumentTotals(this).vatGroups.map((group) => ({
|
|
817
|
+
taxPercent: group.rate,
|
|
818
|
+
netAmount: group.taxableAmount.toNumber(),
|
|
819
|
+
taxAmount: group.taxAmount.toNumber(),
|
|
834
820
|
}));
|
|
835
821
|
}
|
|
836
822
|
|
|
@@ -158,6 +158,20 @@ export abstract class CIIBaseDecoder extends BaseDecoder {
|
|
|
158
158
|
return dueDate === undefined ? absentDueInDays : Math.round((dueDate - issueDate) / (1000 * 60 * 60 * 24));
|
|
159
159
|
}
|
|
160
160
|
|
|
161
|
+
/**
|
|
162
|
+
* Whether every line of the document has the VAT category reverse charge (AE, BT-151); the
|
|
163
|
+
* envelope states it for the whole document, so a document mixing AE lines with others
|
|
164
|
+
* cannot say it
|
|
165
|
+
*/
|
|
166
|
+
protected isReverseChargeDocument(): boolean {
|
|
167
|
+
const categories = this.select(
|
|
168
|
+
'/rsm:CrossIndustryInvoice/rsm:SupplyChainTradeTransaction/ram:IncludedSupplyChainTradeLineItem/ram:SpecifiedLineTradeSettlement/ram:ApplicableTradeTax/ram:CategoryCode',
|
|
169
|
+
this.doc,
|
|
170
|
+
);
|
|
171
|
+
const codes = (Array.isArray(categories) ? categories : []).map((node) => ((node as Node).textContent ?? '').trim());
|
|
172
|
+
return codes.length > 0 && codes.every((code) => code === 'AE');
|
|
173
|
+
}
|
|
174
|
+
|
|
161
175
|
/**
|
|
162
176
|
* Reads the actual delivery date (BT-72) of the header delivery; undefined
|
|
163
177
|
* when the document does not state one.
|
|
@@ -3,6 +3,7 @@ import type { TAccountingDoc, TCreditNote, TInvoiceDocument } from '../../interf
|
|
|
3
3
|
import { EInvoiceFormatError } from '../../errors.js';
|
|
4
4
|
import { getPrecedingInvoiceReferences } from '../utils/preceding.invoice.js';
|
|
5
5
|
import { getWritableDate } from '../utils/date.value.js';
|
|
6
|
+
import { assertVatCategoryWritable } from '../utils/vat.category.js';
|
|
6
7
|
import { CII_NAMESPACES, CIIProfile } from './cii.types.js';
|
|
7
8
|
|
|
8
9
|
/**
|
|
@@ -25,6 +26,8 @@ export abstract class CIIBaseEncoder extends BaseEncoder {
|
|
|
25
26
|
* @returns CII XML string
|
|
26
27
|
*/
|
|
27
28
|
public async encode(invoice: TAccountingDoc): Promise<string> {
|
|
29
|
+
// a reverse charge document that lacks what EN 16931 requires of one is refused (BR-AE-02, BR-AE-05)
|
|
30
|
+
assertVatCategoryWritable(invoice, 'cii');
|
|
28
31
|
// MINIMUM has no preceding invoice reference (BG-3): a reference the document states cannot be written there
|
|
29
32
|
if (this.profile === CIIProfile.MINIMUM && getPrecedingInvoiceReferences(invoice).length > 0) {
|
|
30
33
|
throw new EInvoiceFormatError(
|
|
@@ -94,8 +94,8 @@ export class FacturXDecoder extends CIIBaseDecoder {
|
|
|
94
94
|
// Extract the actual delivery date, if stated
|
|
95
95
|
const deliveryDate = this.extractDeliveryDate();
|
|
96
96
|
|
|
97
|
-
//
|
|
98
|
-
const reverseCharge = this.
|
|
97
|
+
// Reverse charge: every line has the VAT category AE
|
|
98
|
+
const reverseCharge = this.isReverseChargeDocument();
|
|
99
99
|
|
|
100
100
|
// Create the common invoice data
|
|
101
101
|
const invoiceData = {
|
|
@@ -158,7 +158,12 @@ export class FacturXDecoder extends CIIBaseDecoder {
|
|
|
158
158
|
const vatId = this.getText(`${partyXPath}/ram:SpecifiedTaxRegistration/ram:ID[@schemeID="VA"]`) || '';
|
|
159
159
|
|
|
160
160
|
// Extract registration ID
|
|
161
|
-
|
|
161
|
+
// the legal registration identifier (BT-30, BT-47); a document that states only a tax
|
|
162
|
+
// number (scheme FC, BT-32) keeps it here as before, the envelope has no field of its own for it
|
|
163
|
+
const registrationId =
|
|
164
|
+
this.getText(`${partyXPath}/ram:SpecifiedLegalOrganization/ram:ID`) ||
|
|
165
|
+
this.getText(`${partyXPath}/ram:SpecifiedTaxRegistration/ram:ID[@schemeID="FC"]`) ||
|
|
166
|
+
'';
|
|
162
167
|
|
|
163
168
|
// Create contact object
|
|
164
169
|
return {
|
|
@@ -2,10 +2,8 @@ import { CIIBaseEncoder } from '../cii.encoder.js';
|
|
|
2
2
|
import type { TAccountingDoc, TCreditNote, TInvoiceDocument } from '../../../interfaces/common.js';
|
|
3
3
|
import { FACTURX_PROFILE_IDS } from './facturx.types.js';
|
|
4
4
|
import { DOMParser, XMLSerializer } from '../../../plugins.js';
|
|
5
|
-
import { Decimal } from '../../utils/decimal.js';
|
|
6
|
-
import { DecimalCurrencyCalculator } from '../../utils/currency.calculator.decimal.js';
|
|
7
|
-
import { getCurrencyMinorUnits } from '../../utils/currency.utils.js';
|
|
8
5
|
import { getDocumentTypeCode } from '../../utils/document.typecode.js';
|
|
6
|
+
import { computeDocumentTotals } from '../../utils/document.totals.js';
|
|
9
7
|
import { getWritableDate, getWritableDueDate } from '../../utils/date.value.js';
|
|
10
8
|
|
|
11
9
|
/**
|
|
@@ -225,6 +223,17 @@ export class FacturXEncoder extends CIIBaseEncoder {
|
|
|
225
223
|
nameElement.textContent = party.name;
|
|
226
224
|
partyElement.appendChild(nameElement);
|
|
227
225
|
|
|
226
|
+
// Legal registration identifier (BT-30 seller, BT-47 buyer), e.g. the commercial register
|
|
227
|
+
// number; in the schema order it follows the name. It is no tax registration: the scheme
|
|
228
|
+
// FC of ram:SpecifiedTaxRegistration is the seller's tax number (BT-32)
|
|
229
|
+
if (party.registrationDetails && party.registrationDetails.registrationId) {
|
|
230
|
+
const legalOrganizationElement = doc.createElement('ram:SpecifiedLegalOrganization');
|
|
231
|
+
const legalIdElement = doc.createElement('ram:ID');
|
|
232
|
+
legalIdElement.textContent = party.registrationDetails.registrationId;
|
|
233
|
+
legalOrganizationElement.appendChild(legalIdElement);
|
|
234
|
+
partyElement.appendChild(legalOrganizationElement);
|
|
235
|
+
}
|
|
236
|
+
|
|
228
237
|
// Add postal address
|
|
229
238
|
const addressElement = doc.createElement('ram:PostalTradeAddress');
|
|
230
239
|
|
|
@@ -265,15 +274,6 @@ export class FacturXEncoder extends CIIBaseEncoder {
|
|
|
265
274
|
partyElement.appendChild(taxRegistrationElement);
|
|
266
275
|
}
|
|
267
276
|
|
|
268
|
-
// Add registration ID if available
|
|
269
|
-
if (party.registrationDetails && party.registrationDetails.registrationId) {
|
|
270
|
-
const regRegistrationElement = doc.createElement('ram:SpecifiedTaxRegistration');
|
|
271
|
-
const regIdElement = doc.createElement('ram:ID');
|
|
272
|
-
regIdElement.setAttribute('schemeID', 'FC');
|
|
273
|
-
regIdElement.textContent = party.registrationDetails.registrationId;
|
|
274
|
-
regRegistrationElement.appendChild(regIdElement);
|
|
275
|
-
partyElement.appendChild(regRegistrationElement);
|
|
276
|
-
}
|
|
277
277
|
}
|
|
278
278
|
|
|
279
279
|
/**
|
|
@@ -316,6 +316,32 @@ export class FacturXEncoder extends CIIBaseEncoder {
|
|
|
316
316
|
currencyElement.textContent = invoice.currency;
|
|
317
317
|
settlementElement.appendChild(currencyElement);
|
|
318
318
|
|
|
319
|
+
// the EN 16931 totals in decimal arithmetic, rounded as the business rules check them
|
|
320
|
+
const totals = computeDocumentTotals(invoice);
|
|
321
|
+
|
|
322
|
+
// VAT breakdown (BG-23), one per VAT category and rate, before the payment terms as the CII
|
|
323
|
+
// schema orders them
|
|
324
|
+
for (const { category, rate, taxableAmount, taxAmount, exemption } of totals.vatGroups) {
|
|
325
|
+
const taxElement = doc.createElement('ram:ApplicableTradeTax');
|
|
326
|
+
const appendText = (name: string, text: string) => {
|
|
327
|
+
const element = doc.createElement(name);
|
|
328
|
+
element.textContent = text;
|
|
329
|
+
taxElement.appendChild(element);
|
|
330
|
+
};
|
|
331
|
+
appendText('ram:CalculatedAmount', taxAmount.toFixed(totals.minorUnits)); // BT-117
|
|
332
|
+
appendText('ram:TypeCode', 'VAT');
|
|
333
|
+
if (exemption) {
|
|
334
|
+
appendText('ram:ExemptionReason', exemption.reason); // BT-120 (BR-AE-10)
|
|
335
|
+
}
|
|
336
|
+
appendText('ram:BasisAmount', taxableAmount.toFixed(totals.minorUnits)); // BT-116
|
|
337
|
+
appendText('ram:CategoryCode', category); // BT-118
|
|
338
|
+
if (exemption) {
|
|
339
|
+
appendText('ram:ExemptionReasonCode', exemption.code); // BT-121
|
|
340
|
+
}
|
|
341
|
+
appendText('ram:RateApplicablePercent', rate.toString()); // BT-119
|
|
342
|
+
settlementElement.appendChild(taxElement);
|
|
343
|
+
}
|
|
344
|
+
|
|
319
345
|
// Add payment terms
|
|
320
346
|
const paymentTermsElement = doc.createElement('ram:SpecifiedTradePaymentTerms');
|
|
321
347
|
|
|
@@ -333,50 +359,21 @@ export class FacturXEncoder extends CIIBaseEncoder {
|
|
|
333
359
|
|
|
334
360
|
settlementElement.appendChild(paymentTermsElement);
|
|
335
361
|
|
|
336
|
-
// Add totals
|
|
362
|
+
// Add totals, in the order of the CII schema
|
|
337
363
|
const monetarySummationElement = doc.createElement('ram:SpecifiedTradeSettlementHeaderMonetarySummation');
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
let totalTaxAmount = Decimal.ZERO;
|
|
344
|
-
|
|
345
|
-
// Calculate from items
|
|
346
|
-
if (invoice.items) {
|
|
347
|
-
for (const item of invoice.items) {
|
|
348
|
-
const itemNetAmount = calculator.calculateLineNet(item.unitQuantity, item.unitNetPrice);
|
|
349
|
-
const itemTaxAmount = calculator.calculateVAT(itemNetAmount, item.vatPercentage);
|
|
350
|
-
|
|
351
|
-
totalNetAmount = totalNetAmount.add(itemNetAmount);
|
|
352
|
-
totalTaxAmount = totalTaxAmount.add(itemTaxAmount);
|
|
364
|
+
const appendAmount = (name: string, value: { toFixed(decimalPlaces: number): string }, withCurrency = false) => {
|
|
365
|
+
const element = doc.createElement(name);
|
|
366
|
+
element.textContent = value.toFixed(totals.minorUnits);
|
|
367
|
+
if (withCurrency) {
|
|
368
|
+
element.setAttribute('currencyID', invoice.currency);
|
|
353
369
|
}
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
const lineTotalElement = doc.createElement('ram:LineTotalAmount');
|
|
362
|
-
lineTotalElement.textContent = totalNetAmount.toFixed(minorUnits);
|
|
363
|
-
monetarySummationElement.appendChild(lineTotalElement);
|
|
364
|
-
|
|
365
|
-
// Add tax total amount
|
|
366
|
-
const taxTotalElement = doc.createElement('ram:TaxTotalAmount');
|
|
367
|
-
taxTotalElement.textContent = totalTaxAmount.toFixed(minorUnits);
|
|
368
|
-
taxTotalElement.setAttribute('currencyID', invoice.currency);
|
|
369
|
-
monetarySummationElement.appendChild(taxTotalElement);
|
|
370
|
-
|
|
371
|
-
// Add grand total amount
|
|
372
|
-
const grandTotalElement = doc.createElement('ram:GrandTotalAmount');
|
|
373
|
-
grandTotalElement.textContent = totalGrossAmount.toFixed(minorUnits);
|
|
374
|
-
monetarySummationElement.appendChild(grandTotalElement);
|
|
375
|
-
|
|
376
|
-
// Add due payable amount
|
|
377
|
-
const duePayableElement = doc.createElement('ram:DuePayableAmount');
|
|
378
|
-
duePayableElement.textContent = totalGrossAmount.toFixed(minorUnits);
|
|
379
|
-
monetarySummationElement.appendChild(duePayableElement);
|
|
370
|
+
monetarySummationElement.appendChild(element);
|
|
371
|
+
};
|
|
372
|
+
appendAmount('ram:LineTotalAmount', totals.lineTotal); // BT-106
|
|
373
|
+
appendAmount('ram:TaxBasisTotalAmount', totals.taxBasisTotal); // BT-109
|
|
374
|
+
appendAmount('ram:TaxTotalAmount', totals.taxTotal, true); // BT-110
|
|
375
|
+
appendAmount('ram:GrandTotalAmount', totals.grandTotal); // BT-112
|
|
376
|
+
appendAmount('ram:DuePayableAmount', totals.duePayable); // BT-115
|
|
380
377
|
|
|
381
378
|
settlementElement.appendChild(monetarySummationElement);
|
|
382
379
|
}
|
|
@@ -388,12 +385,12 @@ export class FacturXEncoder extends CIIBaseEncoder {
|
|
|
388
385
|
* @param invoice Invoice data
|
|
389
386
|
*/
|
|
390
387
|
private addLineItems(doc: Document, transactionElement: Element, invoice: TAccountingDoc): void {
|
|
391
|
-
|
|
392
|
-
const
|
|
388
|
+
// the line net amounts of the one EN 16931 totals computation
|
|
389
|
+
const totals = computeDocumentTotals(invoice);
|
|
393
390
|
|
|
394
391
|
// Add each line item
|
|
395
392
|
if (invoice.items) {
|
|
396
|
-
for (const item of invoice.items) {
|
|
393
|
+
for (const [index, item] of invoice.items.entries()) {
|
|
397
394
|
// Create line item element
|
|
398
395
|
const lineItemElement = doc.createElement('ram:IncludedSupplyChainTradeLineItem');
|
|
399
396
|
|
|
@@ -454,7 +451,7 @@ export class FacturXEncoder extends CIIBaseEncoder {
|
|
|
454
451
|
|
|
455
452
|
// Add tax category code
|
|
456
453
|
const taxCategoryCodeElement = doc.createElement('ram:CategoryCode');
|
|
457
|
-
taxCategoryCodeElement.textContent =
|
|
454
|
+
taxCategoryCodeElement.textContent = totals.lineVatCategories[index]; // BT-151
|
|
458
455
|
taxElement.appendChild(taxCategoryCodeElement);
|
|
459
456
|
|
|
460
457
|
// Add tax rate
|
|
@@ -464,15 +461,12 @@ export class FacturXEncoder extends CIIBaseEncoder {
|
|
|
464
461
|
|
|
465
462
|
settlementElement.appendChild(taxElement);
|
|
466
463
|
|
|
467
|
-
//
|
|
468
|
-
const monetarySummationElement = doc.createElement('ram:
|
|
469
|
-
|
|
470
|
-
// Calculate item total with decimal arithmetic and currency rounding.
|
|
471
|
-
const itemNetAmount = calculator.calculateLineNet(item.unitQuantity, item.unitNetPrice);
|
|
464
|
+
// Line monetary summation, as the CII schema names it
|
|
465
|
+
const monetarySummationElement = doc.createElement('ram:SpecifiedTradeSettlementLineMonetarySummation');
|
|
472
466
|
|
|
473
|
-
//
|
|
467
|
+
// Invoice line net amount (BT-131): quantity × net price, rounded
|
|
474
468
|
const lineTotalElement = doc.createElement('ram:LineTotalAmount');
|
|
475
|
-
lineTotalElement.textContent =
|
|
469
|
+
lineTotalElement.textContent = totals.lineNetAmounts[index].toFixed(totals.minorUnits);
|
|
476
470
|
monetarySummationElement.appendChild(lineTotalElement);
|
|
477
471
|
|
|
478
472
|
settlementElement.appendChild(monetarySummationElement);
|
|
@@ -84,8 +84,8 @@ export class ZUGFeRDDecoder extends CIIBaseDecoder {
|
|
|
84
84
|
// Extract the actual delivery date, if stated
|
|
85
85
|
const deliveryDate = this.extractDeliveryDate();
|
|
86
86
|
|
|
87
|
-
//
|
|
88
|
-
const reverseCharge = this.
|
|
87
|
+
// Reverse charge: every line has the VAT category AE
|
|
88
|
+
const reverseCharge = this.isReverseChargeDocument();
|
|
89
89
|
|
|
90
90
|
// Create the common invoice data
|
|
91
91
|
const invoiceData = {
|
|
@@ -161,7 +161,12 @@ export class ZUGFeRDDecoder extends CIIBaseDecoder {
|
|
|
161
161
|
const vatId = this.getText(`${partyXPath}/ram:SpecifiedTaxRegistration/ram:ID[@schemeID="VA"]`) || '';
|
|
162
162
|
|
|
163
163
|
// Extract registration ID
|
|
164
|
-
|
|
164
|
+
// the legal registration identifier (BT-30, BT-47); a document that states only a tax
|
|
165
|
+
// number (scheme FC, BT-32) keeps it here as before, the envelope has no field of its own for it
|
|
166
|
+
const registrationId =
|
|
167
|
+
this.getText(`${partyXPath}/ram:SpecifiedLegalOrganization/ram:ID`) ||
|
|
168
|
+
this.getText(`${partyXPath}/ram:SpecifiedTaxRegistration/ram:ID[@schemeID="FC"]`) ||
|
|
169
|
+
'';
|
|
165
170
|
|
|
166
171
|
// Create contact object
|
|
167
172
|
return {
|
|
@@ -6,6 +6,7 @@ import { DOMParser, XMLSerializer } from '../../../plugins.js';
|
|
|
6
6
|
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
|
+
import { computeDocumentTotals } from '../../utils/document.totals.js';
|
|
9
10
|
|
|
10
11
|
/**
|
|
11
12
|
* Encoder for ZUGFeRD invoice format
|
|
@@ -248,6 +249,17 @@ export class ZUGFeRDEncoder extends CIIBaseEncoder {
|
|
|
248
249
|
nameElement.textContent = party.name;
|
|
249
250
|
partyElement.appendChild(nameElement);
|
|
250
251
|
|
|
252
|
+
// Legal registration identifier (BT-30 seller, BT-47 buyer), e.g. the commercial register
|
|
253
|
+
// number; in the schema order it follows the name. It is no tax registration: the scheme
|
|
254
|
+
// FC of ram:SpecifiedTaxRegistration is the seller's tax number (BT-32)
|
|
255
|
+
if (party.registrationDetails && party.registrationDetails.registrationId) {
|
|
256
|
+
const legalOrganizationElement = doc.createElement('ram:SpecifiedLegalOrganization');
|
|
257
|
+
const legalIdElement = doc.createElement('ram:ID');
|
|
258
|
+
legalIdElement.textContent = party.registrationDetails.registrationId;
|
|
259
|
+
legalOrganizationElement.appendChild(legalIdElement);
|
|
260
|
+
partyElement.appendChild(legalOrganizationElement);
|
|
261
|
+
}
|
|
262
|
+
|
|
251
263
|
// Add postal address
|
|
252
264
|
const addressElement = doc.createElement('ram:PostalTradeAddress');
|
|
253
265
|
|
|
@@ -298,15 +310,6 @@ export class ZUGFeRDEncoder extends CIIBaseEncoder {
|
|
|
298
310
|
partyElement.appendChild(taxRegistrationElement);
|
|
299
311
|
}
|
|
300
312
|
|
|
301
|
-
// Add registration ID if available
|
|
302
|
-
if (party.registrationDetails && party.registrationDetails.registrationId) {
|
|
303
|
-
const regRegistrationElement = doc.createElement('ram:SpecifiedTaxRegistration');
|
|
304
|
-
const regIdElement = doc.createElement('ram:ID');
|
|
305
|
-
regIdElement.setAttribute('schemeID', 'FC');
|
|
306
|
-
regIdElement.textContent = party.registrationDetails.registrationId;
|
|
307
|
-
regRegistrationElement.appendChild(regIdElement);
|
|
308
|
-
partyElement.appendChild(regRegistrationElement);
|
|
309
|
-
}
|
|
310
313
|
}
|
|
311
314
|
|
|
312
315
|
/**
|
|
@@ -456,46 +459,46 @@ export class ZUGFeRDEncoder extends CIIBaseEncoder {
|
|
|
456
459
|
* @param invoice Invoice data
|
|
457
460
|
*/
|
|
458
461
|
private addTaxDetails(doc: Document, settlementElement: Element, invoice: TAccountingDoc): void {
|
|
459
|
-
//
|
|
460
|
-
const
|
|
462
|
+
// the EN 16931 totals in decimal arithmetic, rounded as the business rules check them
|
|
463
|
+
const totals = computeDocumentTotals(invoice);
|
|
461
464
|
|
|
462
|
-
//
|
|
463
|
-
|
|
464
|
-
for (const item of invoice.items) {
|
|
465
|
-
const itemNetAmount = item.unitNetPrice * item.unitQuantity;
|
|
466
|
-
const vatRate = item.vatPercentage;
|
|
467
|
-
|
|
468
|
-
const currentAmount = taxCategories.get(vatRate) || 0;
|
|
469
|
-
taxCategories.set(vatRate, currentAmount + itemNetAmount);
|
|
470
|
-
}
|
|
471
|
-
}
|
|
472
|
-
|
|
473
|
-
// Add each tax category
|
|
474
|
-
for (const [rate, baseAmount] of taxCategories.entries()) {
|
|
465
|
+
// VAT breakdown (BG-23), one per VAT category and rate
|
|
466
|
+
for (const { category, rate, taxableAmount, taxAmount, exemption } of totals.vatGroups) {
|
|
475
467
|
const taxElement = doc.createElement('ram:ApplicableTradeTax');
|
|
476
|
-
|
|
477
|
-
//
|
|
478
|
-
const taxAmount = baseAmount * (rate / 100);
|
|
479
|
-
|
|
480
|
-
// Add calculated amount
|
|
468
|
+
|
|
469
|
+
// VAT category tax amount (BT-117)
|
|
481
470
|
const calculatedAmountElement = doc.createElement('ram:CalculatedAmount');
|
|
482
|
-
calculatedAmountElement.textContent = taxAmount.toFixed(
|
|
471
|
+
calculatedAmountElement.textContent = taxAmount.toFixed(totals.minorUnits);
|
|
483
472
|
taxElement.appendChild(calculatedAmountElement);
|
|
484
473
|
|
|
485
474
|
// Add type code (VAT)
|
|
486
475
|
const typeCodeElement = doc.createElement('ram:TypeCode');
|
|
487
476
|
typeCodeElement.textContent = 'VAT';
|
|
488
477
|
taxElement.appendChild(typeCodeElement);
|
|
478
|
+
|
|
479
|
+
// VAT exemption reason text (BT-120), for reverse charge (BR-AE-10)
|
|
480
|
+
if (exemption) {
|
|
481
|
+
const exemptionReasonElement = doc.createElement('ram:ExemptionReason');
|
|
482
|
+
exemptionReasonElement.textContent = exemption.reason;
|
|
483
|
+
taxElement.appendChild(exemptionReasonElement);
|
|
484
|
+
}
|
|
489
485
|
|
|
490
|
-
//
|
|
486
|
+
// VAT category taxable amount (BT-116)
|
|
491
487
|
const basisAmountElement = doc.createElement('ram:BasisAmount');
|
|
492
|
-
basisAmountElement.textContent =
|
|
488
|
+
basisAmountElement.textContent = taxableAmount.toFixed(totals.minorUnits);
|
|
493
489
|
taxElement.appendChild(basisAmountElement);
|
|
494
490
|
|
|
495
491
|
// Add category code
|
|
496
492
|
const categoryCodeElement = doc.createElement('ram:CategoryCode');
|
|
497
|
-
categoryCodeElement.textContent =
|
|
493
|
+
categoryCodeElement.textContent = category;
|
|
498
494
|
taxElement.appendChild(categoryCodeElement);
|
|
495
|
+
|
|
496
|
+
// VAT exemption reason code (BT-121)
|
|
497
|
+
if (exemption) {
|
|
498
|
+
const exemptionReasonCodeElement = doc.createElement('ram:ExemptionReasonCode');
|
|
499
|
+
exemptionReasonCodeElement.textContent = exemption.code;
|
|
500
|
+
taxElement.appendChild(exemptionReasonCodeElement);
|
|
501
|
+
}
|
|
499
502
|
|
|
500
503
|
// Add rate
|
|
501
504
|
const rateElement = doc.createElement('ram:RateApplicablePercent');
|
|
@@ -515,43 +518,23 @@ export class ZUGFeRDEncoder extends CIIBaseEncoder {
|
|
|
515
518
|
private addMonetarySummation(doc: Document, settlementElement: Element, invoice: TAccountingDoc): void {
|
|
516
519
|
const monetarySummationElement = doc.createElement('ram:SpecifiedTradeSettlementHeaderMonetarySummation');
|
|
517
520
|
|
|
518
|
-
//
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
const itemNetAmount = item.unitNetPrice * item.unitQuantity;
|
|
526
|
-
const itemTaxAmount = itemNetAmount * (item.vatPercentage / 100);
|
|
527
|
-
|
|
528
|
-
totalNetAmount += itemNetAmount;
|
|
529
|
-
totalTaxAmount += itemTaxAmount;
|
|
521
|
+
// the EN 16931 totals in decimal arithmetic, rounded as the business rules check them
|
|
522
|
+
const totals = computeDocumentTotals(invoice);
|
|
523
|
+
const appendAmount = (name: string, value: { toFixed(decimalPlaces: number): string }, withCurrency = false) => {
|
|
524
|
+
const element = doc.createElement(name);
|
|
525
|
+
element.textContent = value.toFixed(totals.minorUnits);
|
|
526
|
+
if (withCurrency) {
|
|
527
|
+
element.setAttribute('currencyID', invoice.currency);
|
|
530
528
|
}
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
const totalGrossAmount = totalNetAmount + totalTaxAmount;
|
|
534
|
-
|
|
535
|
-
// Add line total amount
|
|
536
|
-
const lineTotalElement = doc.createElement('ram:LineTotalAmount');
|
|
537
|
-
lineTotalElement.textContent = totalNetAmount.toFixed(2);
|
|
538
|
-
monetarySummationElement.appendChild(lineTotalElement);
|
|
529
|
+
monetarySummationElement.appendChild(element);
|
|
530
|
+
};
|
|
539
531
|
|
|
540
|
-
//
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
// Add grand total amount
|
|
547
|
-
const grandTotalElement = doc.createElement('ram:GrandTotalAmount');
|
|
548
|
-
grandTotalElement.textContent = totalGrossAmount.toFixed(2);
|
|
549
|
-
monetarySummationElement.appendChild(grandTotalElement);
|
|
550
|
-
|
|
551
|
-
// Add due payable amount
|
|
552
|
-
const duePayableElement = doc.createElement('ram:DuePayableAmount');
|
|
553
|
-
duePayableElement.textContent = totalGrossAmount.toFixed(2);
|
|
554
|
-
monetarySummationElement.appendChild(duePayableElement);
|
|
532
|
+
// in the order of the CII schema
|
|
533
|
+
appendAmount('ram:LineTotalAmount', totals.lineTotal); // BT-106
|
|
534
|
+
appendAmount('ram:TaxBasisTotalAmount', totals.taxBasisTotal); // BT-109
|
|
535
|
+
appendAmount('ram:TaxTotalAmount', totals.taxTotal, true); // BT-110
|
|
536
|
+
appendAmount('ram:GrandTotalAmount', totals.grandTotal); // BT-112
|
|
537
|
+
appendAmount('ram:DuePayableAmount', totals.duePayable); // BT-115
|
|
555
538
|
|
|
556
539
|
settlementElement.appendChild(monetarySummationElement);
|
|
557
540
|
}
|
|
@@ -565,7 +548,8 @@ export class ZUGFeRDEncoder extends CIIBaseEncoder {
|
|
|
565
548
|
private addLineItems(doc: Document, transactionElement: Element, invoice: TAccountingDoc): void {
|
|
566
549
|
// Add each line item
|
|
567
550
|
if (invoice.items) {
|
|
568
|
-
|
|
551
|
+
const totals = computeDocumentTotals(invoice);
|
|
552
|
+
for (const [index, item] of invoice.items.entries()) {
|
|
569
553
|
// Create line item element
|
|
570
554
|
const lineItemElement = doc.createElement('ram:IncludedSupplyChainTradeLineItem');
|
|
571
555
|
|
|
@@ -626,7 +610,7 @@ export class ZUGFeRDEncoder extends CIIBaseEncoder {
|
|
|
626
610
|
|
|
627
611
|
// Add tax category code
|
|
628
612
|
const taxCategoryCodeElement = doc.createElement('ram:CategoryCode');
|
|
629
|
-
taxCategoryCodeElement.textContent =
|
|
613
|
+
taxCategoryCodeElement.textContent = totals.lineVatCategories[index]; // BT-151
|
|
630
614
|
taxElement.appendChild(taxCategoryCodeElement);
|
|
631
615
|
|
|
632
616
|
// Add tax rate
|
|
@@ -636,15 +620,12 @@ export class ZUGFeRDEncoder extends CIIBaseEncoder {
|
|
|
636
620
|
|
|
637
621
|
settlementElement.appendChild(taxElement);
|
|
638
622
|
|
|
639
|
-
//
|
|
640
|
-
const monetarySummationElement = doc.createElement('ram:
|
|
641
|
-
|
|
642
|
-
// Calculate item total
|
|
643
|
-
const itemNetAmount = item.unitNetPrice * item.unitQuantity;
|
|
623
|
+
// Line monetary summation, as the CII schema names it
|
|
624
|
+
const monetarySummationElement = doc.createElement('ram:SpecifiedTradeSettlementLineMonetarySummation');
|
|
644
625
|
|
|
645
|
-
//
|
|
626
|
+
// Invoice line net amount (BT-131): quantity × net price, rounded
|
|
646
627
|
const lineTotalElement = doc.createElement('ram:LineTotalAmount');
|
|
647
|
-
lineTotalElement.textContent =
|
|
628
|
+
lineTotalElement.textContent = totals.lineNetAmounts[index].toFixed(totals.minorUnits);
|
|
648
629
|
monetarySummationElement.appendChild(lineTotalElement);
|
|
649
630
|
|
|
650
631
|
settlementElement.appendChild(monetarySummationElement);
|
|
@@ -35,6 +35,19 @@ export class ZUGFeRDV1Decoder extends CIIBaseDecoder {
|
|
|
35
35
|
};
|
|
36
36
|
}
|
|
37
37
|
|
|
38
|
+
/**
|
|
39
|
+
* Whether every line of the v1 document has the VAT category reverse charge (AE), read with
|
|
40
|
+
* the v1 namespaces from the line settlement (`ram:SpecifiedSupplyChainTradeSettlement`)
|
|
41
|
+
*/
|
|
42
|
+
protected override isReverseChargeDocument(): boolean {
|
|
43
|
+
const categories = zugferdV1Select(
|
|
44
|
+
'/rsm:CrossIndustryDocument/rsm:SpecifiedSupplyChainTradeTransaction/ram:IncludedSupplyChainTradeLineItem/ram:SpecifiedSupplyChainTradeSettlement/ram:ApplicableTradeTax/ram:CategoryCode',
|
|
45
|
+
this.doc,
|
|
46
|
+
);
|
|
47
|
+
const codes = (Array.isArray(categories) ? categories : []).map((node) => ((node as Node).textContent ?? '').trim());
|
|
48
|
+
return codes.length > 0 && codes.every((code) => code === 'AE');
|
|
49
|
+
}
|
|
50
|
+
|
|
38
51
|
/**
|
|
39
52
|
* Reads the issue date of the v1 header (`rsm:HeaderExchangedDocument`); a
|
|
40
53
|
* missing or invalid one is refused with an `EInvoiceParsingError`.
|
|
@@ -147,8 +160,8 @@ export class ZUGFeRDV1Decoder extends CIIBaseDecoder {
|
|
|
147
160
|
// Extract the actual delivery date, if stated
|
|
148
161
|
const deliveryDate = this.extractDeliveryDate();
|
|
149
162
|
|
|
150
|
-
//
|
|
151
|
-
const reverseCharge = this.
|
|
163
|
+
// Reverse charge: every line has the VAT category AE
|
|
164
|
+
const reverseCharge = this.isReverseChargeDocument();
|
|
152
165
|
|
|
153
166
|
// Create the common invoice data
|
|
154
167
|
const invoiceData = {
|
|
@@ -217,8 +230,13 @@ export class ZUGFeRDV1Decoder extends CIIBaseDecoder {
|
|
|
217
230
|
// Extract VAT ID
|
|
218
231
|
const vatId = this.getText(`${partyXPath}/ram:SpecifiedTaxRegistration/ram:ID[@schemeID="VA"]`) || '';
|
|
219
232
|
|
|
220
|
-
//
|
|
221
|
-
|
|
233
|
+
// the legal registration identifier, and a tax number (scheme FC) as before when that is all
|
|
234
|
+
// the document states; the party is still read with the ZUGFeRD 2 namespaces (the known v1
|
|
235
|
+
// header defect), so for a v1 document this finds nothing yet
|
|
236
|
+
const registrationId =
|
|
237
|
+
this.getText(`${partyXPath}/ram:SpecifiedLegalOrganization/ram:ID`) ||
|
|
238
|
+
this.getText(`${partyXPath}/ram:SpecifiedTaxRegistration/ram:ID[@schemeID="FC"]`) ||
|
|
239
|
+
'';
|
|
222
240
|
|
|
223
241
|
// Create contact object
|
|
224
242
|
return {
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
* Validates invoices against EN16931 Business Terms and Business Groups
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
+
import { findInvalidItemAmounts, getTotalsSkippedResult } from '../utils/document.totals.js';
|
|
6
7
|
import type { ValidationResult } from '../validation/validation.types.js';
|
|
7
8
|
import type { EN16931SemanticModel, BusinessTerms, BusinessGroups } from './bt-bg.model.js';
|
|
8
9
|
import type { EInvoice } from '../../einvoice.js';
|
|
@@ -36,6 +37,25 @@ export class SemanticModelValidator {
|
|
|
36
37
|
*/
|
|
37
38
|
public validate(invoice: EInvoice): ValidationResult[] {
|
|
38
39
|
const results: ValidationResult[] = [];
|
|
40
|
+
|
|
41
|
+
// The semantic model carries the totals, which are computed from the line amounts; while
|
|
42
|
+
// one is no number the model cannot be built, so each such line is reported (BR-24 for the
|
|
43
|
+
// net amount, BR-CO-04 for the VAT rate) and the model rules are skipped
|
|
44
|
+
const invalidAmounts = findInvalidItemAmounts(invoice.items);
|
|
45
|
+
if (invalidAmounts.length > 0) {
|
|
46
|
+
for (const entry of invalidAmounts) {
|
|
47
|
+
results.push({
|
|
48
|
+
ruleId: entry.field === 'vatPercentage' ? 'BR-CO-04' : 'BR-24',
|
|
49
|
+
source: 'SEMANTIC',
|
|
50
|
+
severity: 'error',
|
|
51
|
+
message: `Invoice line ${entry.index + 1}: ${entry.field} is no number`,
|
|
52
|
+
field: `items[${entry.index}].${entry.field}`,
|
|
53
|
+
value: entry.value,
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
results.push(getTotalsSkippedResult(invalidAmounts, 'SEMANTIC'));
|
|
57
|
+
return results;
|
|
58
|
+
}
|
|
39
59
|
|
|
40
60
|
// Convert to semantic model
|
|
41
61
|
const model = this.adapter.toSemanticModel(invoice);
|