@fin.cx/einvoice 8.2.0 → 8.2.2
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/base/base.decoder.js +4 -3
- package/dist_ts/formats/cii/cii.decoder.d.ts +15 -0
- package/dist_ts/formats/cii/cii.decoder.js +28 -1
- package/dist_ts/formats/cii/facturx/facturx.decoder.js +3 -5
- package/dist_ts/formats/cii/facturx/facturx.encoder.js +46 -53
- package/dist_ts/formats/cii/zugferd/zugferd.decoder.js +4 -7
- package/dist_ts/formats/cii/zugferd/zugferd.encoder.js +42 -63
- package/dist_ts/formats/cii/zugferd/zugferd.v1.decoder.d.ts +12 -0
- package/dist_ts/formats/cii/zugferd/zugferd.v1.decoder.js +25 -7
- package/dist_ts/formats/semantic/semantic.validator.js +20 -1
- package/dist_ts/formats/ubl/generic/ubl.encoder.js +28 -52
- package/dist_ts/formats/ubl/ubl.types.d.ts +6 -0
- package/dist_ts/formats/ubl/ubl.types.js +9 -3
- package/dist_ts/formats/ubl/xrechnung/xrechnung.decoder.js +10 -8
- package/dist_ts/formats/ubl/xrechnung/xrechnung.encoder.js +4 -11
- 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/date.value.d.ts +18 -0
- package/dist_ts/formats/utils/date.value.js +30 -1
- package/dist_ts/formats/utils/document.totals.d.ts +79 -0
- package/dist_ts/formats/utils/document.totals.js +93 -0
- package/dist_ts/formats/utils/format.detector.js +6 -4
- package/dist_ts/formats/utils/payment.terms.d.ts +18 -0
- package/dist_ts/formats/utils/payment.terms.js +32 -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/integrated.validator.js +6 -3
- package/dist_ts/formats/validation/vat-categories.validator.js +9 -1
- package/dist_ts/formats/validation/xrechnung.validator.js +6 -4
- package/package.json +2 -2
- package/readme.md +17 -0
- package/ts/00_commitinfo_data.ts +1 -1
- package/ts/einvoice.ts +11 -25
- package/ts/formats/base/base.decoder.ts +3 -2
- package/ts/formats/cii/cii.decoder.ts +31 -0
- package/ts/formats/cii/facturx/facturx.decoder.ts +2 -4
- package/ts/formats/cii/facturx/facturx.encoder.ts +47 -61
- package/ts/formats/cii/zugferd/zugferd.decoder.ts +3 -6
- package/ts/formats/cii/zugferd/zugferd.encoder.ts +42 -73
- package/ts/formats/cii/zugferd/zugferd.v1.decoder.ts +28 -6
- package/ts/formats/semantic/semantic.validator.ts +20 -0
- package/ts/formats/ubl/generic/ubl.encoder.ts +33 -63
- package/ts/formats/ubl/ubl.types.ts +8 -2
- package/ts/formats/ubl/xrechnung/xrechnung.decoder.ts +11 -7
- package/ts/formats/ubl/xrechnung/xrechnung.encoder.ts +3 -11
- package/ts/formats/utils/currency.calculator.decimal.ts +10 -2
- package/ts/formats/utils/date.value.ts +31 -0
- package/ts/formats/utils/document.totals.ts +144 -0
- package/ts/formats/utils/format.detector.ts +5 -5
- package/ts/formats/utils/payment.terms.ts +37 -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/integrated.validator.ts +5 -2
- package/ts/formats/validation/vat-categories.validator.ts +9 -0
- package/ts/formats/validation/xrechnung.validator.ts +5 -3
|
@@ -127,6 +127,37 @@ export abstract class CIIBaseDecoder extends BaseDecoder {
|
|
|
127
127
|
return references;
|
|
128
128
|
}
|
|
129
129
|
|
|
130
|
+
/**
|
|
131
|
+
* Reads the invoice issue date (BT-2); a missing or invalid one is refused
|
|
132
|
+
* with an `EInvoiceParsingError`, as no other date can stand in for it.
|
|
133
|
+
*/
|
|
134
|
+
protected extractIssueDate(): number {
|
|
135
|
+
const issueDatePath = '/rsm:CrossIndustryInvoice/rsm:ExchangedDocument/ram:IssueDateTime/udt:DateTimeString';
|
|
136
|
+
return this.parseRequiredCIIDate(this.getText(issueDatePath), this.getText(`${issueDatePath}/@format`).trim());
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* Reads the payment due date (BT-9) of the header payment terms; undefined
|
|
141
|
+
* when the document states none.
|
|
142
|
+
*/
|
|
143
|
+
protected extractDueDate(): number | undefined {
|
|
144
|
+
const dueDatePath =
|
|
145
|
+
'/rsm:CrossIndustryInvoice/rsm:SupplyChainTradeTransaction/ram:ApplicableHeaderTradeSettlement/ram:SpecifiedTradePaymentTerms/ram:DueDateDateTime/udt:DateTimeString';
|
|
146
|
+
const dueDate = this.getText(dueDatePath).trim();
|
|
147
|
+
if (!dueDate) {
|
|
148
|
+
return undefined;
|
|
149
|
+
}
|
|
150
|
+
return this.parseRequiredCIIDate(dueDate, this.getText(`${dueDatePath}/@format`).trim());
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* The payment term in days from the issue date to the stated due date, or
|
|
155
|
+
* `absentDueInDays` when the document states no due date.
|
|
156
|
+
*/
|
|
157
|
+
protected dueInDaysOf(issueDate: number, dueDate: number | undefined, absentDueInDays: number): number {
|
|
158
|
+
return dueDate === undefined ? absentDueInDays : Math.round((dueDate - issueDate) / (1000 * 60 * 60 * 24));
|
|
159
|
+
}
|
|
160
|
+
|
|
130
161
|
/**
|
|
131
162
|
* Reads the actual delivery date (BT-72) of the header delivery; undefined
|
|
132
163
|
* when the document does not state one.
|
|
@@ -67,10 +67,8 @@ export class FacturXDecoder extends CIIBaseDecoder {
|
|
|
67
67
|
const items = this.extractItems();
|
|
68
68
|
|
|
69
69
|
// Extract due date
|
|
70
|
-
|
|
71
|
-
const
|
|
72
|
-
const dueDate = dueDateStr ? this.parseCIIDate(dueDateStr, dueDateFormat) : issueDate;
|
|
73
|
-
const dueInDays = Math.round((dueDate - issueDate) / (1000 * 60 * 60 * 24));
|
|
70
|
+
// no due date stated: `dueInDays` cannot express it yet (required field in @tsclass/tsclass); value unchanged: 0 days
|
|
71
|
+
const dueInDays = this.dueInDaysOf(issueDate, this.extractDueDate(), 0);
|
|
74
72
|
|
|
75
73
|
// Extract currency
|
|
76
74
|
const currencyCode = this.getText('//ram:InvoiceCurrencyCode') || 'EUR';
|
|
@@ -2,11 +2,9 @@ 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';
|
|
9
|
-
import {
|
|
6
|
+
import { computeDocumentTotals } from '../../utils/document.totals.js';
|
|
7
|
+
import { getWritableDate, getWritableDueDate } from '../../utils/date.value.js';
|
|
10
8
|
|
|
11
9
|
/**
|
|
12
10
|
* Encoder for Factur-X invoice format
|
|
@@ -316,6 +314,26 @@ export class FacturXEncoder extends CIIBaseEncoder {
|
|
|
316
314
|
currencyElement.textContent = invoice.currency;
|
|
317
315
|
settlementElement.appendChild(currencyElement);
|
|
318
316
|
|
|
317
|
+
// the EN 16931 totals in decimal arithmetic, rounded as the business rules check them
|
|
318
|
+
const totals = computeDocumentTotals(invoice);
|
|
319
|
+
|
|
320
|
+
// VAT breakdown (BG-23), one per rate, before the payment terms as the CII schema orders them;
|
|
321
|
+
// the category is the one the lines carry
|
|
322
|
+
for (const { rate, taxableAmount, taxAmount } of totals.vatGroups) {
|
|
323
|
+
const taxElement = doc.createElement('ram:ApplicableTradeTax');
|
|
324
|
+
const appendText = (name: string, text: string) => {
|
|
325
|
+
const element = doc.createElement(name);
|
|
326
|
+
element.textContent = text;
|
|
327
|
+
taxElement.appendChild(element);
|
|
328
|
+
};
|
|
329
|
+
appendText('ram:CalculatedAmount', taxAmount.toFixed(totals.minorUnits)); // BT-117
|
|
330
|
+
appendText('ram:TypeCode', 'VAT');
|
|
331
|
+
appendText('ram:BasisAmount', taxableAmount.toFixed(totals.minorUnits)); // BT-116
|
|
332
|
+
appendText('ram:CategoryCode', 'S'); // BT-118
|
|
333
|
+
appendText('ram:RateApplicablePercent', rate.toString()); // BT-119
|
|
334
|
+
settlementElement.appendChild(taxElement);
|
|
335
|
+
}
|
|
336
|
+
|
|
319
337
|
// Add payment terms
|
|
320
338
|
const paymentTermsElement = doc.createElement('ram:SpecifiedTradePaymentTerms');
|
|
321
339
|
|
|
@@ -325,8 +343,7 @@ export class FacturXEncoder extends CIIBaseEncoder {
|
|
|
325
343
|
dateStringElement.setAttribute('format', '102'); // YYYYMMDD format
|
|
326
344
|
|
|
327
345
|
// Calculate due date
|
|
328
|
-
const dueDate =
|
|
329
|
-
dueDate.setDate(dueDate.getDate() + invoice.dueInDays);
|
|
346
|
+
const dueDate = getWritableDueDate(invoice.date, invoice.dueInDays, 'cii');
|
|
330
347
|
|
|
331
348
|
dateStringElement.textContent = this.formatDateYYYYMMDD(dueDate.getTime(), 'BT-9 payment due date');
|
|
332
349
|
dueDateElement.appendChild(dateStringElement);
|
|
@@ -334,50 +351,21 @@ export class FacturXEncoder extends CIIBaseEncoder {
|
|
|
334
351
|
|
|
335
352
|
settlementElement.appendChild(paymentTermsElement);
|
|
336
353
|
|
|
337
|
-
// Add totals
|
|
354
|
+
// Add totals, in the order of the CII schema
|
|
338
355
|
const monetarySummationElement = doc.createElement('ram:SpecifiedTradeSettlementHeaderMonetarySummation');
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
let totalTaxAmount = Decimal.ZERO;
|
|
345
|
-
|
|
346
|
-
// Calculate from items
|
|
347
|
-
if (invoice.items) {
|
|
348
|
-
for (const item of invoice.items) {
|
|
349
|
-
const itemNetAmount = calculator.calculateLineNet(item.unitQuantity, item.unitNetPrice);
|
|
350
|
-
const itemTaxAmount = calculator.calculateVAT(itemNetAmount, item.vatPercentage);
|
|
351
|
-
|
|
352
|
-
totalNetAmount = totalNetAmount.add(itemNetAmount);
|
|
353
|
-
totalTaxAmount = totalTaxAmount.add(itemTaxAmount);
|
|
356
|
+
const appendAmount = (name: string, value: { toFixed(decimalPlaces: number): string }, withCurrency = false) => {
|
|
357
|
+
const element = doc.createElement(name);
|
|
358
|
+
element.textContent = value.toFixed(totals.minorUnits);
|
|
359
|
+
if (withCurrency) {
|
|
360
|
+
element.setAttribute('currencyID', invoice.currency);
|
|
354
361
|
}
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
const lineTotalElement = doc.createElement('ram:LineTotalAmount');
|
|
363
|
-
lineTotalElement.textContent = totalNetAmount.toFixed(minorUnits);
|
|
364
|
-
monetarySummationElement.appendChild(lineTotalElement);
|
|
365
|
-
|
|
366
|
-
// Add tax total amount
|
|
367
|
-
const taxTotalElement = doc.createElement('ram:TaxTotalAmount');
|
|
368
|
-
taxTotalElement.textContent = totalTaxAmount.toFixed(minorUnits);
|
|
369
|
-
taxTotalElement.setAttribute('currencyID', invoice.currency);
|
|
370
|
-
monetarySummationElement.appendChild(taxTotalElement);
|
|
371
|
-
|
|
372
|
-
// Add grand total amount
|
|
373
|
-
const grandTotalElement = doc.createElement('ram:GrandTotalAmount');
|
|
374
|
-
grandTotalElement.textContent = totalGrossAmount.toFixed(minorUnits);
|
|
375
|
-
monetarySummationElement.appendChild(grandTotalElement);
|
|
376
|
-
|
|
377
|
-
// Add due payable amount
|
|
378
|
-
const duePayableElement = doc.createElement('ram:DuePayableAmount');
|
|
379
|
-
duePayableElement.textContent = totalGrossAmount.toFixed(minorUnits);
|
|
380
|
-
monetarySummationElement.appendChild(duePayableElement);
|
|
362
|
+
monetarySummationElement.appendChild(element);
|
|
363
|
+
};
|
|
364
|
+
appendAmount('ram:LineTotalAmount', totals.lineTotal); // BT-106
|
|
365
|
+
appendAmount('ram:TaxBasisTotalAmount', totals.taxBasisTotal); // BT-109
|
|
366
|
+
appendAmount('ram:TaxTotalAmount', totals.taxTotal, true); // BT-110
|
|
367
|
+
appendAmount('ram:GrandTotalAmount', totals.grandTotal); // BT-112
|
|
368
|
+
appendAmount('ram:DuePayableAmount', totals.duePayable); // BT-115
|
|
381
369
|
|
|
382
370
|
settlementElement.appendChild(monetarySummationElement);
|
|
383
371
|
}
|
|
@@ -389,12 +377,12 @@ export class FacturXEncoder extends CIIBaseEncoder {
|
|
|
389
377
|
* @param invoice Invoice data
|
|
390
378
|
*/
|
|
391
379
|
private addLineItems(doc: Document, transactionElement: Element, invoice: TAccountingDoc): void {
|
|
392
|
-
|
|
393
|
-
const
|
|
380
|
+
// the line net amounts of the one EN 16931 totals computation
|
|
381
|
+
const totals = computeDocumentTotals(invoice);
|
|
394
382
|
|
|
395
383
|
// Add each line item
|
|
396
384
|
if (invoice.items) {
|
|
397
|
-
for (const item of invoice.items) {
|
|
385
|
+
for (const [index, item] of invoice.items.entries()) {
|
|
398
386
|
// Create line item element
|
|
399
387
|
const lineItemElement = doc.createElement('ram:IncludedSupplyChainTradeLineItem');
|
|
400
388
|
|
|
@@ -465,15 +453,12 @@ export class FacturXEncoder extends CIIBaseEncoder {
|
|
|
465
453
|
|
|
466
454
|
settlementElement.appendChild(taxElement);
|
|
467
455
|
|
|
468
|
-
//
|
|
469
|
-
const monetarySummationElement = doc.createElement('ram:
|
|
470
|
-
|
|
471
|
-
// Calculate item total with decimal arithmetic and currency rounding.
|
|
472
|
-
const itemNetAmount = calculator.calculateLineNet(item.unitQuantity, item.unitNetPrice);
|
|
456
|
+
// Line monetary summation, as the CII schema names it
|
|
457
|
+
const monetarySummationElement = doc.createElement('ram:SpecifiedTradeSettlementLineMonetarySummation');
|
|
473
458
|
|
|
474
|
-
//
|
|
459
|
+
// Invoice line net amount (BT-131): quantity × net price, rounded
|
|
475
460
|
const lineTotalElement = doc.createElement('ram:LineTotalAmount');
|
|
476
|
-
lineTotalElement.textContent =
|
|
461
|
+
lineTotalElement.textContent = totals.lineNetAmounts[index].toFixed(totals.minorUnits);
|
|
477
462
|
monetarySummationElement.appendChild(lineTotalElement);
|
|
478
463
|
|
|
479
464
|
settlementElement.appendChild(monetarySummationElement);
|
|
@@ -493,9 +478,10 @@ export class FacturXEncoder extends CIIBaseEncoder {
|
|
|
493
478
|
*/
|
|
494
479
|
private formatDateYYYYMMDD(timestamp: number, field: string): string {
|
|
495
480
|
const date = getWritableDate(timestamp, field, 'cii');
|
|
496
|
-
|
|
497
|
-
const
|
|
498
|
-
const
|
|
481
|
+
// the UTC fields: a calendar day is its UTC midnight, whatever the server's zone
|
|
482
|
+
const year = date.getUTCFullYear();
|
|
483
|
+
const month = (date.getUTCMonth() + 1).toString().padStart(2, '0');
|
|
484
|
+
const day = date.getUTCDate().toString().padStart(2, '0');
|
|
499
485
|
return `${year}${month}${day}`;
|
|
500
486
|
}
|
|
501
487
|
}
|
|
@@ -44,9 +44,7 @@ export class ZUGFeRDDecoder extends CIIBaseDecoder {
|
|
|
44
44
|
const invoiceId = this.getText('//rsm:ExchangedDocument/ram:ID');
|
|
45
45
|
|
|
46
46
|
// Extract issue date
|
|
47
|
-
const
|
|
48
|
-
const issueDateFormat = this.getText('//ram:IssueDateTime/udt:DateTimeString/@format');
|
|
49
|
-
const issueDate = this.parseCIIDate(issueDateStr, issueDateFormat);
|
|
47
|
+
const issueDate = this.extractIssueDate();
|
|
50
48
|
|
|
51
49
|
// Extract seller information
|
|
52
50
|
const seller = this.extractParty('//ram:SellerTradeParty');
|
|
@@ -58,9 +56,8 @@ export class ZUGFeRDDecoder extends CIIBaseDecoder {
|
|
|
58
56
|
const items = this.extractItems();
|
|
59
57
|
|
|
60
58
|
// Extract due date
|
|
61
|
-
|
|
62
|
-
const
|
|
63
|
-
const dueInDays = Math.round((dueDate - issueDate) / (1000 * 60 * 60 * 24));
|
|
59
|
+
// no due date stated: `dueInDays` cannot express it yet (required field in @tsclass/tsclass); value unchanged: the days from the issue date to the moment of decoding
|
|
60
|
+
const dueInDays = this.dueInDaysOf(issueDate, this.extractDueDate(), Math.round((Date.now() - issueDate) / (1000 * 60 * 60 * 24)));
|
|
64
61
|
|
|
65
62
|
// Extract currency
|
|
66
63
|
const currencyCode = this.getText('//ram:InvoiceCurrencyCode') || 'EUR';
|
|
@@ -4,7 +4,9 @@ import { ZUGFERD_PROFILE_IDS } from './zugferd.types.js';
|
|
|
4
4
|
import { CIIProfile } from '../cii.types.js';
|
|
5
5
|
import { DOMParser, XMLSerializer } from '../../../plugins.js';
|
|
6
6
|
import { getDocumentTypeCode } from '../../utils/document.typecode.js';
|
|
7
|
-
import { getWritableDate } from '../../utils/date.value.js';
|
|
7
|
+
import { getWritableDate, getWritableDueDate } from '../../utils/date.value.js';
|
|
8
|
+
import { getPaymentTermsNote } from '../../utils/payment.terms.js';
|
|
9
|
+
import { computeDocumentTotals } from '../../utils/document.totals.js';
|
|
8
10
|
|
|
9
11
|
/**
|
|
10
12
|
* Encoder for ZUGFeRD invoice format
|
|
@@ -382,7 +384,10 @@ export class ZUGFeRDEncoder extends CIIBaseEncoder {
|
|
|
382
384
|
if (invoice.paymentOptions) {
|
|
383
385
|
// Add payment instructions as description - this is generic enough to work with any payment type
|
|
384
386
|
const descriptionElement = doc.createElement('ram:Description');
|
|
385
|
-
descriptionElement.textContent =
|
|
387
|
+
descriptionElement.textContent = [
|
|
388
|
+
getPaymentTermsNote(invoice.date, invoice.dueInDays, invoice.language, 'cii'),
|
|
389
|
+
invoice.paymentOptions.description,
|
|
390
|
+
].filter(Boolean).join('. ');
|
|
386
391
|
paymentTermsElement.appendChild(descriptionElement);
|
|
387
392
|
}
|
|
388
393
|
|
|
@@ -392,8 +397,7 @@ export class ZUGFeRDEncoder extends CIIBaseEncoder {
|
|
|
392
397
|
dateStringElement.setAttribute('format', '102'); // YYYYMMDD format
|
|
393
398
|
|
|
394
399
|
// Calculate due date
|
|
395
|
-
const dueDate =
|
|
396
|
-
dueDate.setDate(dueDate.getDate() + invoice.dueInDays);
|
|
400
|
+
const dueDate = getWritableDueDate(invoice.date, invoice.dueInDays, 'cii');
|
|
397
401
|
|
|
398
402
|
dateStringElement.textContent = this.formatDateYYYYMMDD(dueDate.getTime(), 'BT-9 payment due date');
|
|
399
403
|
dueDateElement.appendChild(dateStringElement);
|
|
@@ -453,30 +457,16 @@ export class ZUGFeRDEncoder extends CIIBaseEncoder {
|
|
|
453
457
|
* @param invoice Invoice data
|
|
454
458
|
*/
|
|
455
459
|
private addTaxDetails(doc: Document, settlementElement: Element, invoice: TAccountingDoc): void {
|
|
456
|
-
//
|
|
457
|
-
const
|
|
460
|
+
// the EN 16931 totals in decimal arithmetic, rounded as the business rules check them
|
|
461
|
+
const totals = computeDocumentTotals(invoice);
|
|
458
462
|
|
|
459
|
-
//
|
|
460
|
-
|
|
461
|
-
for (const item of invoice.items) {
|
|
462
|
-
const itemNetAmount = item.unitNetPrice * item.unitQuantity;
|
|
463
|
-
const vatRate = item.vatPercentage;
|
|
464
|
-
|
|
465
|
-
const currentAmount = taxCategories.get(vatRate) || 0;
|
|
466
|
-
taxCategories.set(vatRate, currentAmount + itemNetAmount);
|
|
467
|
-
}
|
|
468
|
-
}
|
|
469
|
-
|
|
470
|
-
// Add each tax category
|
|
471
|
-
for (const [rate, baseAmount] of taxCategories.entries()) {
|
|
463
|
+
// VAT breakdown (BG-23), one per rate
|
|
464
|
+
for (const { rate, taxableAmount, taxAmount } of totals.vatGroups) {
|
|
472
465
|
const taxElement = doc.createElement('ram:ApplicableTradeTax');
|
|
473
|
-
|
|
474
|
-
//
|
|
475
|
-
const taxAmount = baseAmount * (rate / 100);
|
|
476
|
-
|
|
477
|
-
// Add calculated amount
|
|
466
|
+
|
|
467
|
+
// VAT category tax amount (BT-117)
|
|
478
468
|
const calculatedAmountElement = doc.createElement('ram:CalculatedAmount');
|
|
479
|
-
calculatedAmountElement.textContent = taxAmount.toFixed(
|
|
469
|
+
calculatedAmountElement.textContent = taxAmount.toFixed(totals.minorUnits);
|
|
480
470
|
taxElement.appendChild(calculatedAmountElement);
|
|
481
471
|
|
|
482
472
|
// Add type code (VAT)
|
|
@@ -484,9 +474,9 @@ export class ZUGFeRDEncoder extends CIIBaseEncoder {
|
|
|
484
474
|
typeCodeElement.textContent = 'VAT';
|
|
485
475
|
taxElement.appendChild(typeCodeElement);
|
|
486
476
|
|
|
487
|
-
//
|
|
477
|
+
// VAT category taxable amount (BT-116)
|
|
488
478
|
const basisAmountElement = doc.createElement('ram:BasisAmount');
|
|
489
|
-
basisAmountElement.textContent =
|
|
479
|
+
basisAmountElement.textContent = taxableAmount.toFixed(totals.minorUnits);
|
|
490
480
|
taxElement.appendChild(basisAmountElement);
|
|
491
481
|
|
|
492
482
|
// Add category code
|
|
@@ -512,43 +502,23 @@ export class ZUGFeRDEncoder extends CIIBaseEncoder {
|
|
|
512
502
|
private addMonetarySummation(doc: Document, settlementElement: Element, invoice: TAccountingDoc): void {
|
|
513
503
|
const monetarySummationElement = doc.createElement('ram:SpecifiedTradeSettlementHeaderMonetarySummation');
|
|
514
504
|
|
|
515
|
-
//
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
const itemNetAmount = item.unitNetPrice * item.unitQuantity;
|
|
523
|
-
const itemTaxAmount = itemNetAmount * (item.vatPercentage / 100);
|
|
524
|
-
|
|
525
|
-
totalNetAmount += itemNetAmount;
|
|
526
|
-
totalTaxAmount += itemTaxAmount;
|
|
505
|
+
// the EN 16931 totals in decimal arithmetic, rounded as the business rules check them
|
|
506
|
+
const totals = computeDocumentTotals(invoice);
|
|
507
|
+
const appendAmount = (name: string, value: { toFixed(decimalPlaces: number): string }, withCurrency = false) => {
|
|
508
|
+
const element = doc.createElement(name);
|
|
509
|
+
element.textContent = value.toFixed(totals.minorUnits);
|
|
510
|
+
if (withCurrency) {
|
|
511
|
+
element.setAttribute('currencyID', invoice.currency);
|
|
527
512
|
}
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
const totalGrossAmount = totalNetAmount + totalTaxAmount;
|
|
513
|
+
monetarySummationElement.appendChild(element);
|
|
514
|
+
};
|
|
531
515
|
|
|
532
|
-
//
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
const taxTotalElement = doc.createElement('ram:TaxTotalAmount');
|
|
539
|
-
taxTotalElement.textContent = totalTaxAmount.toFixed(2);
|
|
540
|
-
taxTotalElement.setAttribute('currencyID', invoice.currency);
|
|
541
|
-
monetarySummationElement.appendChild(taxTotalElement);
|
|
542
|
-
|
|
543
|
-
// Add grand total amount
|
|
544
|
-
const grandTotalElement = doc.createElement('ram:GrandTotalAmount');
|
|
545
|
-
grandTotalElement.textContent = totalGrossAmount.toFixed(2);
|
|
546
|
-
monetarySummationElement.appendChild(grandTotalElement);
|
|
547
|
-
|
|
548
|
-
// Add due payable amount
|
|
549
|
-
const duePayableElement = doc.createElement('ram:DuePayableAmount');
|
|
550
|
-
duePayableElement.textContent = totalGrossAmount.toFixed(2);
|
|
551
|
-
monetarySummationElement.appendChild(duePayableElement);
|
|
516
|
+
// in the order of the CII schema
|
|
517
|
+
appendAmount('ram:LineTotalAmount', totals.lineTotal); // BT-106
|
|
518
|
+
appendAmount('ram:TaxBasisTotalAmount', totals.taxBasisTotal); // BT-109
|
|
519
|
+
appendAmount('ram:TaxTotalAmount', totals.taxTotal, true); // BT-110
|
|
520
|
+
appendAmount('ram:GrandTotalAmount', totals.grandTotal); // BT-112
|
|
521
|
+
appendAmount('ram:DuePayableAmount', totals.duePayable); // BT-115
|
|
552
522
|
|
|
553
523
|
settlementElement.appendChild(monetarySummationElement);
|
|
554
524
|
}
|
|
@@ -562,7 +532,8 @@ export class ZUGFeRDEncoder extends CIIBaseEncoder {
|
|
|
562
532
|
private addLineItems(doc: Document, transactionElement: Element, invoice: TAccountingDoc): void {
|
|
563
533
|
// Add each line item
|
|
564
534
|
if (invoice.items) {
|
|
565
|
-
|
|
535
|
+
const totals = computeDocumentTotals(invoice);
|
|
536
|
+
for (const [index, item] of invoice.items.entries()) {
|
|
566
537
|
// Create line item element
|
|
567
538
|
const lineItemElement = doc.createElement('ram:IncludedSupplyChainTradeLineItem');
|
|
568
539
|
|
|
@@ -633,15 +604,12 @@ export class ZUGFeRDEncoder extends CIIBaseEncoder {
|
|
|
633
604
|
|
|
634
605
|
settlementElement.appendChild(taxElement);
|
|
635
606
|
|
|
636
|
-
//
|
|
637
|
-
const monetarySummationElement = doc.createElement('ram:
|
|
638
|
-
|
|
639
|
-
// Calculate item total
|
|
640
|
-
const itemNetAmount = item.unitNetPrice * item.unitQuantity;
|
|
607
|
+
// Line monetary summation, as the CII schema names it
|
|
608
|
+
const monetarySummationElement = doc.createElement('ram:SpecifiedTradeSettlementLineMonetarySummation');
|
|
641
609
|
|
|
642
|
-
//
|
|
610
|
+
// Invoice line net amount (BT-131): quantity × net price, rounded
|
|
643
611
|
const lineTotalElement = doc.createElement('ram:LineTotalAmount');
|
|
644
|
-
lineTotalElement.textContent =
|
|
612
|
+
lineTotalElement.textContent = totals.lineNetAmounts[index].toFixed(totals.minorUnits);
|
|
645
613
|
monetarySummationElement.appendChild(lineTotalElement);
|
|
646
614
|
|
|
647
615
|
settlementElement.appendChild(monetarySummationElement);
|
|
@@ -661,9 +629,10 @@ export class ZUGFeRDEncoder extends CIIBaseEncoder {
|
|
|
661
629
|
*/
|
|
662
630
|
private formatDateYYYYMMDD(timestamp: number, field: string): string {
|
|
663
631
|
const date = getWritableDate(timestamp, field, 'cii');
|
|
664
|
-
|
|
665
|
-
const
|
|
666
|
-
const
|
|
632
|
+
// the UTC fields: a calendar day is its UTC midnight, whatever the server's zone
|
|
633
|
+
const year = date.getUTCFullYear();
|
|
634
|
+
const month = (date.getUTCMonth() + 1).toString().padStart(2, '0');
|
|
635
|
+
const day = date.getUTCDate().toString().padStart(2, '0');
|
|
667
636
|
return `${year}${month}${day}`;
|
|
668
637
|
}
|
|
669
638
|
}
|
|
@@ -35,6 +35,31 @@ export class ZUGFeRDV1Decoder extends CIIBaseDecoder {
|
|
|
35
35
|
};
|
|
36
36
|
}
|
|
37
37
|
|
|
38
|
+
/**
|
|
39
|
+
* Reads the issue date of the v1 header (`rsm:HeaderExchangedDocument`); a
|
|
40
|
+
* missing or invalid one is refused with an `EInvoiceParsingError`.
|
|
41
|
+
*/
|
|
42
|
+
protected override extractIssueDate(): number {
|
|
43
|
+
const issueDatePath = '/rsm:CrossIndustryDocument/rsm:HeaderExchangedDocument/ram:IssueDateTime/udt:DateTimeString';
|
|
44
|
+
return this.parseRequiredCIIDate(this.v1Text(issueDatePath), this.v1Text(`${issueDatePath}/@format`));
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Reads the payment due date of the v1 header payment terms; undefined when
|
|
49
|
+
* the document states none.
|
|
50
|
+
*/
|
|
51
|
+
protected override extractDueDate(): number | undefined {
|
|
52
|
+
const dueDatePath =
|
|
53
|
+
'/rsm:CrossIndustryDocument/rsm:SpecifiedSupplyChainTradeTransaction/ram:ApplicableSupplyChainTradeSettlement/ram:SpecifiedTradePaymentTerms/ram:DueDateDateTime/udt:DateTimeString';
|
|
54
|
+
const dueDate = this.v1Text(dueDatePath);
|
|
55
|
+
return dueDate ? this.parseRequiredCIIDate(dueDate, this.v1Text(`${dueDatePath}/@format`)) : undefined;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/** The trimmed text of the first node a v1 path selects, or '' */
|
|
59
|
+
private v1Text(path: string): string {
|
|
60
|
+
return String(zugferdV1Select(`string(${path})`, this.doc)).trim();
|
|
61
|
+
}
|
|
62
|
+
|
|
38
63
|
/**
|
|
39
64
|
* Reads the actual delivery date of the v1 header delivery
|
|
40
65
|
* (`ram:ApplicableSupplyChainTradeDelivery`); undefined when the document
|
|
@@ -82,9 +107,7 @@ export class ZUGFeRDV1Decoder extends CIIBaseDecoder {
|
|
|
82
107
|
const invoiceId = this.getText('//ram:ID');
|
|
83
108
|
|
|
84
109
|
// Extract issue date
|
|
85
|
-
const
|
|
86
|
-
const issueDateFormat = this.getText('//ram:IssueDateTime/udt:DateTimeString/@format');
|
|
87
|
-
const issueDate = this.parseCIIDate(issueDateStr, issueDateFormat);
|
|
110
|
+
const issueDate = this.extractIssueDate();
|
|
88
111
|
|
|
89
112
|
// Extract seller information
|
|
90
113
|
const seller = this.extractParty('//ram:SellerTradeParty');
|
|
@@ -96,9 +119,8 @@ export class ZUGFeRDV1Decoder extends CIIBaseDecoder {
|
|
|
96
119
|
const items = this.extractItems();
|
|
97
120
|
|
|
98
121
|
// Extract due date
|
|
99
|
-
|
|
100
|
-
const
|
|
101
|
-
const dueInDays = Math.round((dueDate - issueDate) / (1000 * 60 * 60 * 24));
|
|
122
|
+
// no due date stated: `dueInDays` cannot express it yet (required field in @tsclass/tsclass); value unchanged: 0 days, what this decoder returned before it read the v1 dates
|
|
123
|
+
const dueInDays = this.dueInDaysOf(issueDate, this.extractDueDate(), 0);
|
|
102
124
|
|
|
103
125
|
// Extract currency
|
|
104
126
|
const currencyCode = this.getText('//ram:InvoiceCurrencyCode') || 'EUR';
|
|
@@ -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);
|