@fin.cx/einvoice 8.2.2 → 8.2.3
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/cii/cii.decoder.d.ts +6 -0
- package/dist_ts/formats/cii/cii.decoder.js +11 -1
- package/dist_ts/formats/cii/cii.encoder.js +4 -1
- package/dist_ts/formats/cii/facturx/facturx.decoder.js +8 -4
- package/dist_ts/formats/cii/facturx/facturx.encoder.js +22 -15
- package/dist_ts/formats/cii/zugferd/zugferd.decoder.js +8 -4
- package/dist_ts/formats/cii/zugferd/zugferd.encoder.js +27 -14
- package/dist_ts/formats/cii/zugferd/zugferd.v1.decoder.d.ts +5 -0
- package/dist_ts/formats/cii/zugferd/zugferd.v1.decoder.js +18 -5
- package/dist_ts/formats/ubl/generic/ubl.encoder.js +10 -13
- package/dist_ts/formats/ubl/ubl.encoder.js +4 -1
- package/dist_ts/formats/ubl/xrechnung/xrechnung.decoder.d.ts +4 -0
- package/dist_ts/formats/ubl/xrechnung/xrechnung.decoder.js +12 -2
- package/dist_ts/formats/utils/document.totals.d.ts +16 -6
- package/dist_ts/formats/utils/document.totals.js +25 -10
- package/dist_ts/formats/utils/vat.category.d.ts +50 -0
- package/dist_ts/formats/utils/vat.category.js +64 -0
- package/package.json +2 -2
- package/readme.md +25 -0
- package/ts/00_commitinfo_data.ts +1 -1
- package/ts/formats/cii/cii.decoder.ts +14 -0
- package/ts/formats/cii/cii.encoder.ts +3 -0
- package/ts/formats/cii/facturx/facturx.decoder.ts +8 -3
- package/ts/formats/cii/facturx/facturx.encoder.ts +22 -14
- package/ts/formats/cii/zugferd/zugferd.decoder.ts +8 -3
- package/ts/formats/cii/zugferd/zugferd.encoder.ts +29 -13
- package/ts/formats/cii/zugferd/zugferd.v1.decoder.ts +22 -4
- package/ts/formats/ubl/generic/ubl.encoder.ts +9 -13
- package/ts/formats/ubl/ubl.encoder.ts +3 -0
- package/ts/formats/ubl/xrechnung/xrechnung.decoder.ts +15 -1
- package/ts/formats/utils/document.totals.ts +36 -13
- package/ts/formats/utils/vat.category.ts +89 -0
|
@@ -249,6 +249,17 @@ export class ZUGFeRDEncoder extends CIIBaseEncoder {
|
|
|
249
249
|
nameElement.textContent = party.name;
|
|
250
250
|
partyElement.appendChild(nameElement);
|
|
251
251
|
|
|
252
|
+
// Legal registration identifier (BT-30 seller, BT-47 buyer), e.g. the commercial register
|
|
253
|
+
// number; in the schema order it follows the name. It is no tax registration: the scheme
|
|
254
|
+
// FC of ram:SpecifiedTaxRegistration is the seller's tax number (BT-32)
|
|
255
|
+
if (party.registrationDetails && party.registrationDetails.registrationId) {
|
|
256
|
+
const legalOrganizationElement = doc.createElement('ram:SpecifiedLegalOrganization');
|
|
257
|
+
const legalIdElement = doc.createElement('ram:ID');
|
|
258
|
+
legalIdElement.textContent = party.registrationDetails.registrationId;
|
|
259
|
+
legalOrganizationElement.appendChild(legalIdElement);
|
|
260
|
+
partyElement.appendChild(legalOrganizationElement);
|
|
261
|
+
}
|
|
262
|
+
|
|
252
263
|
// Add postal address
|
|
253
264
|
const addressElement = doc.createElement('ram:PostalTradeAddress');
|
|
254
265
|
|
|
@@ -299,15 +310,6 @@ export class ZUGFeRDEncoder extends CIIBaseEncoder {
|
|
|
299
310
|
partyElement.appendChild(taxRegistrationElement);
|
|
300
311
|
}
|
|
301
312
|
|
|
302
|
-
// Add registration ID if available
|
|
303
|
-
if (party.registrationDetails && party.registrationDetails.registrationId) {
|
|
304
|
-
const regRegistrationElement = doc.createElement('ram:SpecifiedTaxRegistration');
|
|
305
|
-
const regIdElement = doc.createElement('ram:ID');
|
|
306
|
-
regIdElement.setAttribute('schemeID', 'FC');
|
|
307
|
-
regIdElement.textContent = party.registrationDetails.registrationId;
|
|
308
|
-
regRegistrationElement.appendChild(regIdElement);
|
|
309
|
-
partyElement.appendChild(regRegistrationElement);
|
|
310
|
-
}
|
|
311
313
|
}
|
|
312
314
|
|
|
313
315
|
/**
|
|
@@ -460,8 +462,8 @@ export class ZUGFeRDEncoder extends CIIBaseEncoder {
|
|
|
460
462
|
// the EN 16931 totals in decimal arithmetic, rounded as the business rules check them
|
|
461
463
|
const totals = computeDocumentTotals(invoice);
|
|
462
464
|
|
|
463
|
-
// VAT breakdown (BG-23), one per rate
|
|
464
|
-
for (const { rate, taxableAmount, taxAmount } of totals.vatGroups) {
|
|
465
|
+
// VAT breakdown (BG-23), one per VAT category and rate
|
|
466
|
+
for (const { category, rate, taxableAmount, taxAmount, exemption } of totals.vatGroups) {
|
|
465
467
|
const taxElement = doc.createElement('ram:ApplicableTradeTax');
|
|
466
468
|
|
|
467
469
|
// VAT category tax amount (BT-117)
|
|
@@ -473,6 +475,13 @@ export class ZUGFeRDEncoder extends CIIBaseEncoder {
|
|
|
473
475
|
const typeCodeElement = doc.createElement('ram:TypeCode');
|
|
474
476
|
typeCodeElement.textContent = 'VAT';
|
|
475
477
|
taxElement.appendChild(typeCodeElement);
|
|
478
|
+
|
|
479
|
+
// VAT exemption reason text (BT-120), for reverse charge (BR-AE-10)
|
|
480
|
+
if (exemption) {
|
|
481
|
+
const exemptionReasonElement = doc.createElement('ram:ExemptionReason');
|
|
482
|
+
exemptionReasonElement.textContent = exemption.reason;
|
|
483
|
+
taxElement.appendChild(exemptionReasonElement);
|
|
484
|
+
}
|
|
476
485
|
|
|
477
486
|
// VAT category taxable amount (BT-116)
|
|
478
487
|
const basisAmountElement = doc.createElement('ram:BasisAmount');
|
|
@@ -481,8 +490,15 @@ export class ZUGFeRDEncoder extends CIIBaseEncoder {
|
|
|
481
490
|
|
|
482
491
|
// Add category code
|
|
483
492
|
const categoryCodeElement = doc.createElement('ram:CategoryCode');
|
|
484
|
-
categoryCodeElement.textContent =
|
|
493
|
+
categoryCodeElement.textContent = category;
|
|
485
494
|
taxElement.appendChild(categoryCodeElement);
|
|
495
|
+
|
|
496
|
+
// VAT exemption reason code (BT-121)
|
|
497
|
+
if (exemption) {
|
|
498
|
+
const exemptionReasonCodeElement = doc.createElement('ram:ExemptionReasonCode');
|
|
499
|
+
exemptionReasonCodeElement.textContent = exemption.code;
|
|
500
|
+
taxElement.appendChild(exemptionReasonCodeElement);
|
|
501
|
+
}
|
|
486
502
|
|
|
487
503
|
// Add rate
|
|
488
504
|
const rateElement = doc.createElement('ram:RateApplicablePercent');
|
|
@@ -594,7 +610,7 @@ export class ZUGFeRDEncoder extends CIIBaseEncoder {
|
|
|
594
610
|
|
|
595
611
|
// Add tax category code
|
|
596
612
|
const taxCategoryCodeElement = doc.createElement('ram:CategoryCode');
|
|
597
|
-
taxCategoryCodeElement.textContent =
|
|
613
|
+
taxCategoryCodeElement.textContent = totals.lineVatCategories[index]; // BT-151
|
|
598
614
|
taxElement.appendChild(taxCategoryCodeElement);
|
|
599
615
|
|
|
600
616
|
// Add tax rate
|
|
@@ -35,6 +35,19 @@ export class ZUGFeRDV1Decoder extends CIIBaseDecoder {
|
|
|
35
35
|
};
|
|
36
36
|
}
|
|
37
37
|
|
|
38
|
+
/**
|
|
39
|
+
* Whether every line of the v1 document has the VAT category reverse charge (AE), read with
|
|
40
|
+
* the v1 namespaces from the line settlement (`ram:SpecifiedSupplyChainTradeSettlement`)
|
|
41
|
+
*/
|
|
42
|
+
protected override isReverseChargeDocument(): boolean {
|
|
43
|
+
const categories = zugferdV1Select(
|
|
44
|
+
'/rsm:CrossIndustryDocument/rsm:SpecifiedSupplyChainTradeTransaction/ram:IncludedSupplyChainTradeLineItem/ram:SpecifiedSupplyChainTradeSettlement/ram:ApplicableTradeTax/ram:CategoryCode',
|
|
45
|
+
this.doc,
|
|
46
|
+
);
|
|
47
|
+
const codes = (Array.isArray(categories) ? categories : []).map((node) => ((node as Node).textContent ?? '').trim());
|
|
48
|
+
return codes.length > 0 && codes.every((code) => code === 'AE');
|
|
49
|
+
}
|
|
50
|
+
|
|
38
51
|
/**
|
|
39
52
|
* Reads the issue date of the v1 header (`rsm:HeaderExchangedDocument`); a
|
|
40
53
|
* missing or invalid one is refused with an `EInvoiceParsingError`.
|
|
@@ -147,8 +160,8 @@ export class ZUGFeRDV1Decoder extends CIIBaseDecoder {
|
|
|
147
160
|
// Extract the actual delivery date, if stated
|
|
148
161
|
const deliveryDate = this.extractDeliveryDate();
|
|
149
162
|
|
|
150
|
-
//
|
|
151
|
-
const reverseCharge = this.
|
|
163
|
+
// Reverse charge: every line has the VAT category AE
|
|
164
|
+
const reverseCharge = this.isReverseChargeDocument();
|
|
152
165
|
|
|
153
166
|
// Create the common invoice data
|
|
154
167
|
const invoiceData = {
|
|
@@ -217,8 +230,13 @@ export class ZUGFeRDV1Decoder extends CIIBaseDecoder {
|
|
|
217
230
|
// Extract VAT ID
|
|
218
231
|
const vatId = this.getText(`${partyXPath}/ram:SpecifiedTaxRegistration/ram:ID[@schemeID="VA"]`) || '';
|
|
219
232
|
|
|
220
|
-
//
|
|
221
|
-
|
|
233
|
+
// the legal registration identifier, and a tax number (scheme FC) as before when that is all
|
|
234
|
+
// the document states; the party is still read with the ZUGFeRD 2 namespaces (the known v1
|
|
235
|
+
// header defect), so for a v1 document this finds nothing yet
|
|
236
|
+
const registrationId =
|
|
237
|
+
this.getText(`${partyXPath}/ram:SpecifiedLegalOrganization/ram:ID`) ||
|
|
238
|
+
this.getText(`${partyXPath}/ram:SpecifiedTaxRegistration/ram:ID[@schemeID="FC"]`) ||
|
|
239
|
+
'';
|
|
222
240
|
|
|
223
241
|
// Create contact object
|
|
224
242
|
return {
|
|
@@ -395,7 +395,7 @@ export class UBLEncoder extends UBLBaseEncoder {
|
|
|
395
395
|
taxTotalNode.appendChild(taxAmountElement);
|
|
396
396
|
|
|
397
397
|
// VAT breakdown (BG-23), one subtotal per rate
|
|
398
|
-
for (const { rate, taxableAmount, taxAmount } of totals.vatGroups) {
|
|
398
|
+
for (const { category, rate, taxableAmount, taxAmount, exemption } of totals.vatGroups) {
|
|
399
399
|
const taxSubtotalNode = doc.createElement('cac:TaxSubtotal');
|
|
400
400
|
taxTotalNode.appendChild(taxSubtotalNode);
|
|
401
401
|
|
|
@@ -415,17 +415,14 @@ export class UBLEncoder extends UBLBaseEncoder {
|
|
|
415
415
|
const taxCategoryNode = doc.createElement('cac:TaxCategory');
|
|
416
416
|
taxSubtotalNode.appendChild(taxCategoryNode);
|
|
417
417
|
|
|
418
|
-
//
|
|
419
|
-
|
|
420
|
-
this.appendElement(doc, taxCategoryNode, 'cbc:ID', categoryId);
|
|
421
|
-
|
|
422
|
-
// Add percent with 2 decimal places
|
|
418
|
+
// VAT category code (BT-118) and rate (BT-119)
|
|
419
|
+
this.appendElement(doc, taxCategoryNode, 'cbc:ID', category);
|
|
423
420
|
this.appendElement(doc, taxCategoryNode, 'cbc:Percent', rate.toFixed(2));
|
|
424
421
|
|
|
425
|
-
//
|
|
426
|
-
if (
|
|
427
|
-
this.appendElement(doc, taxCategoryNode, 'cbc:TaxExemptionReasonCode',
|
|
428
|
-
this.appendElement(doc, taxCategoryNode, 'cbc:TaxExemptionReason',
|
|
422
|
+
// VAT exemption reason code (BT-121) and text (BT-120), for reverse charge (BR-AE-10)
|
|
423
|
+
if (exemption) {
|
|
424
|
+
this.appendElement(doc, taxCategoryNode, 'cbc:TaxExemptionReasonCode', exemption.code);
|
|
425
|
+
this.appendElement(doc, taxCategoryNode, 'cbc:TaxExemptionReason', exemption.reason);
|
|
429
426
|
}
|
|
430
427
|
|
|
431
428
|
// Add tax scheme
|
|
@@ -527,9 +524,8 @@ export class UBLEncoder extends UBLBaseEncoder {
|
|
|
527
524
|
const classifiedTaxCategoryNode = doc.createElement('cac:ClassifiedTaxCategory');
|
|
528
525
|
itemNode.appendChild(classifiedTaxCategoryNode);
|
|
529
526
|
|
|
530
|
-
//
|
|
531
|
-
|
|
532
|
-
this.appendElement(doc, classifiedTaxCategoryNode, 'cbc:ID', categoryId);
|
|
527
|
+
// Invoiced item VAT category code (BT-151)
|
|
528
|
+
this.appendElement(doc, classifiedTaxCategoryNode, 'cbc:ID', totals.lineVatCategories[index]);
|
|
533
529
|
|
|
534
530
|
// Tax percent with 2 decimal places
|
|
535
531
|
this.appendElement(doc, classifiedTaxCategoryNode, 'cbc:Percent', item.vatPercentage.toFixed(2));
|
|
@@ -2,6 +2,7 @@ import { BaseEncoder } from '../base/base.encoder.js';
|
|
|
2
2
|
import type { TAccountingDoc, TCreditNote, TInvoiceDocument } from '../../interfaces/common.js';
|
|
3
3
|
import { UBLDocumentType, UBL_NAMESPACES } from './ubl.types.js';
|
|
4
4
|
import { getWritableDate } from '../utils/date.value.js';
|
|
5
|
+
import { assertVatCategoryWritable } from '../utils/vat.category.js';
|
|
5
6
|
|
|
6
7
|
/**
|
|
7
8
|
* The children of the document root in the order the UBL 2.1 schema requires
|
|
@@ -48,6 +49,8 @@ export abstract class UBLBaseEncoder extends BaseEncoder {
|
|
|
48
49
|
* @returns UBL XML string
|
|
49
50
|
*/
|
|
50
51
|
public async encode(invoice: TAccountingDoc): Promise<string> {
|
|
52
|
+
// a reverse charge document that lacks what EN 16931 requires of one is refused (BR-AE-02, BR-AE-05)
|
|
53
|
+
assertVatCategoryWritable(invoice, 'ubl');
|
|
51
54
|
// 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
|
|
52
55
|
if (invoice.accountingDocType === 'creditnote') {
|
|
53
56
|
return this.encodeCreditNote(invoice);
|
|
@@ -261,7 +261,9 @@ export class XRechnungDecoder extends UBLBaseDecoder {
|
|
|
261
261
|
subject: subject,
|
|
262
262
|
items: items,
|
|
263
263
|
dueInDays: dueInDays,
|
|
264
|
-
|
|
264
|
+
// reverse charge when every line is VAT category AE; the envelope states it for the
|
|
265
|
+
// whole document, so a document mixing AE lines with others cannot say it
|
|
266
|
+
reverseCharge: this.isReverseChargeDocument(),
|
|
265
267
|
currency: currencyCode as finance.TCurrency,
|
|
266
268
|
notes: notes,
|
|
267
269
|
objectActions: [],
|
|
@@ -301,6 +303,18 @@ export class XRechnungDecoder extends UBLBaseDecoder {
|
|
|
301
303
|
}
|
|
302
304
|
}
|
|
303
305
|
|
|
306
|
+
/**
|
|
307
|
+
* Whether every line of the document has the VAT category reverse charge (AE, BT-151)
|
|
308
|
+
*/
|
|
309
|
+
private isReverseChargeDocument(): boolean {
|
|
310
|
+
const categories = this.select(
|
|
311
|
+
'/*/cac:InvoiceLine/cac:Item/cac:ClassifiedTaxCategory/cbc:ID | /*/cac:CreditNoteLine/cac:Item/cac:ClassifiedTaxCategory/cbc:ID',
|
|
312
|
+
this.doc,
|
|
313
|
+
);
|
|
314
|
+
const codes = (Array.isArray(categories) ? categories : []).map((node) => (node.textContent ?? '').trim());
|
|
315
|
+
return codes.length > 0 && codes.every((code) => code === 'AE');
|
|
316
|
+
}
|
|
317
|
+
|
|
304
318
|
/**
|
|
305
319
|
* Reads the preceding invoice references (BG-3): the number (BT-25) and,
|
|
306
320
|
* when stated, the issue date (BT-26) of each.
|
|
@@ -3,6 +3,7 @@ import { Decimal } from './decimal.js';
|
|
|
3
3
|
import { DecimalCurrencyCalculator } from './currency.calculator.decimal.js';
|
|
4
4
|
import { EInvoiceFormatError } from '../../errors.js';
|
|
5
5
|
import type { ValidationResult } from '../validation/validation.types.js';
|
|
6
|
+
import { getVatCategory, getVatExemption, type IVatExemption, type TVatCategoryCode } from './vat.category.js';
|
|
6
7
|
|
|
7
8
|
/** An item amount that is no finite number, so no total can be computed from it */
|
|
8
9
|
export interface IInvalidItemAmount {
|
|
@@ -61,13 +62,17 @@ export const getDocumentCalculator = (currency: TAccountingDoc['currency']): Dec
|
|
|
61
62
|
new DecimalCurrencyCalculator(currency, 'HALF_UP', { maxDecimals: EN16931_MAX_AMOUNT_DECIMALS });
|
|
62
63
|
|
|
63
64
|
/**
|
|
64
|
-
* The VAT breakdown of one rate (BG-23): the
|
|
65
|
-
*
|
|
65
|
+
* The VAT breakdown of one VAT category and rate (BG-23): the category
|
|
66
|
+
* (BT-118), the rate (BT-119), the taxable amount (BT-116), the tax on it
|
|
67
|
+
* (BT-117) and, for a category that states one, the exemption reason
|
|
68
|
+
* (BT-121, BT-120).
|
|
66
69
|
*/
|
|
67
70
|
export interface IDocumentVatGroup {
|
|
71
|
+
category: TVatCategoryCode;
|
|
68
72
|
rate: number;
|
|
69
73
|
taxableAmount: Decimal;
|
|
70
74
|
taxAmount: Decimal;
|
|
75
|
+
exemption?: IVatExemption;
|
|
71
76
|
}
|
|
72
77
|
|
|
73
78
|
/**
|
|
@@ -80,11 +85,13 @@ export interface IDocumentTotals {
|
|
|
80
85
|
minorUnits: number;
|
|
81
86
|
/** the Invoice line net amount (BT-131) of each item, in item order: quantity × net price, rounded */
|
|
82
87
|
lineNetAmounts: Decimal[];
|
|
88
|
+
/** the VAT category (BT-151) of each item, in item order */
|
|
89
|
+
lineVatCategories: TVatCategoryCode[];
|
|
83
90
|
/** Sum of Invoice line net amount (BT-106) */
|
|
84
91
|
lineTotal: Decimal;
|
|
85
92
|
/** Invoice total amount without VAT (BT-109): BT-106, as the envelope has no document level allowances or charges */
|
|
86
93
|
taxBasisTotal: Decimal;
|
|
87
|
-
/** VAT breakdown (BG-23), in the order
|
|
94
|
+
/** VAT breakdown (BG-23), one per VAT category and rate, in the order they first appear */
|
|
88
95
|
vatGroups: IDocumentVatGroup[];
|
|
89
96
|
/** Invoice total VAT amount (BT-110): the sum of the rounded VAT category tax amounts */
|
|
90
97
|
taxTotal: Decimal;
|
|
@@ -100,16 +107,20 @@ export interface IDocumentTotals {
|
|
|
100
107
|
* decimals (BR-DEC-*): each line net amount (BT-131) is quantity × net price
|
|
101
108
|
* rounded; the sum of line net amounts (BT-106) is their sum (BR-CO-10);
|
|
102
109
|
* each VAT category taxable amount (BT-116) is the sum of the line net amounts
|
|
103
|
-
*
|
|
104
|
-
* (BR-CO-17); the total VAT (BT-110) is the sum of the category tax amounts
|
|
110
|
+
* of that category and rate, and its tax amount (BT-117) is BT-116 × rate / 100
|
|
111
|
+
* rounded (BR-CO-17), 0 for reverse charge (BR-AE-09); the total VAT (BT-110) is the sum of the category tax amounts
|
|
105
112
|
* (BR-CO-14); the total with VAT (BT-112) is BT-109 + BT-110 (BR-CO-15).
|
|
106
113
|
* @param accountingDoc The document
|
|
107
114
|
*/
|
|
108
|
-
export const computeDocumentTotals = (
|
|
115
|
+
export const computeDocumentTotals = (
|
|
116
|
+
accountingDoc: Pick<TAccountingDoc, 'currency' | 'items'> & { reverseCharge?: boolean; language?: string },
|
|
117
|
+
): IDocumentTotals => {
|
|
109
118
|
const calculator = getDocumentCalculator(accountingDoc.currency);
|
|
110
119
|
const minorUnits = calculator.getCurrencyInfo().minorUnits;
|
|
120
|
+
const category = getVatCategory(accountingDoc);
|
|
111
121
|
const lineNetAmounts: Decimal[] = [];
|
|
112
|
-
const
|
|
122
|
+
const lineVatCategories: TVatCategoryCode[] = [];
|
|
123
|
+
const taxableByGroup = new Map<string, { category: TVatCategoryCode; rate: number; taxable: Decimal }>();
|
|
113
124
|
// an amount that is no number cannot be computed with; it is refused, not treated as 0
|
|
114
125
|
const [firstInvalid] = findInvalidItemAmounts(accountingDoc.items);
|
|
115
126
|
if (firstInvalid) {
|
|
@@ -121,19 +132,31 @@ export const computeDocumentTotals = (accountingDoc: Pick<TAccountingDoc, 'curre
|
|
|
121
132
|
for (const item of accountingDoc.items ?? []) {
|
|
122
133
|
const lineNet = calculator.calculateLineNet(item.unitQuantity, item.unitNetPrice);
|
|
123
134
|
lineNetAmounts.push(lineNet);
|
|
124
|
-
|
|
135
|
+
lineVatCategories.push(category);
|
|
136
|
+
// grouped by VAT category and rate: the same rate under two categories is two groups
|
|
137
|
+
const key = `${category}|${item.vatPercentage}`;
|
|
138
|
+
const group = taxableByGroup.get(key) ?? { category, rate: item.vatPercentage, taxable: Decimal.ZERO };
|
|
139
|
+
group.taxable = group.taxable.add(lineNet);
|
|
140
|
+
taxableByGroup.set(key, group);
|
|
125
141
|
}
|
|
126
|
-
const vatGroups: IDocumentVatGroup[] = [...
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
142
|
+
const vatGroups: IDocumentVatGroup[] = [...taxableByGroup.values()].map((group) => {
|
|
143
|
+
const exemption = getVatExemption(group.category, accountingDoc.language);
|
|
144
|
+
return {
|
|
145
|
+
category: group.category,
|
|
146
|
+
rate: group.rate,
|
|
147
|
+
taxableAmount: calculator.round(group.taxable),
|
|
148
|
+
// reverse charge states no tax: the recipient owes it (BR-AE-09)
|
|
149
|
+
taxAmount: group.category === 'AE' ? calculator.round(Decimal.ZERO) : calculator.calculateVAT(group.taxable, group.rate),
|
|
150
|
+
...(exemption ? { exemption } : {}),
|
|
151
|
+
};
|
|
152
|
+
});
|
|
131
153
|
const lineTotal = calculator.round(Decimal.sum(lineNetAmounts));
|
|
132
154
|
const taxTotal = calculator.round(Decimal.sum(vatGroups.map((group) => group.taxAmount)));
|
|
133
155
|
const grandTotal = calculator.round(lineTotal.add(taxTotal));
|
|
134
156
|
return {
|
|
135
157
|
minorUnits,
|
|
136
158
|
lineNetAmounts,
|
|
159
|
+
lineVatCategories,
|
|
137
160
|
lineTotal,
|
|
138
161
|
taxBasisTotal: lineTotal,
|
|
139
162
|
vatGroups,
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import type { TAccountingDoc } from '../../interfaces/common.js';
|
|
2
|
+
import { EInvoiceFormatError } from '../../errors.js';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* The VAT category codes (UNTDID 5305, EN 16931 BT-118/BT-151) the encoders
|
|
6
|
+
* write: standard rated, or reverse charge. The envelope states reverse charge
|
|
7
|
+
* for the whole document (`reverseCharge`); an item carries no category of its
|
|
8
|
+
* own, so a document cannot mix reverse charge lines with standard rated ones.
|
|
9
|
+
*/
|
|
10
|
+
export type TVatCategoryCode = 'S' | 'AE';
|
|
11
|
+
|
|
12
|
+
/** The VAT exemption reason (BT-121 code, BT-120 text) a VAT category states */
|
|
13
|
+
export interface IVatExemption {
|
|
14
|
+
code: string;
|
|
15
|
+
reason: string;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* The VAT category of every line of a document: reverse charge (`AE`) when the
|
|
20
|
+
* document states it, standard rated (`S`) otherwise.
|
|
21
|
+
* @param accountingDoc The document
|
|
22
|
+
*/
|
|
23
|
+
export const getVatCategory = (accountingDoc: { reverseCharge?: boolean }): TVatCategoryCode =>
|
|
24
|
+
accountingDoc.reverseCharge ? 'AE' : 'S';
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* The exemption reason a reverse charge VAT breakdown states (BR-AE-10): the
|
|
28
|
+
* code `VATEX-EU-AE` and the wording the law prescribes, "Steuerschuldnerschaft
|
|
29
|
+
* des Leistungsempfängers" (§ 14a Abs. 1 and 5 UStG). A document in another
|
|
30
|
+
* language may use the wording of Article 226 Nr. 11a of the VAT Directive in
|
|
31
|
+
* that language, "Reverse charge" in English (Abschnitt 14a.1 Abs. 6 Satz 2
|
|
32
|
+
* UStAE). Other categories state none.
|
|
33
|
+
* @param category The VAT category
|
|
34
|
+
* @param language The document language
|
|
35
|
+
*/
|
|
36
|
+
export const getVatExemption = (category: TVatCategoryCode, language: string | undefined): IVatExemption | undefined =>
|
|
37
|
+
category === 'AE'
|
|
38
|
+
? {
|
|
39
|
+
code: 'VATEX-EU-AE',
|
|
40
|
+
reason: (language ?? '').toLowerCase().startsWith('de')
|
|
41
|
+
? 'Steuerschuldnerschaft des Leistungsempfängers'
|
|
42
|
+
: 'Reverse charge',
|
|
43
|
+
}
|
|
44
|
+
: undefined;
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Refuses a reverse charge document that lacks what EN 16931 requires of one,
|
|
48
|
+
* naming the rule; no value is put in place of a missing one.
|
|
49
|
+
* - BR-AE-05: every line of a reverse charge document has the VAT rate 0; the
|
|
50
|
+
* recipient owes the tax, and the invoice states none (§ 14a Abs. 5 Satz 2
|
|
51
|
+
* UStG; a stated amount would be owed under § 14c Abs. 1 UStG).
|
|
52
|
+
* - BR-AE-02: the seller states a VAT identifier (BT-31) and the buyer a VAT
|
|
53
|
+
* identifier (BT-48) or a legal registration identifier (BT-47). The rule
|
|
54
|
+
* also accepts the seller's tax registration identifier (BT-32, e.g. the
|
|
55
|
+
* Steuernummer) or a tax representative's VAT identifier (BT-63) instead of
|
|
56
|
+
* BT-31; the envelope can state neither, so a seller without a VAT
|
|
57
|
+
* identifier is refused. A domestic § 13b issuer that states only a
|
|
58
|
+
* Steuernummer, which § 14 Abs. 4 Satz 1 Nr. 2 UStG allows, is therefore
|
|
59
|
+
* refused until the envelope carries a tax number.
|
|
60
|
+
* @param accountingDoc The document
|
|
61
|
+
* @param targetFormat The format being written
|
|
62
|
+
*/
|
|
63
|
+
export const assertVatCategoryWritable = (accountingDoc: TAccountingDoc, targetFormat: string): void => {
|
|
64
|
+
if (!accountingDoc.reverseCharge) {
|
|
65
|
+
return;
|
|
66
|
+
}
|
|
67
|
+
const refuse = (rule: string, message: string): never => {
|
|
68
|
+
throw new EInvoiceFormatError(`${rule}: ${message}`, { targetFormat, unsupportedFeatures: [rule] });
|
|
69
|
+
};
|
|
70
|
+
for (const [index, item] of (accountingDoc.items ?? []).entries()) {
|
|
71
|
+
if (item.vatPercentage !== 0) {
|
|
72
|
+
refuse('BR-AE-05', `a reverse charge line has the VAT rate 0, items[${index}].vatPercentage is ${String(item.vatPercentage)}`);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
const seller = accountingDoc.from?.registrationDetails;
|
|
76
|
+
if (!seller?.vatId) {
|
|
77
|
+
refuse(
|
|
78
|
+
'BR-AE-02',
|
|
79
|
+
'a reverse charge document states the seller VAT identifier (BT-31), from.registrationDetails.vatId is missing; the rule would also accept a tax registration identifier (BT-32) or a tax representative (BT-63), which the envelope cannot state',
|
|
80
|
+
);
|
|
81
|
+
}
|
|
82
|
+
const buyer = accountingDoc.to?.registrationDetails;
|
|
83
|
+
if (!buyer?.vatId && !buyer?.registrationId) {
|
|
84
|
+
refuse(
|
|
85
|
+
'BR-AE-02',
|
|
86
|
+
'a reverse charge document states the buyer VAT identifier (BT-48) or legal registration identifier (BT-47), to.registrationDetails.vatId and .registrationId are missing',
|
|
87
|
+
);
|
|
88
|
+
}
|
|
89
|
+
};
|