@fin.cx/einvoice 8.3.1 → 9.0.0
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.d.ts +14 -0
- package/dist_ts/einvoice.js +17 -1
- package/dist_ts/formats/cii/cii.encoder.js +5 -1
- package/dist_ts/formats/cii/facturx/facturx.decoder.js +5 -1
- package/dist_ts/formats/cii/facturx/facturx.encoder.js +7 -2
- package/dist_ts/formats/cii/zugferd/zugferd.decoder.js +5 -1
- package/dist_ts/formats/cii/zugferd/zugferd.encoder.js +7 -2
- package/dist_ts/formats/cii/zugferd/zugferd.v1.decoder.js +5 -1
- package/dist_ts/formats/semantic/semantic.adapter.js +10 -3
- package/dist_ts/formats/semantic/semantic.validator.js +17 -2
- package/dist_ts/formats/ubl/generic/ubl.encoder.js +12 -3
- package/dist_ts/formats/ubl/ubl.encoder.js +5 -1
- package/dist_ts/formats/ubl/xrechnung/xrechnung.decoder.js +5 -1
- package/dist_ts/formats/utils/document.totals.d.ts +1 -3
- package/dist_ts/formats/utils/document.totals.js +1 -2
- package/dist_ts/formats/utils/paid.amount.d.ts +44 -0
- package/dist_ts/formats/utils/paid.amount.js +124 -0
- package/dist_ts/formats/utils/preceding.invoice.d.ts +12 -2
- package/dist_ts/formats/utils/preceding.invoice.js +22 -3
- package/dist_ts/formats/validation/en16931.business-rules.validator.js +20 -18
- package/dist_ts/interfaces/common.d.ts +1 -0
- package/dist_ts/interfaces/en16931-metadata.d.ts +0 -1
- package/package.json +2 -2
- package/readme.md +36 -0
- package/ts/00_commitinfo_data.ts +1 -1
- package/ts/einvoice.ts +18 -0
- package/ts/formats/cii/cii.encoder.ts +4 -0
- package/ts/formats/cii/facturx/facturx.decoder.ts +5 -0
- package/ts/formats/cii/facturx/facturx.encoder.ts +6 -1
- package/ts/formats/cii/zugferd/zugferd.decoder.ts +5 -0
- package/ts/formats/cii/zugferd/zugferd.encoder.ts +6 -1
- package/ts/formats/cii/zugferd/zugferd.v1.decoder.ts +10 -0
- package/ts/formats/semantic/semantic.adapter.ts +9 -2
- package/ts/formats/semantic/semantic.validator.ts +17 -1
- package/ts/formats/ubl/generic/ubl.encoder.ts +12 -2
- package/ts/formats/ubl/ubl.encoder.ts +4 -0
- package/ts/formats/ubl/xrechnung/xrechnung.decoder.ts +5 -0
- package/ts/formats/utils/document.totals.ts +1 -4
- package/ts/formats/utils/paid.amount.ts +160 -0
- package/ts/formats/utils/preceding.invoice.ts +23 -2
- package/ts/formats/validation/en16931.business-rules.validator.ts +27 -25
- package/ts/interfaces/common.ts +1 -0
- package/ts/interfaces/en16931-metadata.ts +0 -1
|
@@ -7,6 +7,7 @@ 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
9
|
import { computeDocumentTotals } from '../../utils/document.totals.js';
|
|
10
|
+
import { getPaymentTotals } from '../../utils/paid.amount.js';
|
|
10
11
|
import { toPlainDecimalString } from '../../utils/number.text.js';
|
|
11
12
|
import { resolveCountryCode } from '../../utils/country.code.js';
|
|
12
13
|
|
|
@@ -554,7 +555,11 @@ export class ZUGFeRDEncoder extends CIIBaseEncoder {
|
|
|
554
555
|
appendAmount('ram:TaxBasisTotalAmount', totals.taxBasisTotal); // BT-109
|
|
555
556
|
appendAmount('ram:TaxTotalAmount', totals.taxTotal, true); // BT-110
|
|
556
557
|
appendAmount('ram:GrandTotalAmount', totals.grandTotal); // BT-112
|
|
557
|
-
|
|
558
|
+
const payment = getPaymentTotals(invoice, totals);
|
|
559
|
+
if (payment.paidAmount) {
|
|
560
|
+
appendAmount('ram:TotalPrepaidAmount', payment.paidAmount); // BT-113
|
|
561
|
+
}
|
|
562
|
+
appendAmount('ram:DuePayableAmount', payment.duePayable); // BT-115
|
|
558
563
|
|
|
559
564
|
settlementElement.appendChild(monetarySummationElement);
|
|
560
565
|
}
|
|
@@ -8,6 +8,7 @@ import type {
|
|
|
8
8
|
import { ZUGFERD_V1_NAMESPACES } from '../cii.types.js';
|
|
9
9
|
import { business, finance, xpath } from '../../../plugins.js';
|
|
10
10
|
import { EN16931Validator } from '../../validation/en16931.validator.js';
|
|
11
|
+
import { readPaidAmount } from '../../utils/paid.amount.js';
|
|
11
12
|
|
|
12
13
|
/** XPath selection with the ZUGFeRD v1 namespaces */
|
|
13
14
|
const zugferdV1Select = xpath.useNamespaces({
|
|
@@ -163,6 +164,14 @@ export class ZUGFeRDV1Decoder extends CIIBaseDecoder {
|
|
|
163
164
|
// Reverse charge: every line has the VAT category AE
|
|
164
165
|
const reverseCharge = this.isReverseChargeDocument();
|
|
165
166
|
|
|
167
|
+
// the paid amount (BT-113) of the v1 header settlement, read with the v1 namespaces
|
|
168
|
+
const paidAmount = readPaidAmount(
|
|
169
|
+
this.v1Text(
|
|
170
|
+
'/rsm:CrossIndustryDocument/rsm:SpecifiedSupplyChainTradeTransaction/ram:ApplicableSupplyChainTradeSettlement/ram:SpecifiedTradeSettlementMonetarySummation/ram:TotalPrepaidAmount',
|
|
171
|
+
),
|
|
172
|
+
'zugferd',
|
|
173
|
+
);
|
|
174
|
+
|
|
166
175
|
// Create the common invoice data
|
|
167
176
|
const invoiceData = {
|
|
168
177
|
type: 'accounting-doc' as const,
|
|
@@ -189,6 +198,7 @@ export class ZUGFeRDV1Decoder extends CIIBaseDecoder {
|
|
|
189
198
|
notes: notes,
|
|
190
199
|
// the day of supply as the document states it (BT-72), never the issue date in its place
|
|
191
200
|
...(deliveryDate === undefined ? {} : { deliveryDate }),
|
|
201
|
+
...(paidAmount === undefined ? {} : { paidAmount }),
|
|
192
202
|
objectActions: []
|
|
193
203
|
};
|
|
194
204
|
|
|
@@ -5,6 +5,8 @@
|
|
|
5
5
|
|
|
6
6
|
import { EInvoice } from '../../einvoice.js';
|
|
7
7
|
import { getAccountingDocType, getDocumentTypeCode } from '../utils/document.typecode.js';
|
|
8
|
+
import { computeDocumentTotals } from '../utils/document.totals.js';
|
|
9
|
+
import { getPaymentTotals } from '../utils/paid.amount.js';
|
|
8
10
|
import type {
|
|
9
11
|
EN16931SemanticModel,
|
|
10
12
|
Seller,
|
|
@@ -132,6 +134,10 @@ export class SemanticModelAdapter {
|
|
|
132
134
|
invoice.from = this.reverseMapSeller(model.seller);
|
|
133
135
|
invoice.to = this.reverseMapBuyer(model.buyer);
|
|
134
136
|
invoice.items = this.reverseMapInvoiceLines(model.invoiceLines);
|
|
137
|
+
// the paid amount (BT-113); a stated zero is nothing paid
|
|
138
|
+
if (model.documentTotals?.prepaidAmount) {
|
|
139
|
+
invoice.paidAmount = model.documentTotals.prepaidAmount;
|
|
140
|
+
}
|
|
135
141
|
|
|
136
142
|
// Set metadata
|
|
137
143
|
if (model.processControl) {
|
|
@@ -405,9 +411,10 @@ export class SemanticModelAdapter {
|
|
|
405
411
|
chargeTotalAmount: invoice.metadata?.extensions?.documentCharges?.reduce(
|
|
406
412
|
(sum: number, c: { amount: number }) => sum + c.amount, 0
|
|
407
413
|
),
|
|
408
|
-
|
|
414
|
+
// the paid amount (BT-113) of the envelope; the amount due (BT-115) is the gross total less it
|
|
415
|
+
prepaidAmount: invoice.paidAmount,
|
|
409
416
|
roundingAmount: invoice.metadata?.extensions?.roundingAmount,
|
|
410
|
-
payableAmount: invoice.
|
|
417
|
+
payableAmount: getPaymentTotals(invoice, computeDocumentTotals(invoice)).duePayable.toNumber()
|
|
411
418
|
};
|
|
412
419
|
}
|
|
413
420
|
|
|
@@ -3,7 +3,8 @@
|
|
|
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
|
+
import { computeDocumentTotals, findInvalidItemAmounts, getTotalsSkippedResult } from '../utils/document.totals.js';
|
|
7
|
+
import { findPaymentProblem } from '../utils/paid.amount.js';
|
|
7
8
|
import type { ValidationResult } from '../validation/validation.types.js';
|
|
8
9
|
import type { EN16931SemanticModel, BusinessTerms, BusinessGroups } from './bt-bg.model.js';
|
|
9
10
|
import type { EInvoice } from '../../einvoice.js';
|
|
@@ -57,6 +58,21 @@ export class SemanticModelValidator {
|
|
|
57
58
|
return results;
|
|
58
59
|
}
|
|
59
60
|
|
|
61
|
+
// The amount due (BT-115) follows from the paid amount; a paid amount that cannot be written
|
|
62
|
+
// (more decimals than the currency has, or advance payments that do not add up to it) is
|
|
63
|
+
// reported and the model rules are skipped, as for a line amount that is no number
|
|
64
|
+
const paymentProblem = findPaymentProblem(invoice, computeDocumentTotals(invoice));
|
|
65
|
+
if (paymentProblem) {
|
|
66
|
+
results.push({
|
|
67
|
+
ruleId: paymentProblem.ruleId,
|
|
68
|
+
source: 'SEMANTIC',
|
|
69
|
+
severity: 'error',
|
|
70
|
+
message: paymentProblem.message,
|
|
71
|
+
field: paymentProblem.field,
|
|
72
|
+
});
|
|
73
|
+
return results;
|
|
74
|
+
}
|
|
75
|
+
|
|
60
76
|
// Convert to semantic model
|
|
61
77
|
const model = this.adapter.toSemanticModel(invoice);
|
|
62
78
|
|
|
@@ -7,6 +7,7 @@ import { getPrecedingInvoiceReferences } from '../../utils/preceding.invoice.js'
|
|
|
7
7
|
import { getWritableDueDate } from '../../utils/date.value.js';
|
|
8
8
|
import { getPaymentTermsNote } from '../../utils/payment.terms.js';
|
|
9
9
|
import { computeDocumentTotals } from '../../utils/document.totals.js';
|
|
10
|
+
import { getPaymentTotals } from '../../utils/paid.amount.js';
|
|
10
11
|
import { toPlainDecimalString } from '../../utils/number.text.js';
|
|
11
12
|
import { resolveCountryCode } from '../../utils/country.code.js';
|
|
12
13
|
|
|
@@ -481,10 +482,19 @@ export class UBLEncoder extends UBLBaseEncoder {
|
|
|
481
482
|
taxInclusiveAmountElement.textContent = amount(totals.grandTotal);
|
|
482
483
|
legalMonetaryTotalNode.appendChild(taxInclusiveAmountElement);
|
|
483
484
|
|
|
484
|
-
//
|
|
485
|
+
// Paid amount (BT-113), before the payable amount in the schema order
|
|
486
|
+
const payment = getPaymentTotals(invoice, totals);
|
|
487
|
+
if (payment.paidAmount) {
|
|
488
|
+
const prepaidAmountElement = doc.createElement('cbc:PrepaidAmount');
|
|
489
|
+
prepaidAmountElement.setAttribute('currencyID', invoice.currency);
|
|
490
|
+
prepaidAmountElement.textContent = amount(payment.paidAmount);
|
|
491
|
+
legalMonetaryTotalNode.appendChild(prepaidAmountElement);
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
// Payable amount (BT-115)
|
|
485
495
|
const payableAmountElement = doc.createElement('cbc:PayableAmount');
|
|
486
496
|
payableAmountElement.setAttribute('currencyID', invoice.currency);
|
|
487
|
-
payableAmountElement.textContent = amount(
|
|
497
|
+
payableAmountElement.textContent = amount(payment.duePayable);
|
|
488
498
|
legalMonetaryTotalNode.appendChild(payableAmountElement);
|
|
489
499
|
}
|
|
490
500
|
|
|
@@ -3,6 +3,8 @@ import type { TAccountingDoc, TCreditNote, TInvoiceDocument } from '../../interf
|
|
|
3
3
|
import { UBLDocumentType, UBL_NAMESPACES } from './ubl.types.js';
|
|
4
4
|
import { getWritableDate } from '../utils/date.value.js';
|
|
5
5
|
import { assertVatCategoryWritable } from '../utils/vat.category.js';
|
|
6
|
+
import { computeDocumentTotals } from '../utils/document.totals.js';
|
|
7
|
+
import { getPaymentTotals } from '../utils/paid.amount.js';
|
|
6
8
|
|
|
7
9
|
/**
|
|
8
10
|
* The children of the document root in the order the UBL 2.1 schema requires
|
|
@@ -51,6 +53,8 @@ export abstract class UBLBaseEncoder extends BaseEncoder {
|
|
|
51
53
|
public async encode(invoice: TAccountingDoc): Promise<string> {
|
|
52
54
|
// a reverse charge document that lacks what EN 16931 requires of one is refused (BR-AE-02, BR-AE-05)
|
|
53
55
|
assertVatCategoryWritable(invoice, 'ubl');
|
|
56
|
+
// a paid amount the advance payments do not add up to, or with more decimals than the currency has, is refused
|
|
57
|
+
getPaymentTotals(invoice, computeDocumentTotals(invoice));
|
|
54
58
|
// a credit note is a CreditNote document; an invoice, a debit note and a self-billed invoice are Invoice documents that differ in their type code
|
|
55
59
|
if (invoice.accountingDocType === 'creditnote') {
|
|
56
60
|
return this.encodeCreditNote(invoice);
|
|
@@ -12,6 +12,7 @@ import {
|
|
|
12
12
|
getPrecedingInvoiceFields,
|
|
13
13
|
type TPrecedingInvoiceReference,
|
|
14
14
|
} from '../../utils/preceding.invoice.js';
|
|
15
|
+
import { readPaidAmount } from '../../utils/paid.amount.js';
|
|
15
16
|
|
|
16
17
|
/**
|
|
17
18
|
* Decoder for XRechnung (UBL) format
|
|
@@ -240,6 +241,9 @@ export class XRechnungDecoder extends UBLBaseDecoder {
|
|
|
240
241
|
const hasPaymentInformation = Object.values(paymentInformation).some(value => Boolean(value));
|
|
241
242
|
const hasDateInformation = Object.values(dateInformation).some(value => Boolean(value));
|
|
242
243
|
|
|
244
|
+
// the paid amount (BT-113); the amount due (BT-115) follows from it and the totals
|
|
245
|
+
const paidAmount = readPaidAmount(this.getText('/*/cac:LegalMonetaryTotal/cbc:PrepaidAmount'), 'ubl');
|
|
246
|
+
|
|
243
247
|
// Create the common invoice data with metadata only when the source XML actually contains it.
|
|
244
248
|
const invoiceData: any = {
|
|
245
249
|
type: 'accounting-doc' as const,
|
|
@@ -278,6 +282,7 @@ export class XRechnungDecoder extends UBLBaseDecoder {
|
|
|
278
282
|
}
|
|
279
283
|
: {}),
|
|
280
284
|
...getPrecedingInvoiceFields(accountingDocType, this.extractPrecedingInvoiceReferences()),
|
|
285
|
+
...(paidAmount === undefined ? {} : { paidAmount }),
|
|
281
286
|
};
|
|
282
287
|
|
|
283
288
|
if (hasBusinessReferences || hasPaymentInformation || hasDateInformation) {
|
|
@@ -96,10 +96,8 @@ export interface IDocumentTotals {
|
|
|
96
96
|
vatGroups: IDocumentVatGroup[];
|
|
97
97
|
/** Invoice total VAT amount (BT-110): the sum of the rounded VAT category tax amounts */
|
|
98
98
|
taxTotal: Decimal;
|
|
99
|
-
/** Invoice total amount with VAT (BT-112): BT-109 + BT-110 */
|
|
99
|
+
/** Invoice total amount with VAT (BT-112): BT-109 + BT-110; the paid amount and the amount due are `getPaymentTotals` */
|
|
100
100
|
grandTotal: Decimal;
|
|
101
|
-
/** Amount due for payment (BT-115): BT-112, as the envelope states no paid amount yet */
|
|
102
|
-
duePayable: Decimal;
|
|
103
101
|
}
|
|
104
102
|
|
|
105
103
|
/**
|
|
@@ -168,6 +166,5 @@ export const computeDocumentTotals = (
|
|
|
168
166
|
vatGroups,
|
|
169
167
|
taxTotal,
|
|
170
168
|
grandTotal,
|
|
171
|
-
duePayable: grandTotal,
|
|
172
169
|
};
|
|
173
170
|
};
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
import type { TAccountingDoc, TAdvancePayment } from '../../interfaces/common.js';
|
|
2
|
+
import { EInvoiceFormatError, EInvoiceParsingError } from '../../errors.js';
|
|
3
|
+
import { Decimal } from './decimal.js';
|
|
4
|
+
import { toPlainDecimalString } from './number.text.js';
|
|
5
|
+
import type { IDocumentTotals } from './document.totals.js';
|
|
6
|
+
|
|
7
|
+
/** The paid amount (BT-113) and the amount due for payment (BT-115) an encoder writes */
|
|
8
|
+
export interface IPaymentTotals {
|
|
9
|
+
/** Paid amount (BT-113) as the document states it; undefined when it states none or zero, and nothing is written */
|
|
10
|
+
paidAmount?: Decimal;
|
|
11
|
+
/** Amount due for payment (BT-115): BT-112 − BT-113 (BR-CO-16; the envelope has no rounding amount BT-114); zero or negative when paid in full or overpaid */
|
|
12
|
+
duePayable: Decimal;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/** Why a paid amount cannot be written: the rule it breaks, the field and what is wrong */
|
|
16
|
+
export interface IPaymentProblem {
|
|
17
|
+
ruleId: string;
|
|
18
|
+
field: string;
|
|
19
|
+
message: string;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/** Carries a payment problem out of the computation */
|
|
23
|
+
class PaymentProblemSignal extends Error {
|
|
24
|
+
constructor(public readonly problem: IPaymentProblem) {
|
|
25
|
+
super(problem.message);
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const refuse = (field: string, message: string, ruleId = 'PAID-AMOUNT'): never => {
|
|
30
|
+
throw new PaymentProblemSignal({ ruleId, field, message });
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
/** An amount as the document states it, at its full precision; a value that is no finite number is refused */
|
|
34
|
+
const statedDecimal = (value: unknown, field: string): Decimal => {
|
|
35
|
+
if (typeof value !== 'number' || !Number.isFinite(value)) {
|
|
36
|
+
return refuse(field, `${field} is no finite number: ${String(value)}`);
|
|
37
|
+
}
|
|
38
|
+
return new Decimal(toPlainDecimalString(value));
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
/** The number of decimals of a stated amount */
|
|
42
|
+
const decimalsOf = (value: number): number => (toPlainDecimalString(value).split('.')[1] ?? '').length;
|
|
43
|
+
|
|
44
|
+
/** The document types that can be a final invoice (Endrechnung) and deduct advance payments */
|
|
45
|
+
const finalInvoiceTypes: TAccountingDoc['accountingDocType'][] = ['invoice', 'corrected-invoice', 'self-billed-invoice'];
|
|
46
|
+
|
|
47
|
+
/** What the paid amount and the amount due are computed from */
|
|
48
|
+
export type TPaymentSource = Pick<TAccountingDoc, 'accountingDocType' | 'paidAmount'> & {
|
|
49
|
+
advancePayments?: TAdvancePayment[];
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* The paid amount and the amount due of a document, as an encoder writes them.
|
|
54
|
+
* - When the document lists `advancePayments`, its `paidAmount` equals their
|
|
55
|
+
* gross sum, the `net` plus the `vat` of every group of every payment, to
|
|
56
|
+
* the last decimal (the rule `@tsclass/tsclass` sets). A document that
|
|
57
|
+
* breaks it, or lists advance payments without a paid amount, is refused;
|
|
58
|
+
* neither value is recomputed from the other.
|
|
59
|
+
* - The paid amount (BT-113) has at most the currency's decimals, two at most
|
|
60
|
+
* (BR-DEC-16); one with more is refused, not rounded.
|
|
61
|
+
* - A paid amount of zero is nothing paid, as an absent one: BT-113 is not
|
|
62
|
+
* written and BT-115 is BT-112.
|
|
63
|
+
* @param accountingDoc The document
|
|
64
|
+
* @param totals Its totals (`computeDocumentTotals`)
|
|
65
|
+
*/
|
|
66
|
+
const computePaymentTotals = (accountingDoc: TPaymentSource, totals: IDocumentTotals): IPaymentTotals => {
|
|
67
|
+
const advancePayments = accountingDoc.advancePayments ?? [];
|
|
68
|
+
if (advancePayments.length > 0 && !finalInvoiceTypes.includes(accountingDoc.accountingDocType)) {
|
|
69
|
+
refuse(
|
|
70
|
+
'advancePayments',
|
|
71
|
+
`advancePayments: a ${accountingDoc.accountingDocType} deducts no advance payments; only an invoice, a corrected invoice or a self-billed invoice is a final invoice (§ 14 Abs. 5 Satz 2 UStG)`,
|
|
72
|
+
);
|
|
73
|
+
}
|
|
74
|
+
const statedPaid = accountingDoc.paidAmount;
|
|
75
|
+
if (advancePayments.length > 0) {
|
|
76
|
+
const grossSum = Decimal.sum(
|
|
77
|
+
advancePayments.flatMap((payment, paymentIndex) =>
|
|
78
|
+
payment.vatGroups.flatMap((group, groupIndex) => [
|
|
79
|
+
statedDecimal(group.net, `advancePayments[${paymentIndex}].vatGroups[${groupIndex}].net`),
|
|
80
|
+
statedDecimal(group.vat, `advancePayments[${paymentIndex}].vatGroups[${groupIndex}].vat`),
|
|
81
|
+
]),
|
|
82
|
+
),
|
|
83
|
+
);
|
|
84
|
+
if (statedPaid === undefined || !statedDecimal(statedPaid, 'paidAmount').equals(grossSum)) {
|
|
85
|
+
refuse(
|
|
86
|
+
'paidAmount',
|
|
87
|
+
`paidAmount (${statedPaid === undefined ? 'absent' : toPlainDecimalString(statedPaid)}) does not equal the gross sum of the advancePayments (${grossSum.toString()}), the net plus the vat of every group of every payment; neither value is recomputed from the other`,
|
|
88
|
+
);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
if (statedPaid === undefined) {
|
|
92
|
+
return { duePayable: totals.grandTotal };
|
|
93
|
+
}
|
|
94
|
+
const paid = statedDecimal(statedPaid, 'paidAmount');
|
|
95
|
+
if (decimalsOf(statedPaid) > totals.minorUnits) {
|
|
96
|
+
refuse('paidAmount', `paidAmount: the paid amount (BT-113) has at most ${totals.minorUnits} decimals, it is ${toPlainDecimalString(statedPaid)}`, 'BR-DEC-16');
|
|
97
|
+
}
|
|
98
|
+
if (paid.isZero()) {
|
|
99
|
+
return { duePayable: totals.grandTotal };
|
|
100
|
+
}
|
|
101
|
+
return { paidAmount: paid, duePayable: totals.grandTotal.subtract(paid) };
|
|
102
|
+
};
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* The paid amount and the amount due as an encoder writes them (see
|
|
106
|
+
* `computePaymentTotals` above for the rules); a document that breaks one is
|
|
107
|
+
* refused with an `EInvoiceFormatError` naming the field.
|
|
108
|
+
* @param accountingDoc The document
|
|
109
|
+
* @param totals Its totals (`computeDocumentTotals`)
|
|
110
|
+
*/
|
|
111
|
+
export const getPaymentTotals = (accountingDoc: TPaymentSource, totals: IDocumentTotals): IPaymentTotals => {
|
|
112
|
+
try {
|
|
113
|
+
return computePaymentTotals(accountingDoc, totals);
|
|
114
|
+
} catch (error) {
|
|
115
|
+
if (error instanceof PaymentProblemSignal) {
|
|
116
|
+
throw new EInvoiceFormatError(`${error.problem.ruleId}: ${error.problem.message}`, {
|
|
117
|
+
unsupportedFeatures: [error.problem.field],
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
throw error;
|
|
121
|
+
}
|
|
122
|
+
};
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* What keeps a document's paid amount from being written, for a validator to
|
|
126
|
+
* report instead of throwing; undefined when there is nothing
|
|
127
|
+
* @param accountingDoc The document
|
|
128
|
+
* @param totals Its totals (`computeDocumentTotals`)
|
|
129
|
+
*/
|
|
130
|
+
export const findPaymentProblem = (accountingDoc: TPaymentSource, totals: IDocumentTotals): IPaymentProblem | undefined => {
|
|
131
|
+
try {
|
|
132
|
+
computePaymentTotals(accountingDoc, totals);
|
|
133
|
+
return undefined;
|
|
134
|
+
} catch (error) {
|
|
135
|
+
if (error instanceof PaymentProblemSignal) {
|
|
136
|
+
return error.problem;
|
|
137
|
+
}
|
|
138
|
+
throw error;
|
|
139
|
+
}
|
|
140
|
+
};
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* The paid amount (BT-113) a decoder reads: the number the document states,
|
|
144
|
+
* or undefined when it states none or zero (a stated zero is nothing paid, as
|
|
145
|
+
* an absent one). A paid amount that is no number is refused, since the amount
|
|
146
|
+
* due depends on it.
|
|
147
|
+
* @param text The text of the paid amount element, '' when absent
|
|
148
|
+
* @param format The format being read
|
|
149
|
+
*/
|
|
150
|
+
export const readPaidAmount = (text: string, format: string): number | undefined => {
|
|
151
|
+
const trimmed = text.trim();
|
|
152
|
+
if (!trimmed) {
|
|
153
|
+
return undefined;
|
|
154
|
+
}
|
|
155
|
+
const value = Number(trimmed);
|
|
156
|
+
if (!/^[+-]?(\d+\.?\d*|\.\d+)$/.test(trimmed) || !Number.isFinite(value)) {
|
|
157
|
+
throw new EInvoiceParsingError(`The paid amount (BT-113) is no decimal number: ${trimmed}`, { format });
|
|
158
|
+
}
|
|
159
|
+
return value === 0 ? undefined : value;
|
|
160
|
+
};
|
|
@@ -16,7 +16,13 @@ export type TPrecedingInvoiceReference = TCorrectedInvoiceReference;
|
|
|
16
16
|
* taken only from what the document states:
|
|
17
17
|
* - a corrected invoice: the invoice it corrects (`correctedInvoice`);
|
|
18
18
|
* - a corrected invoice or a credit note: every related document with the
|
|
19
|
-
* relation `'corrects'
|
|
19
|
+
* relation `'corrects'`;
|
|
20
|
+
* - a document that can be a final invoice (an invoice, a corrected invoice or
|
|
21
|
+
* a self-billed invoice): the advance invoice of every advance payment that
|
|
22
|
+
* states one (`advancePayments[].invoice`), after the references above. The
|
|
23
|
+
* amounts are the paid amount (BT-113); the deduction per rate that § 14
|
|
24
|
+
* Abs. 5 Satz 2 UStG asks of a final invoice has no element in EN 16931
|
|
25
|
+
* (BMF letter of 15 October 2024, Rn. 48).
|
|
20
26
|
* Other documents and other relations write none.
|
|
21
27
|
*/
|
|
22
28
|
export const getPrecedingInvoiceReferences = (
|
|
@@ -40,6 +46,17 @@ export const getPrecedingInvoiceReferences = (
|
|
|
40
46
|
}
|
|
41
47
|
}
|
|
42
48
|
}
|
|
49
|
+
if ('advancePayments' in accountingDoc) {
|
|
50
|
+
for (const advancePayment of accountingDoc.advancePayments ?? []) {
|
|
51
|
+
if (advancePayment.invoice) {
|
|
52
|
+
references.push(
|
|
53
|
+
advancePayment.invoice.issueDate === undefined
|
|
54
|
+
? { documentId: advancePayment.invoice.documentId }
|
|
55
|
+
: { documentId: advancePayment.invoice.documentId, issueDate: advancePayment.invoice.issueDate },
|
|
56
|
+
);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
}
|
|
43
60
|
return references;
|
|
44
61
|
};
|
|
45
62
|
|
|
@@ -48,7 +65,11 @@ export const getPrecedingInvoiceReferences = (
|
|
|
48
65
|
* inverse of {@link getPrecedingInvoiceReferences}: on a corrected invoice the
|
|
49
66
|
* first reference is the invoice it corrects and further ones are related
|
|
50
67
|
* documents it corrects; on a credit note every reference is a related
|
|
51
|
-
* document it corrects. Other documents keep no reference, as before.
|
|
68
|
+
* document it corrects. Other documents keep no reference, as before. The
|
|
69
|
+
* XML does not tell an advance invoice from a corrected one, so a corrected
|
|
70
|
+
* invoice's advance invoices read back as related documents it corrects, and
|
|
71
|
+
* an invoice's advance invoices are not read: `advancePayments` needs the net
|
|
72
|
+
* and the tax per rate of every payment, which the XML does not carry.
|
|
52
73
|
*/
|
|
53
74
|
export const getPrecedingInvoiceFields = (
|
|
54
75
|
accountingDocType: TAccountingDocType,
|
|
@@ -247,31 +247,33 @@ export class EN16931BusinessRulesValidator {
|
|
|
247
247
|
);
|
|
248
248
|
}
|
|
249
249
|
|
|
250
|
-
// BR-CO-16: Amount due for payment = Invoice total with VAT - Paid amount
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
const
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
250
|
+
// BR-CO-16: Amount due for payment = Invoice total with VAT - Paid amount. The envelope's
|
|
251
|
+
// paid amount (BT-113) is the one the encoders write; the amount due (BT-115) is compared when
|
|
252
|
+
// a declared one is at hand (`metadata.amountDue`), since the envelope derives it otherwise
|
|
253
|
+
const paidAmountValue = invoice.paidAmount ?? 0;
|
|
254
|
+
const declaredDueValue = invoice.metadata?.amountDue;
|
|
255
|
+
if (declaredDueValue !== undefined) {
|
|
256
|
+
const paidAmount = useDecimal ? new Decimal(paidAmountValue) : paidAmountValue;
|
|
257
|
+
const expectedDueAmount = useDecimal
|
|
258
|
+
? (expectedGrossTotal as Decimal).subtract(paidAmount)
|
|
259
|
+
: (expectedGrossTotal as number) - (paidAmount as number);
|
|
260
|
+
const declaredDueAmount = useDecimal ? new Decimal(declaredDueValue) : declaredDueValue;
|
|
261
|
+
|
|
262
|
+
const isDueEqual = useDecimal
|
|
263
|
+
? this.decimalCalculator!.areEqual(expectedDueAmount, declaredDueAmount)
|
|
264
|
+
: this.currencyCalculator
|
|
265
|
+
? this.currencyCalculator.areEqual(expectedDueAmount as number, declaredDueAmount as number)
|
|
266
|
+
: Math.abs((expectedDueAmount as number) - (declaredDueAmount as number)) < 0.01;
|
|
267
|
+
|
|
268
|
+
if (!isDueEqual) {
|
|
269
|
+
this.addError(
|
|
270
|
+
'BR-CO-16',
|
|
271
|
+
`Amount due (${useDecimal ? (declaredDueAmount as Decimal).toFixed(2) : (declaredDueAmount as number).toFixed(2)}) does not match calculation (${useDecimal ? (expectedDueAmount as Decimal).toFixed(2) : (expectedDueAmount as number).toFixed(2)})`,
|
|
272
|
+
'amountDue',
|
|
273
|
+
useDecimal ? (declaredDueAmount as Decimal).toNumber() : declaredDueAmount as number,
|
|
274
|
+
useDecimal ? (expectedDueAmount as Decimal).toNumber() : expectedDueAmount as number
|
|
275
|
+
);
|
|
276
|
+
}
|
|
275
277
|
}
|
|
276
278
|
}
|
|
277
279
|
|
package/ts/interfaces/common.ts
CHANGED
|
@@ -93,6 +93,7 @@ export type { TCorrectedInvoice } from '@tsclass/tsclass/dist_ts/finance/index.j
|
|
|
93
93
|
export type { TCorrectedInvoiceReference } from '@tsclass/tsclass/dist_ts/finance/index.js';
|
|
94
94
|
export type { TInvoiceCorrection } from '@tsclass/tsclass/dist_ts/finance/index.js';
|
|
95
95
|
export type { TRelatedDocument } from '@tsclass/tsclass/dist_ts/finance/index.js';
|
|
96
|
+
export type { TAdvancePayment } from '@tsclass/tsclass/dist_ts/finance/index.js';
|
|
96
97
|
export type { TCreditNote } from '@tsclass/tsclass/dist_ts/finance/index.js';
|
|
97
98
|
export type { TDebitNote } from '@tsclass/tsclass/dist_ts/finance/index.js';
|
|
98
99
|
export type { TSelfBilledInvoice } from '@tsclass/tsclass/dist_ts/finance/index.js';
|