@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.
Files changed (44) hide show
  1. package/dist_ts/00_commitinfo_data.js +1 -1
  2. package/dist_ts/einvoice.d.ts +14 -0
  3. package/dist_ts/einvoice.js +17 -1
  4. package/dist_ts/formats/cii/cii.encoder.js +5 -1
  5. package/dist_ts/formats/cii/facturx/facturx.decoder.js +5 -1
  6. package/dist_ts/formats/cii/facturx/facturx.encoder.js +7 -2
  7. package/dist_ts/formats/cii/zugferd/zugferd.decoder.js +5 -1
  8. package/dist_ts/formats/cii/zugferd/zugferd.encoder.js +7 -2
  9. package/dist_ts/formats/cii/zugferd/zugferd.v1.decoder.js +5 -1
  10. package/dist_ts/formats/semantic/semantic.adapter.js +10 -3
  11. package/dist_ts/formats/semantic/semantic.validator.js +17 -2
  12. package/dist_ts/formats/ubl/generic/ubl.encoder.js +12 -3
  13. package/dist_ts/formats/ubl/ubl.encoder.js +5 -1
  14. package/dist_ts/formats/ubl/xrechnung/xrechnung.decoder.js +5 -1
  15. package/dist_ts/formats/utils/document.totals.d.ts +1 -3
  16. package/dist_ts/formats/utils/document.totals.js +1 -2
  17. package/dist_ts/formats/utils/paid.amount.d.ts +44 -0
  18. package/dist_ts/formats/utils/paid.amount.js +124 -0
  19. package/dist_ts/formats/utils/preceding.invoice.d.ts +12 -2
  20. package/dist_ts/formats/utils/preceding.invoice.js +22 -3
  21. package/dist_ts/formats/validation/en16931.business-rules.validator.js +20 -18
  22. package/dist_ts/interfaces/common.d.ts +1 -0
  23. package/dist_ts/interfaces/en16931-metadata.d.ts +0 -1
  24. package/package.json +2 -2
  25. package/readme.md +36 -0
  26. package/ts/00_commitinfo_data.ts +1 -1
  27. package/ts/einvoice.ts +18 -0
  28. package/ts/formats/cii/cii.encoder.ts +4 -0
  29. package/ts/formats/cii/facturx/facturx.decoder.ts +5 -0
  30. package/ts/formats/cii/facturx/facturx.encoder.ts +6 -1
  31. package/ts/formats/cii/zugferd/zugferd.decoder.ts +5 -0
  32. package/ts/formats/cii/zugferd/zugferd.encoder.ts +6 -1
  33. package/ts/formats/cii/zugferd/zugferd.v1.decoder.ts +10 -0
  34. package/ts/formats/semantic/semantic.adapter.ts +9 -2
  35. package/ts/formats/semantic/semantic.validator.ts +17 -1
  36. package/ts/formats/ubl/generic/ubl.encoder.ts +12 -2
  37. package/ts/formats/ubl/ubl.encoder.ts +4 -0
  38. package/ts/formats/ubl/xrechnung/xrechnung.decoder.ts +5 -0
  39. package/ts/formats/utils/document.totals.ts +1 -4
  40. package/ts/formats/utils/paid.amount.ts +160 -0
  41. package/ts/formats/utils/preceding.invoice.ts +23 -2
  42. package/ts/formats/validation/en16931.business-rules.validator.ts +27 -25
  43. package/ts/interfaces/common.ts +1 -0
  44. 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
- appendAmount('ram:DuePayableAmount', totals.duePayable); // BT-115
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
- prepaidAmount: invoice.metadata?.extensions?.prepaidAmount,
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.totalGross
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
- // Payable amount
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(totals.duePayable);
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
- const paidAmount = useDecimal
252
- ? new Decimal(invoice.metadata?.paidAmount || 0)
253
- : invoice.metadata?.paidAmount || 0;
254
- const expectedDueAmount = useDecimal
255
- ? (expectedGrossTotal as Decimal).subtract(paidAmount)
256
- : (expectedGrossTotal as number) - (paidAmount as number);
257
- const declaredDueAmount = useDecimal
258
- ? new Decimal(invoice.metadata?.amountDue || (useDecimal ? (expectedGrossTotal as Decimal).toNumber() : expectedGrossTotal))
259
- : invoice.metadata?.amountDue || expectedGrossTotal;
260
-
261
- const isDueEqual = useDecimal
262
- ? this.decimalCalculator!.areEqual(expectedDueAmount, declaredDueAmount)
263
- : this.currencyCalculator
264
- ? this.currencyCalculator.areEqual(expectedDueAmount as number, declaredDueAmount as number)
265
- : Math.abs((expectedDueAmount as number) - (declaredDueAmount as number)) < 0.01;
266
-
267
- if (!isDueEqual) {
268
- this.addError(
269
- 'BR-CO-16',
270
- `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)})`,
271
- 'amountDue',
272
- useDecimal ? (declaredDueAmount as Decimal).toNumber() : declaredDueAmount as number,
273
- useDecimal ? (expectedDueAmount as Decimal).toNumber() : expectedDueAmount as number
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
 
@@ -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';
@@ -20,7 +20,6 @@ export interface IEInvoiceMetadata {
20
20
  vatAccountingCurrency?: string; // BT-6
21
21
  documentTypeCode?: string; // BT-3
22
22
  paymentMeansCode?: string; // BT-81
23
- paidAmount?: number; // BT-113
24
23
  amountDue?: number; // BT-115
25
24
 
26
25
  // Tax identifiers