@fin.cx/einvoice 8.2.0 → 8.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist_ts/00_commitinfo_data.js +1 -1
- package/dist_ts/formats/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 +7 -7
- package/dist_ts/formats/cii/zugferd/zugferd.decoder.js +4 -7
- package/dist_ts/formats/cii/zugferd/zugferd.encoder.js +12 -8
- 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/ubl/generic/ubl.encoder.js +6 -7
- 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/date.value.d.ts +18 -0
- package/dist_ts/formats/utils/date.value.js +30 -1
- 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/integrated.validator.js +6 -3
- 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/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 +6 -6
- package/ts/formats/cii/zugferd/zugferd.decoder.ts +3 -6
- package/ts/formats/cii/zugferd/zugferd.encoder.ts +11 -7
- package/ts/formats/cii/zugferd/zugferd.v1.decoder.ts +28 -6
- package/ts/formats/ubl/generic/ubl.encoder.ts +5 -6
- 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/date.value.ts +31 -0
- package/ts/formats/utils/format.detector.ts +5 -5
- package/ts/formats/utils/payment.terms.ts +37 -0
- package/ts/formats/validation/integrated.validator.ts +5 -2
- package/ts/formats/validation/xrechnung.validator.ts +5 -3
|
@@ -59,14 +59,15 @@ export abstract class BaseDecoder {
|
|
|
59
59
|
const year = parseInt(dateStr.substring(0, 4));
|
|
60
60
|
const month = parseInt(dateStr.substring(4, 6)) - 1; // Month is 0-indexed in JS
|
|
61
61
|
const day = parseInt(dateStr.substring(6, 8));
|
|
62
|
-
|
|
62
|
+
// a calendar day is its UTC midnight, whatever the server's zone
|
|
63
|
+
return Date.UTC(year, month, day);
|
|
63
64
|
}
|
|
64
65
|
|
|
65
66
|
// Format 610 is YYYYMM
|
|
66
67
|
if (format === '610' && dateStr.length === 6) {
|
|
67
68
|
const year = parseInt(dateStr.substring(0, 4));
|
|
68
69
|
const month = parseInt(dateStr.substring(4, 6)) - 1;
|
|
69
|
-
return
|
|
70
|
+
return Date.UTC(year, month, 1);
|
|
70
71
|
}
|
|
71
72
|
|
|
72
73
|
// Try to parse as ISO date or other standard formats
|
|
@@ -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';
|
|
@@ -6,7 +6,7 @@ import { Decimal } from '../../utils/decimal.js';
|
|
|
6
6
|
import { DecimalCurrencyCalculator } from '../../utils/currency.calculator.decimal.js';
|
|
7
7
|
import { getCurrencyMinorUnits } from '../../utils/currency.utils.js';
|
|
8
8
|
import { getDocumentTypeCode } from '../../utils/document.typecode.js';
|
|
9
|
-
import { getWritableDate } from '../../utils/date.value.js';
|
|
9
|
+
import { getWritableDate, getWritableDueDate } from '../../utils/date.value.js';
|
|
10
10
|
|
|
11
11
|
/**
|
|
12
12
|
* Encoder for Factur-X invoice format
|
|
@@ -325,8 +325,7 @@ export class FacturXEncoder extends CIIBaseEncoder {
|
|
|
325
325
|
dateStringElement.setAttribute('format', '102'); // YYYYMMDD format
|
|
326
326
|
|
|
327
327
|
// Calculate due date
|
|
328
|
-
const dueDate =
|
|
329
|
-
dueDate.setDate(dueDate.getDate() + invoice.dueInDays);
|
|
328
|
+
const dueDate = getWritableDueDate(invoice.date, invoice.dueInDays, 'cii');
|
|
330
329
|
|
|
331
330
|
dateStringElement.textContent = this.formatDateYYYYMMDD(dueDate.getTime(), 'BT-9 payment due date');
|
|
332
331
|
dueDateElement.appendChild(dateStringElement);
|
|
@@ -493,9 +492,10 @@ export class FacturXEncoder extends CIIBaseEncoder {
|
|
|
493
492
|
*/
|
|
494
493
|
private formatDateYYYYMMDD(timestamp: number, field: string): string {
|
|
495
494
|
const date = getWritableDate(timestamp, field, 'cii');
|
|
496
|
-
|
|
497
|
-
const
|
|
498
|
-
const
|
|
495
|
+
// the UTC fields: a calendar day is its UTC midnight, whatever the server's zone
|
|
496
|
+
const year = date.getUTCFullYear();
|
|
497
|
+
const month = (date.getUTCMonth() + 1).toString().padStart(2, '0');
|
|
498
|
+
const day = date.getUTCDate().toString().padStart(2, '0');
|
|
499
499
|
return `${year}${month}${day}`;
|
|
500
500
|
}
|
|
501
501
|
}
|
|
@@ -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,8 @@ 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';
|
|
8
9
|
|
|
9
10
|
/**
|
|
10
11
|
* Encoder for ZUGFeRD invoice format
|
|
@@ -382,7 +383,10 @@ export class ZUGFeRDEncoder extends CIIBaseEncoder {
|
|
|
382
383
|
if (invoice.paymentOptions) {
|
|
383
384
|
// Add payment instructions as description - this is generic enough to work with any payment type
|
|
384
385
|
const descriptionElement = doc.createElement('ram:Description');
|
|
385
|
-
descriptionElement.textContent =
|
|
386
|
+
descriptionElement.textContent = [
|
|
387
|
+
getPaymentTermsNote(invoice.date, invoice.dueInDays, invoice.language, 'cii'),
|
|
388
|
+
invoice.paymentOptions.description,
|
|
389
|
+
].filter(Boolean).join('. ');
|
|
386
390
|
paymentTermsElement.appendChild(descriptionElement);
|
|
387
391
|
}
|
|
388
392
|
|
|
@@ -392,8 +396,7 @@ export class ZUGFeRDEncoder extends CIIBaseEncoder {
|
|
|
392
396
|
dateStringElement.setAttribute('format', '102'); // YYYYMMDD format
|
|
393
397
|
|
|
394
398
|
// Calculate due date
|
|
395
|
-
const dueDate =
|
|
396
|
-
dueDate.setDate(dueDate.getDate() + invoice.dueInDays);
|
|
399
|
+
const dueDate = getWritableDueDate(invoice.date, invoice.dueInDays, 'cii');
|
|
397
400
|
|
|
398
401
|
dateStringElement.textContent = this.formatDateYYYYMMDD(dueDate.getTime(), 'BT-9 payment due date');
|
|
399
402
|
dueDateElement.appendChild(dateStringElement);
|
|
@@ -661,9 +664,10 @@ export class ZUGFeRDEncoder extends CIIBaseEncoder {
|
|
|
661
664
|
*/
|
|
662
665
|
private formatDateYYYYMMDD(timestamp: number, field: string): string {
|
|
663
666
|
const date = getWritableDate(timestamp, field, 'cii');
|
|
664
|
-
|
|
665
|
-
const
|
|
666
|
-
const
|
|
667
|
+
// the UTC fields: a calendar day is its UTC midnight, whatever the server's zone
|
|
668
|
+
const year = date.getUTCFullYear();
|
|
669
|
+
const month = (date.getUTCMonth() + 1).toString().padStart(2, '0');
|
|
670
|
+
const day = date.getUTCDate().toString().padStart(2, '0');
|
|
667
671
|
return `${year}${month}${day}`;
|
|
668
672
|
}
|
|
669
673
|
}
|
|
@@ -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';
|
|
@@ -4,7 +4,8 @@ import { UBLDocumentType } from '../ubl.types.js';
|
|
|
4
4
|
import { DOMParser, XMLSerializer } from '../../../plugins.js';
|
|
5
5
|
import { getDocumentTypeCode } from '../../utils/document.typecode.js';
|
|
6
6
|
import { getPrecedingInvoiceReferences } from '../../utils/preceding.invoice.js';
|
|
7
|
-
import {
|
|
7
|
+
import { getWritableDueDate } from '../../utils/date.value.js';
|
|
8
|
+
import { getPaymentTermsNote } from '../../utils/payment.terms.js';
|
|
8
9
|
|
|
9
10
|
/**
|
|
10
11
|
* UBL Encoder implementation
|
|
@@ -81,8 +82,7 @@ export class UBLEncoder extends UBLBaseEncoder {
|
|
|
81
82
|
|
|
82
83
|
// Due Date - the CreditNote schema has no DueDate element; its payment due date lives in cac:PaymentMeans
|
|
83
84
|
if (documentType === UBLDocumentType.INVOICE) {
|
|
84
|
-
const dueDate =
|
|
85
|
-
dueDate.setDate(dueDate.getDate() + (invoice.dueInDays || 30));
|
|
85
|
+
const dueDate = getWritableDueDate(invoice.date, invoice.dueInDays, 'ubl');
|
|
86
86
|
this.appendElement(doc, root, 'cbc:DueDate', this.formatDate(dueDate.getTime(), 'BT-9 payment due date'));
|
|
87
87
|
}
|
|
88
88
|
// Document Type Code (380 invoice, 381 credit note, 383 debit note, 384 corrected invoice, 389 self-billed invoice; the element differs per schema)
|
|
@@ -319,7 +319,7 @@ export class UBLEncoder extends UBLBaseEncoder {
|
|
|
319
319
|
parentElement.appendChild(paymentTermsNode);
|
|
320
320
|
|
|
321
321
|
// Payment terms note
|
|
322
|
-
this.appendElement(doc, paymentTermsNode, 'cbc:Note',
|
|
322
|
+
this.appendElement(doc, paymentTermsNode, 'cbc:Note', getPaymentTermsNote(invoice.date, invoice.dueInDays, invoice.language, 'ubl'));
|
|
323
323
|
|
|
324
324
|
// Add payment means if available
|
|
325
325
|
if (invoice.paymentOptions) {
|
|
@@ -346,8 +346,7 @@ export class UBLEncoder extends UBLBaseEncoder {
|
|
|
346
346
|
this.appendElement(doc, paymentMeansNode, 'cbc:PaymentMeansCode', '30');
|
|
347
347
|
|
|
348
348
|
// Payment due date, counted from the issue date the document states
|
|
349
|
-
const dueDate =
|
|
350
|
-
dueDate.setDate(dueDate.getDate() + (invoice.dueInDays || 30));
|
|
349
|
+
const dueDate = getWritableDueDate(invoice.date, invoice.dueInDays, 'ubl');
|
|
351
350
|
this.appendElement(doc, paymentMeansNode, 'cbc:PaymentDueDate', this.formatDate(dueDate.getTime(), 'BT-9 payment due date'));
|
|
352
351
|
|
|
353
352
|
// Add payment channel code if available
|
|
@@ -17,8 +17,14 @@ export enum UBLDocumentType {
|
|
|
17
17
|
CREDIT_NOTE = 'CreditNote'
|
|
18
18
|
}
|
|
19
19
|
|
|
20
|
-
// UBL customization IDs for different formats
|
|
20
|
+
// UBL customization IDs (specification identifier, BT-24) for different formats
|
|
21
21
|
export const UBL_CUSTOMIZATION_IDS = {
|
|
22
|
-
|
|
22
|
+
/**
|
|
23
|
+
* XRechnung 3.0 (CIUS), as the KoSIT XRechnung Schematron defines it (`XR-CIUS-ID` in
|
|
24
|
+
* common.sch: 'urn:cen.eu:en16931:2017#compliant#urn:xeinkauf.de:kosit:xrechnung_' and
|
|
25
|
+
* the version '3.0'). XRechnung 1.x and 2.x used the prefix
|
|
26
|
+
* 'urn:cen.eu:en16931:2017#compliant#urn:xoev-de:kosit:standard:xrechnung_'.
|
|
27
|
+
*/
|
|
28
|
+
XRECHNUNG: 'urn:cen.eu:en16931:2017#compliant#urn:xeinkauf.de:kosit:xrechnung_3.0',
|
|
23
29
|
PEPPOL_BIS: 'urn:cen.eu:en16931:2017#compliant#urn:fdc:peppol.eu:2017:poacc:billing:3.0'
|
|
24
30
|
};
|
|
@@ -60,14 +60,18 @@ export class XRechnungDecoder extends UBLBaseDecoder {
|
|
|
60
60
|
const issueDate = this.parseRequiredUblDate(issueDateText);
|
|
61
61
|
const currencyCode = this.getText('//cbc:DocumentCurrencyCode', this.doc) || 'EUR';
|
|
62
62
|
|
|
63
|
-
//
|
|
64
|
-
|
|
65
|
-
|
|
63
|
+
// The payment due date (BT-9): cbc:DueDate on an Invoice, cac:PaymentMeans/cbc:PaymentDueDate
|
|
64
|
+
// on a CreditNote, cac:PaymentTerms/cbc:PaymentDueDate as a last resort; the payment term in
|
|
65
|
+
// days is the calendar days from the issue date to it.
|
|
66
|
+
// no due date stated: `dueInDays` cannot express it yet (required field in @tsclass/tsclass); value unchanged: 30 days
|
|
67
|
+
let dueInDays = 30;
|
|
68
|
+
const dueDateText = (
|
|
69
|
+
this.getText('/*/cbc:DueDate', this.doc) ||
|
|
70
|
+
this.getText('/*/cac:PaymentMeans/cbc:PaymentDueDate', this.doc) ||
|
|
71
|
+
this.getText('/*/cac:PaymentTerms/cbc:PaymentDueDate', this.doc)
|
|
72
|
+
).trim();
|
|
66
73
|
if (dueDateText) {
|
|
67
|
-
|
|
68
|
-
const issueDateObj = new Date(issueDate);
|
|
69
|
-
const diffTime = Math.abs(dueDateObj.getTime() - issueDateObj.getTime());
|
|
70
|
-
dueInDays = Math.ceil(diffTime / (1000 * 60 * 60 * 24));
|
|
74
|
+
dueInDays = Math.round((this.parseRequiredUblDate(dueDateText) - issueDate) / (1000 * 60 * 60 * 24));
|
|
71
75
|
}
|
|
72
76
|
|
|
73
77
|
// Extract items
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
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
|
+
import { UBL_CUSTOMIZATION_IDS } from '../ubl.types.js';
|
|
4
5
|
|
|
5
6
|
/**
|
|
6
7
|
* Encoder for XRechnung (UBL) format
|
|
@@ -52,10 +53,10 @@ export class XRechnungEncoder extends UBLEncoder {
|
|
|
52
53
|
// Extract metadata if available
|
|
53
54
|
const metadata = (invoice as any).metadata?.extensions;
|
|
54
55
|
|
|
55
|
-
//
|
|
56
|
+
// Specification identifier (BT-24): XRechnung 3.0
|
|
56
57
|
const customizationId = root.getElementsByTagName('cbc:CustomizationID')[0];
|
|
57
58
|
if (customizationId) {
|
|
58
|
-
customizationId.textContent =
|
|
59
|
+
customizationId.textContent = UBL_CUSTOMIZATION_IDS.XRECHNUNG;
|
|
59
60
|
}
|
|
60
61
|
|
|
61
62
|
// Add or update Buyer Reference (required for XRechnung)
|
|
@@ -73,15 +74,6 @@ export class XRechnungEncoder extends UBLEncoder {
|
|
|
73
74
|
buyerRef.textContent = buyerReferenceValue;
|
|
74
75
|
}
|
|
75
76
|
|
|
76
|
-
// Update payment terms to German
|
|
77
|
-
const paymentTermsNotes = root.getElementsByTagName('cac:PaymentTerms');
|
|
78
|
-
if (paymentTermsNotes.length > 0) {
|
|
79
|
-
const noteElement = paymentTermsNotes[0].getElementsByTagName('cbc:Note')[0];
|
|
80
|
-
if (noteElement && noteElement.textContent) {
|
|
81
|
-
noteElement.textContent = `Zahlung innerhalb von ${invoice.dueInDays || 30} Tagen`;
|
|
82
|
-
}
|
|
83
|
-
}
|
|
84
|
-
|
|
85
77
|
// Add electronic address for parties if available
|
|
86
78
|
this.addElectronicAddressToParty(doc, 'cac:AccountingSupplierParty', invoice.from);
|
|
87
79
|
this.addElectronicAddressToParty(doc, 'cac:AccountingCustomerParty', invoice.to);
|
|
@@ -1,5 +1,14 @@
|
|
|
1
1
|
import { EInvoiceFormatError } from '../../errors.js';
|
|
2
2
|
|
|
3
|
+
/**
|
|
4
|
+
* Calendar dates (BT-2, BT-7, BT-9, BT-26, BT-72, BT-73, BT-74) carry no time
|
|
5
|
+
* of day and no time zone: UBL writes them as `xsd:date`, CII in format 102
|
|
6
|
+
* (`YYYYMMDD`). In the accounting document envelope such a day is the UTC
|
|
7
|
+
* midnight of that day, which is what the decoders return. The encoders write
|
|
8
|
+
* it with the UTC fields of the date and count days in UTC, so the day
|
|
9
|
+
* written does not depend on the time zone of the machine that writes it.
|
|
10
|
+
*/
|
|
11
|
+
|
|
3
12
|
/**
|
|
4
13
|
* The date an encoder is about to write, as a `Date`. A value that is no
|
|
5
14
|
* finite timestamp, or lies outside the range of a JavaScript date, is refused
|
|
@@ -19,3 +28,25 @@ export const getWritableDate = (timestamp: unknown, field: string, targetFormat:
|
|
|
19
28
|
}
|
|
20
29
|
return date;
|
|
21
30
|
};
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* The payment due date (BT-9) a document states: its issue date plus
|
|
34
|
+
* `dueInDays` calendar days. `0` is due on the issue date. A `dueInDays` that
|
|
35
|
+
* is no whole number of days is refused with an `EInvoiceFormatError`; no other
|
|
36
|
+
* term is put in its place.
|
|
37
|
+
* @param issueTimestamp The issue date (BT-2)
|
|
38
|
+
* @param dueInDays The payment term in days, as the document states it
|
|
39
|
+
* @param targetFormat The format being written
|
|
40
|
+
*/
|
|
41
|
+
export const getWritableDueDate = (issueTimestamp: unknown, dueInDays: unknown, targetFormat: string): Date => {
|
|
42
|
+
const dueDate = getWritableDate(issueTimestamp, 'BT-2 invoice issue date', targetFormat);
|
|
43
|
+
if (typeof dueInDays !== 'number' || !Number.isInteger(dueInDays)) {
|
|
44
|
+
throw new EInvoiceFormatError(`BT-9 payment due date: dueInDays is no whole number of days: ${String(dueInDays)}`, {
|
|
45
|
+
targetFormat,
|
|
46
|
+
unsupportedFeatures: ['BT-9 payment due date'],
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
// calendar days counted in UTC: a calendar day is its UTC midnight, whatever the server's zone
|
|
50
|
+
dueDate.setUTCDate(dueDate.getUTCDate() + dueInDays);
|
|
51
|
+
return dueDate;
|
|
52
|
+
};
|
|
@@ -80,7 +80,9 @@ export class FormatDetector {
|
|
|
80
80
|
);
|
|
81
81
|
const customizationId = customizationIdMatch?.[1] ?? '';
|
|
82
82
|
|
|
83
|
-
|
|
83
|
+
// both identifier families: urn:xeinkauf.de:kosit:xrechnung_3.0 (XRechnung 3.0) and
|
|
84
|
+
// urn:xoev-de:kosit:standard:xrechnung_x.y (XRechnung 1.x and 2.x)
|
|
85
|
+
if (/xrechnung/i.test(customizationId)) {
|
|
84
86
|
return InvoiceFormat.XRECHNUNG;
|
|
85
87
|
}
|
|
86
88
|
return InvoiceFormat.UBL;
|
|
@@ -133,10 +135,8 @@ export class FormatDetector {
|
|
|
133
135
|
}
|
|
134
136
|
|
|
135
137
|
// Check for obvious XRechnung indicators
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
/urn:xoev-de:kosit:standard:xrechnung/i.test(sample)
|
|
139
|
-
) {
|
|
138
|
+
// covers both identifier families (urn:xeinkauf.de:kosit:xrechnung_, urn:xoev-de:kosit:standard:xrechnung_)
|
|
139
|
+
if (/xrechnung/i.test(sample)) {
|
|
140
140
|
return InvoiceFormat.XRECHNUNG;
|
|
141
141
|
}
|
|
142
142
|
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { getWritableDueDate } from './date.value.js';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* The payment terms note (BT-20) the encoders write: the due date (BT-9) the
|
|
5
|
+
* document states, in the document language (German for `de`, English
|
|
6
|
+
* otherwise). It names the date rather than a count of days, so it reads
|
|
7
|
+
* correctly for every term:
|
|
8
|
+
* - a term of 0 days: "Zahlbar sofort" / "Payable immediately";
|
|
9
|
+
* - a later due date: "Zahlbar bis 04.10.2026" / "Payable by 2026-10-04";
|
|
10
|
+
* - an earlier due date, which is legitimate when the payment fell due before
|
|
11
|
+
* the invoice was issued, as with an agreed advance payment or an invoice
|
|
12
|
+
* issued after the contractual due date: "Fällig seit dem 03.09.2026" /
|
|
13
|
+
* "Due since 2026-09-03".
|
|
14
|
+
* A term that is no whole number of days is refused by `getWritableDueDate`.
|
|
15
|
+
* @param issueTimestamp The issue date (BT-2)
|
|
16
|
+
* @param dueInDays The payment term in days, as the document states it
|
|
17
|
+
* @param language The document language
|
|
18
|
+
* @param targetFormat The format being written
|
|
19
|
+
*/
|
|
20
|
+
export const getPaymentTermsNote = (
|
|
21
|
+
issueTimestamp: unknown,
|
|
22
|
+
dueInDays: unknown,
|
|
23
|
+
language: string | undefined,
|
|
24
|
+
targetFormat: string,
|
|
25
|
+
): string => {
|
|
26
|
+
const dueDate = getWritableDueDate(issueTimestamp, dueInDays, targetFormat);
|
|
27
|
+
const isoDate = dueDate.toISOString().slice(0, 10);
|
|
28
|
+
const german = (language ?? '').toLowerCase().startsWith('de');
|
|
29
|
+
const germanDate = `${isoDate.slice(8, 10)}.${isoDate.slice(5, 7)}.${isoDate.slice(0, 4)}`;
|
|
30
|
+
if (dueInDays === 0) {
|
|
31
|
+
return german ? 'Zahlbar sofort' : 'Payable immediately';
|
|
32
|
+
}
|
|
33
|
+
if ((dueInDays as number) > 0) {
|
|
34
|
+
return german ? `Zahlbar bis ${germanDate}` : `Payable by ${isoDate}`;
|
|
35
|
+
}
|
|
36
|
+
return german ? `Fällig seit dem ${germanDate}` : `Due since ${isoDate}`;
|
|
37
|
+
};
|
|
@@ -11,6 +11,7 @@ import { EN16931BusinessRulesValidator } from './en16931.business-rules.validato
|
|
|
11
11
|
import { CodeListValidator } from './codelist.validator.js';
|
|
12
12
|
import type { ValidationResult, ValidationOptions, ValidationReport } from './validation.types.js';
|
|
13
13
|
import type { EInvoice } from '../../einvoice.js';
|
|
14
|
+
import { UBL_CUSTOMIZATION_IDS } from '../ubl/ubl.types.js';
|
|
14
15
|
|
|
15
16
|
/**
|
|
16
17
|
* Main validator that combines all validation capabilities
|
|
@@ -196,9 +197,11 @@ export class MainValidator {
|
|
|
196
197
|
const profileId = invoice.metadata?.profileId || '';
|
|
197
198
|
const customizationId = invoice.metadata?.customizationId || '';
|
|
198
199
|
|
|
200
|
+
// XRechnung 3.0 (its extension and CVD identifiers begin with it), the identifier
|
|
201
|
+
// family of XRechnung 1.x and 2.x, and any identifier naming XRechnung
|
|
199
202
|
const xrechnungProfiles = [
|
|
200
|
-
|
|
201
|
-
'urn:
|
|
203
|
+
UBL_CUSTOMIZATION_IDS.XRECHNUNG,
|
|
204
|
+
'urn:xoev-de:kosit:standard:xrechnung_',
|
|
202
205
|
'xrechnung'
|
|
203
206
|
];
|
|
204
207
|
|
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
|
|
9
9
|
import type { EInvoice } from '../../einvoice.js';
|
|
10
10
|
import type { ValidationResult } from './validation.types.js';
|
|
11
|
+
import { UBL_CUSTOMIZATION_IDS } from '../ubl/ubl.types.js';
|
|
11
12
|
|
|
12
13
|
/**
|
|
13
14
|
* XRechnung-specific validator implementing German CIUS rules
|
|
@@ -62,10 +63,11 @@ export class XRechnungValidator {
|
|
|
62
63
|
const customizationId = invoice.metadata?.customizationId || '';
|
|
63
64
|
|
|
64
65
|
// XRechnung profile identifiers
|
|
66
|
+
// XRechnung 3.0 (its extension and CVD identifiers begin with it), the identifier
|
|
67
|
+
// family of XRechnung 1.x and 2.x, and any identifier naming XRechnung
|
|
65
68
|
const xrechnungProfiles = [
|
|
66
|
-
|
|
67
|
-
'urn:
|
|
68
|
-
'urn:cen.eu:en16931:2017:xrechnung',
|
|
69
|
+
UBL_CUSTOMIZATION_IDS.XRECHNUNG,
|
|
70
|
+
'urn:xoev-de:kosit:standard:xrechnung_',
|
|
69
71
|
'xrechnung'
|
|
70
72
|
];
|
|
71
73
|
|