@fin.cx/einvoice 8.3.0 → 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 +11 -4
- package/dist_ts/formats/cii/zugferd/zugferd.decoder.js +5 -1
- package/dist_ts/formats/cii/zugferd/zugferd.encoder.js +13 -8
- 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.d.ts +0 -7
- package/dist_ts/formats/ubl/generic/ubl.encoder.js +20 -25
- 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/ubl/xrechnung/xrechnung.encoder.d.ts +0 -5
- package/dist_ts/formats/ubl/xrechnung/xrechnung.encoder.js +1 -23
- package/dist_ts/formats/utils/country.code.d.ts +15 -0
- package/dist_ts/formats/utils/country.code.js +41 -0
- 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/codelist.validator.js +2 -2
- package/dist_ts/formats/validation/en16931.business-rules.validator.js +20 -18
- package/dist_ts/formats/validation/validation.types.js +23 -6
- 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 +37 -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 +10 -3
- package/ts/formats/cii/zugferd/zugferd.decoder.ts +5 -0
- package/ts/formats/cii/zugferd/zugferd.encoder.ts +12 -7
- 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 +25 -26
- package/ts/formats/ubl/ubl.encoder.ts +4 -0
- package/ts/formats/ubl/xrechnung/xrechnung.decoder.ts +5 -0
- package/ts/formats/ubl/xrechnung/xrechnung.encoder.ts +0 -23
- package/ts/formats/utils/country.code.ts +53 -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/codelist.validator.ts +2 -2
- package/ts/formats/validation/en16931.business-rules.validator.ts +27 -25
- package/ts/formats/validation/validation.types.ts +22 -5
- package/ts/interfaces/common.ts +1 -0
- package/ts/interfaces/en16931-metadata.ts +0 -1
|
@@ -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,7 +7,9 @@ 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';
|
|
12
|
+
import { resolveCountryCode } from '../../utils/country.code.js';
|
|
11
13
|
|
|
12
14
|
/**
|
|
13
15
|
* UBL Encoder implementation
|
|
@@ -253,17 +255,18 @@ export class UBLEncoder extends UBLBaseEncoder {
|
|
|
253
255
|
this.appendElement(doc, postalAddressNode, 'cbc:PostalZone', party.address.postalCode);
|
|
254
256
|
}
|
|
255
257
|
|
|
256
|
-
// Country
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
258
|
+
// Country code (BT-40, BT-55), mandatory (BR-09, BR-11): an ISO 3166-1 alpha-2 code, never
|
|
259
|
+
// derived from a name (BR-CL-14)
|
|
260
|
+
const countryNode = doc.createElement('cac:Country');
|
|
261
|
+
postalAddressNode.appendChild(countryNode);
|
|
262
|
+
this.appendElement(
|
|
263
|
+
doc,
|
|
264
|
+
countryNode,
|
|
265
|
+
'cbc:IdentificationCode',
|
|
266
|
+
resolveCountryCode(party.address, elementName === 'cac:AccountingSupplierParty' ? 'seller' : 'buyer', 'ubl'),
|
|
267
|
+
);
|
|
268
|
+
if (party.address.country) {
|
|
269
|
+
this.appendElement(doc, countryNode, 'cbc:Name', party.address.country);
|
|
267
270
|
}
|
|
268
271
|
|
|
269
272
|
// Party tax scheme (VAT ID)
|
|
@@ -479,10 +482,19 @@ export class UBLEncoder extends UBLBaseEncoder {
|
|
|
479
482
|
taxInclusiveAmountElement.textContent = amount(totals.grandTotal);
|
|
480
483
|
legalMonetaryTotalNode.appendChild(taxInclusiveAmountElement);
|
|
481
484
|
|
|
482
|
-
//
|
|
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)
|
|
483
495
|
const payableAmountElement = doc.createElement('cbc:PayableAmount');
|
|
484
496
|
payableAmountElement.setAttribute('currencyID', invoice.currency);
|
|
485
|
-
payableAmountElement.textContent = amount(
|
|
497
|
+
payableAmountElement.textContent = amount(payment.duePayable);
|
|
486
498
|
legalMonetaryTotalNode.appendChild(payableAmountElement);
|
|
487
499
|
}
|
|
488
500
|
|
|
@@ -576,19 +588,6 @@ export class UBLEncoder extends UBLBaseEncoder {
|
|
|
576
588
|
parentElement.appendChild(element);
|
|
577
589
|
}
|
|
578
590
|
|
|
579
|
-
/**
|
|
580
|
-
* Helper method to get country code from country name
|
|
581
|
-
* Simple implementation that assumes the country name is already a code
|
|
582
|
-
* @param countryName Country name
|
|
583
|
-
* @returns Country code (2-letter ISO code)
|
|
584
|
-
*/
|
|
585
|
-
private getCountryCode(countryName: string): string {
|
|
586
|
-
// In a real implementation, this would map country names to ISO codes
|
|
587
|
-
// For now, just return the first 2 characters or "XX" as fallback
|
|
588
|
-
if (!countryName) return 'XX';
|
|
589
|
-
return countryName.length >= 2 ? countryName.substring(0, 2).toUpperCase() : 'XX';
|
|
590
|
-
}
|
|
591
|
-
|
|
592
591
|
/**
|
|
593
592
|
* Preserves metadata from invoice to enhance UBL XML output
|
|
594
593
|
* @param doc XML document
|
|
@@ -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) {
|
|
@@ -89,9 +89,6 @@ export class XRechnungEncoder extends UBLEncoder {
|
|
|
89
89
|
}
|
|
90
90
|
}
|
|
91
91
|
|
|
92
|
-
// Add country code handling for German addresses
|
|
93
|
-
this.fixGermanCountryCodes(doc);
|
|
94
|
-
|
|
95
92
|
// Preserve business references from metadata
|
|
96
93
|
this.addBusinessReferences(doc, metadata?.businessReferences);
|
|
97
94
|
|
|
@@ -207,26 +204,6 @@ export class XRechnungEncoder extends UBLEncoder {
|
|
|
207
204
|
}
|
|
208
205
|
}
|
|
209
206
|
|
|
210
|
-
/**
|
|
211
|
-
* Fixes German country codes in the document
|
|
212
|
-
* @param doc XML document
|
|
213
|
-
*/
|
|
214
|
-
private fixGermanCountryCodes(doc: Document): void {
|
|
215
|
-
const countryNodes = doc.getElementsByTagName('cbc:IdentificationCode');
|
|
216
|
-
for (let i = 0; i < countryNodes.length; i++) {
|
|
217
|
-
const node = countryNodes[i];
|
|
218
|
-
if (node.textContent) {
|
|
219
|
-
const text = node.textContent.toLowerCase();
|
|
220
|
-
if (text === 'germany' || text === 'deutschland' || text === 'de') {
|
|
221
|
-
node.textContent = 'DE';
|
|
222
|
-
} else if (text.length > 2) {
|
|
223
|
-
// Try to use first 2 characters as country code
|
|
224
|
-
node.textContent = text.substring(0, 2).toUpperCase();
|
|
225
|
-
}
|
|
226
|
-
}
|
|
227
|
-
}
|
|
228
|
-
}
|
|
229
|
-
|
|
230
207
|
/**
|
|
231
208
|
* Adds business references from metadata to the document
|
|
232
209
|
* @param doc XML document
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import type { business } from '@tsclass/tsclass';
|
|
2
|
+
import { EInvoiceFormatError } from '../../errors.js';
|
|
3
|
+
import { CodeLists } from '../validation/validation.types.js';
|
|
4
|
+
|
|
5
|
+
/** The party whose address a country code is written for */
|
|
6
|
+
export type TCountryCodeParty = 'seller' | 'buyer';
|
|
7
|
+
|
|
8
|
+
const partyFields: Record<TCountryCodeParty, { path: string; term: string; presenceRule: string }> = {
|
|
9
|
+
// BR-09: the seller postal address (BG-5) states the seller country code (BT-40)
|
|
10
|
+
seller: { path: 'from.address', term: 'BT-40', presenceRule: 'BR-09' },
|
|
11
|
+
// BR-11: the buyer postal address (BG-8) states the buyer country code (BT-55)
|
|
12
|
+
buyer: { path: 'to.address', term: 'BT-55', presenceRule: 'BR-11' },
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* The country code an encoder writes for a party's address: an ISO 3166-1
|
|
17
|
+
* alpha-2 code EN 16931 accepts (BR-CL-14, `CodeLists.ISO3166`), taken from
|
|
18
|
+
* `countryCode`, or from `country` when that is already such a code; letter
|
|
19
|
+
* case does not matter. A code is never derived from a country name, so a
|
|
20
|
+
* name, an unknown code or a missing country is refused with an
|
|
21
|
+
* `EInvoiceFormatError` naming the field and the business term.
|
|
22
|
+
* @param address The party's address
|
|
23
|
+
* @param party Whether it is the seller's or the buyer's
|
|
24
|
+
* @param targetFormat The format being written
|
|
25
|
+
*/
|
|
26
|
+
export const resolveCountryCode = (
|
|
27
|
+
address: Partial<business.IAddress> | undefined,
|
|
28
|
+
party: TCountryCodeParty,
|
|
29
|
+
targetFormat: string,
|
|
30
|
+
): string => {
|
|
31
|
+
const { path, term, presenceRule } = partyFields[party];
|
|
32
|
+
const refuse = (rule: string, message: string): never => {
|
|
33
|
+
throw new EInvoiceFormatError(`${rule}: ${message}`, { targetFormat, unsupportedFeatures: [rule] });
|
|
34
|
+
};
|
|
35
|
+
const countryCode = (address?.countryCode ?? '').trim();
|
|
36
|
+
if (countryCode) {
|
|
37
|
+
if (!CodeLists.ISO3166.codes.has(countryCode.toUpperCase())) {
|
|
38
|
+
refuse('BR-CL-14', `the ${party} country code (${term}) is an ISO 3166-1 alpha-2 code, ${path}.countryCode is "${countryCode}"`);
|
|
39
|
+
}
|
|
40
|
+
return countryCode.toUpperCase();
|
|
41
|
+
}
|
|
42
|
+
const country = (address?.country ?? '').trim();
|
|
43
|
+
if (!country) {
|
|
44
|
+
refuse(presenceRule, `the ${party} postal address states a country code (${term}), ${path}.countryCode and ${path}.country are missing`);
|
|
45
|
+
}
|
|
46
|
+
if (!CodeLists.ISO3166.codes.has(country.toUpperCase())) {
|
|
47
|
+
refuse(
|
|
48
|
+
'BR-CL-14',
|
|
49
|
+
`the ${party} country code (${term}) is an ISO 3166-1 alpha-2 code, ${path}.countryCode is missing and ${path}.country is "${country}", which is not one; a code is not derived from a country name`,
|
|
50
|
+
);
|
|
51
|
+
}
|
|
52
|
+
return country.toUpperCase();
|
|
53
|
+
};
|
|
@@ -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,
|
|
@@ -96,7 +96,7 @@ export class CodeListValidator {
|
|
|
96
96
|
const buyerCountry = invoice.to?.address?.countryCode;
|
|
97
97
|
if (buyerCountry && !CodeLists.ISO3166.codes.has(buyerCountry.toUpperCase())) {
|
|
98
98
|
this.addError(
|
|
99
|
-
'BR-CL-
|
|
99
|
+
'BR-CL-14',
|
|
100
100
|
`Invalid buyer country code: ${buyerCountry}. Must be ISO 3166-1 alpha-2`,
|
|
101
101
|
'EN16931',
|
|
102
102
|
'to.address.countryCode',
|
|
@@ -110,7 +110,7 @@ export class CodeListValidator {
|
|
|
110
110
|
const deliveryCountry = invoice.metadata?.deliveryAddress?.countryCode;
|
|
111
111
|
if (deliveryCountry && !CodeLists.ISO3166.codes.has(deliveryCountry.toUpperCase())) {
|
|
112
112
|
this.addError(
|
|
113
|
-
'BR-CL-
|
|
113
|
+
'BR-CL-14',
|
|
114
114
|
`Invalid delivery country code: ${deliveryCountry}. Must be ISO 3166-1 alpha-2`,
|
|
115
115
|
'EN16931',
|
|
116
116
|
'metadata.deliveryAddress.countryCode',
|
|
@@ -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
|
|
|
@@ -96,13 +96,30 @@ export const CodeLists = {
|
|
|
96
96
|
])
|
|
97
97
|
},
|
|
98
98
|
|
|
99
|
-
// ISO 3166-1 alpha-2
|
|
99
|
+
// ISO 3166-1 alpha-2 country codes, as EN 16931 accepts them (BR-CL-14, BR-CL-15): the code list
|
|
100
|
+
// of the CEN/TC 434 validation artefacts 1.3.16 for UBL, which adds 1A (Kosovo) and XI (Northern
|
|
101
|
+
// Ireland) to the codes ISO 3166-1 assigns
|
|
100
102
|
ISO3166: {
|
|
101
|
-
version: '
|
|
103
|
+
version: 'EN 16931 validation artefacts 1.3.16',
|
|
102
104
|
codes: new Set([
|
|
103
|
-
'
|
|
104
|
-
'
|
|
105
|
-
'
|
|
105
|
+
'1A', 'AD', 'AE', 'AF', 'AG', 'AI', 'AL', 'AM', 'AO', 'AQ', 'AR', 'AS', 'AT', 'AU',
|
|
106
|
+
'AW', 'AX', 'AZ', 'BA', 'BB', 'BD', 'BE', 'BF', 'BG', 'BH', 'BI', 'BJ', 'BL', 'BM',
|
|
107
|
+
'BN', 'BO', 'BQ', 'BR', 'BS', 'BT', 'BV', 'BW', 'BY', 'BZ', 'CA', 'CC', 'CD', 'CF',
|
|
108
|
+
'CG', 'CH', 'CI', 'CK', 'CL', 'CM', 'CN', 'CO', 'CR', 'CU', 'CV', 'CW', 'CX', 'CY',
|
|
109
|
+
'CZ', 'DE', 'DJ', 'DK', 'DM', 'DO', 'DZ', 'EC', 'EE', 'EG', 'EH', 'ER', 'ES', 'ET',
|
|
110
|
+
'FI', 'FJ', 'FK', 'FM', 'FO', 'FR', 'GA', 'GB', 'GD', 'GE', 'GF', 'GG', 'GH', 'GI',
|
|
111
|
+
'GL', 'GM', 'GN', 'GP', 'GQ', 'GR', 'GS', 'GT', 'GU', 'GW', 'GY', 'HK', 'HM', 'HN',
|
|
112
|
+
'HR', 'HT', 'HU', 'ID', 'IE', 'IL', 'IM', 'IN', 'IO', 'IQ', 'IR', 'IS', 'IT', 'JE',
|
|
113
|
+
'JM', 'JO', 'JP', 'KE', 'KG', 'KH', 'KI', 'KM', 'KN', 'KP', 'KR', 'KW', 'KY', 'KZ',
|
|
114
|
+
'LA', 'LB', 'LC', 'LI', 'LK', 'LR', 'LS', 'LT', 'LU', 'LV', 'LY', 'MA', 'MC', 'MD',
|
|
115
|
+
'ME', 'MF', 'MG', 'MH', 'MK', 'ML', 'MM', 'MN', 'MO', 'MP', 'MQ', 'MR', 'MS', 'MT',
|
|
116
|
+
'MU', 'MV', 'MW', 'MX', 'MY', 'MZ', 'NA', 'NC', 'NE', 'NF', 'NG', 'NI', 'NL', 'NO',
|
|
117
|
+
'NP', 'NR', 'NU', 'NZ', 'OM', 'PA', 'PE', 'PF', 'PG', 'PH', 'PK', 'PL', 'PM', 'PN',
|
|
118
|
+
'PR', 'PS', 'PT', 'PW', 'PY', 'QA', 'RE', 'RO', 'RS', 'RU', 'RW', 'SA', 'SB', 'SC',
|
|
119
|
+
'SD', 'SE', 'SG', 'SH', 'SI', 'SJ', 'SK', 'SL', 'SM', 'SN', 'SO', 'SR', 'SS', 'ST',
|
|
120
|
+
'SV', 'SX', 'SY', 'SZ', 'TC', 'TD', 'TF', 'TG', 'TH', 'TJ', 'TK', 'TL', 'TM', 'TN',
|
|
121
|
+
'TO', 'TR', 'TT', 'TV', 'TW', 'TZ', 'UA', 'UG', 'UM', 'US', 'UY', 'UZ', 'VA', 'VC',
|
|
122
|
+
'VE', 'VG', 'VI', 'VN', 'VU', 'WF', 'WS', 'XI', 'YE', 'YT', 'ZA', 'ZM', 'ZW',
|
|
106
123
|
])
|
|
107
124
|
},
|
|
108
125
|
|
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';
|