@fin.cx/chartdata 2.16.0 → 3.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 +2 -2
- package/dist_ts/chartdata.hgb.js +4 -2
- package/dist_ts/chartdata.individual.d.ts +56 -0
- package/dist_ts/chartdata.individual.js +274 -0
- package/dist_ts/chartdata.lookup.d.ts +8 -1
- package/dist_ts/chartdata.lookup.js +13 -3
- package/dist_ts/chartdata.types.d.ts +141 -1
- package/dist_ts/data/datevnumbers.2026.d.ts +5 -0
- package/dist_ts/data/datevnumbers.2026.js +9841 -0
- package/dist_ts/data/skr03.2026.js +308 -6
- package/dist_ts/data/skr04.2026.js +327 -5
- package/dist_ts/index.d.ts +2 -0
- package/dist_ts/index.js +3 -1
- package/package.json +1 -1
- package/readme.md +157 -2
- package/ts/00_commitinfo_data.ts +1 -1
- package/ts/chartdata.hgb.ts +3 -1
- package/ts/chartdata.individual.ts +321 -0
- package/ts/chartdata.lookup.ts +19 -2
- package/ts/chartdata.types.ts +199 -1
- package/ts/data/datevnumbers.2026.ts +9844 -0
- package/ts/data/skr03.2026.ts +307 -5
- package/ts/data/skr04.2026.ts +326 -4
- package/ts/index.ts +2 -0
package/dist_ts/index.d.ts
CHANGED
|
@@ -4,3 +4,5 @@ export * from './chartdata.hgb.js';
|
|
|
4
4
|
export * from './chartdata.euer.js';
|
|
5
5
|
export { SKR03_2026 } from './data/skr03.2026.js';
|
|
6
6
|
export { SKR04_2026 } from './data/skr04.2026.js';
|
|
7
|
+
export * from './chartdata.individual.js';
|
|
8
|
+
export { DATEV_NUMBERS_SKR03_2026, DATEV_NUMBERS_SKR04_2026 } from './data/datevnumbers.2026.js';
|
package/dist_ts/index.js
CHANGED
|
@@ -4,4 +4,6 @@ export * from './chartdata.hgb.js';
|
|
|
4
4
|
export * from './chartdata.euer.js';
|
|
5
5
|
export { SKR03_2026 } from './data/skr03.2026.js';
|
|
6
6
|
export { SKR04_2026 } from './data/skr04.2026.js';
|
|
7
|
-
|
|
7
|
+
export * from './chartdata.individual.js';
|
|
8
|
+
export { DATEV_NUMBERS_SKR03_2026, DATEV_NUMBERS_SKR04_2026 } from './data/datevnumbers.2026.js';
|
|
9
|
+
//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiaW5kZXguanMiLCJzb3VyY2VSb290IjoiIiwic291cmNlcyI6WyIuLi90cy9pbmRleC50cyJdLCJuYW1lcyI6W10sIm1hcHBpbmdzIjoiQUFBQSxjQUFjLHNCQUFzQixDQUFDO0FBQ3JDLGNBQWMsdUJBQXVCLENBQUM7QUFDdEMsY0FBYyxvQkFBb0IsQ0FBQztBQUNuQyxjQUFjLHFCQUFxQixDQUFDO0FBQ3BDLE9BQU8sRUFBRSxVQUFVLEVBQUUsTUFBTSxzQkFBc0IsQ0FBQztBQUNsRCxPQUFPLEVBQUUsVUFBVSxFQUFFLE1BQU0sc0JBQXNCLENBQUM7QUFDbEQsY0FBYywyQkFBMkIsQ0FBQztBQUMxQyxPQUFPLEVBQUUsd0JBQXdCLEVBQUUsd0JBQXdCLEVBQUUsTUFBTSw2QkFBNkIsQ0FBQyJ9
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@fin.cx/chartdata",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "3.0.0",
|
|
4
4
|
"private": false,
|
|
5
5
|
"description": "Year-versioned German SKR03/SKR04 chart-of-accounts datasets with Automatikkonto functions, BU-key rules, UStVA-Kennzahl and Anlage EÜR mappings",
|
|
6
6
|
"main": "dist_ts/index.js",
|
package/readme.md
CHANGED
|
@@ -33,6 +33,11 @@ For reporting bugs, issues, or security vulnerabilities, please visit [community
|
|
|
33
33
|
accounts), the costs of a sale, write-downs and write-ups, the taxes withheld
|
|
34
34
|
from their income and the margin interest, listed per chart by
|
|
35
35
|
`getAccountsBySecuritiesRole` and found one by one with `getSecuritiesAccount`
|
|
36
|
+
- Payroll accounts with their payroll role (`payrollRole`): the pay and the
|
|
37
|
+
flat wage taxes (GuV 6a), the employer's contributions (GuV 6b), the debts
|
|
38
|
+
of a payroll until they are paid, the company-car benefit and the levy
|
|
39
|
+
refunds, and the clearing account, one account per role, found with
|
|
40
|
+
`getPayrollAccount`
|
|
36
41
|
- DATEV's mark "HB" (`commercialOnly`): an account posted in the commercial
|
|
37
42
|
balance sheet only
|
|
38
43
|
- The tax rule an income or expense account is kept for (`taxRule`):
|
|
@@ -43,6 +48,11 @@ For reporting bugs, issues, or security vulnerabilities, please visit [community
|
|
|
43
48
|
(§ 4 Abs. 3 EStG, § 60 Abs. 4 EStDV), with the lines of the 2025 and 2026
|
|
44
49
|
forms (`ANLAGE_EUER_FORMS`); every account maps or is excluded with its
|
|
45
50
|
reason (`euerExclusion`)
|
|
51
|
+
- Individual accounts (Individualkonten): the books' own accounts on the
|
|
52
|
+
numbers DATEV leaves free after an account, inheriting that account's
|
|
53
|
+
mappings (`BooksChart`, `individualAccountProblemsOf`, `freeNumbersOf`),
|
|
54
|
+
checked against the numbers the DATEV Kontenrahmen assigns and reserves
|
|
55
|
+
(`getDatevNumbers`)
|
|
46
56
|
- Coverage introspection that fails loudly on mapping gaps
|
|
47
57
|
- Provenance on every record
|
|
48
58
|
|
|
@@ -142,6 +152,142 @@ sale other expenses (Kz 183), write-downs and write-ups values at a closing date
|
|
|
142
152
|
interest-like expenses other debt interest (Kz 234), and the taxes on income a
|
|
143
153
|
withdrawal (Kz 122, § 12 Nr. 3 EStG).
|
|
144
154
|
|
|
155
|
+
DATEV holds no account of its own for options bought or written, a future's
|
|
156
|
+
variation margin, accrued interest bought with a bond, securities lent or sold
|
|
157
|
+
short, the cash collateral of a loan of securities, the results of
|
|
158
|
+
Termingeschäfte, a writer's premium earned, a short sale's result and the
|
|
159
|
+
compensations paid on securities sold short. Their roles (`options_held`,
|
|
160
|
+
`variation_margin_paid`, `variation_margin_received`, `accrued_interest_claim`,
|
|
161
|
+
`lending_claim`, `written_options`, `short_sale_obligations`,
|
|
162
|
+
`collateral_received`, `termingeschaeft_gain`, `termingeschaeft_loss`,
|
|
163
|
+
`stillhalter_income`, `short_sale_gain`, `short_sale_loss`,
|
|
164
|
+
`compensation_paid`) name no account of the standard charts: the books give
|
|
165
|
+
them an individual account, or book them on the account of their position.
|
|
166
|
+
|
|
167
|
+
### Payroll
|
|
168
|
+
|
|
169
|
+
A payroll run finds its accounts by the payroll role; within a chart each role
|
|
170
|
+
names one account, and every role has its account in both standard charts.
|
|
171
|
+
DATEV states each account's position beside it: the pay under "Löhne und
|
|
172
|
+
Gehälter" (GuV 6a, § 275 Abs. 2 Nr. 6 a HGB), the employer's contributions
|
|
173
|
+
under "Soziale Abgaben und Aufwendungen für Altersversorgung und für
|
|
174
|
+
Unterstützung" (GuV 6b). DATEV's collective accounts (SKR03 4100, SKR04 6000
|
|
175
|
+
and 6100) keep their positions and take no role.
|
|
176
|
+
|
|
177
|
+
```typescript
|
|
178
|
+
import { getPayrollAccount } from '@fin.cx/chartdata';
|
|
179
|
+
|
|
180
|
+
getPayrollAccount('SKR03', 2026, 'salaries')?.accountNumber; // '4120' Gehälter
|
|
181
|
+
getPayrollAccount('SKR04', 2026, 'wage_tax')?.accountNumber; // '3730' Verbindlichkeiten aus Lohn- und Kirchensteuer
|
|
182
|
+
getPayrollAccount('SKR04', 2026, 'company_car_benefit')?.ustva; // { kennzahl: 81, role: 'base' }
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
| Role | What it books | SKR03 | SKR04 |
|
|
186
|
+
| --- | --- | --- | --- |
|
|
187
|
+
| `wages` | Löhne (GuV 6a) | 4110 | 6010 |
|
|
188
|
+
| `salaries` | Gehälter (GuV 6a) | 4120 | 6020 |
|
|
189
|
+
| `managing_director_salaries` | a GmbH's shareholder-managing directors (GuV 6a) | 4124 | 6024 |
|
|
190
|
+
| `temporary_wages` | Aushilfslöhne (GuV 6a) | 4190 | 6030 |
|
|
191
|
+
| `minijob_wages` | Löhne für Minijobs (GuV 6a) | 4195 | 6035 |
|
|
192
|
+
| `benefits_in_kind` | Sachzuwendungen und Dienstleistungen an Arbeitnehmer (GuV 6a) | 4152 | 6072 |
|
|
193
|
+
| `capital_formation_benefits` | vermögenswirksame Leistungen (GuV 6a) | 4170 | 6080 |
|
|
194
|
+
| `commuting_allowances` | Fahrtkostenerstattung Wohnung/Arbeitsstätte (GuV 6a) | 4175 | 6090 |
|
|
195
|
+
| `minijob_flat_tax` | the flat tax of a Minijob (GuV 6a) | 4194 | 6036 |
|
|
196
|
+
| `employee_flat_tax` | the flat tax for employees (GuV 6a) | 4198 | 6039 |
|
|
197
|
+
| `temporary_flat_tax` | the flat tax for temporary staff (GuV 6a) | 4199 | 6040 |
|
|
198
|
+
| `employer_social_security` | the employer's contributions and the levies U1, U2, U3 (GuV 6b) | 4130 | 6110 |
|
|
199
|
+
| `minijob_social_security` | the flat contributions of a Minijob (GuV 6b) | 4144 | 6171 |
|
|
200
|
+
| `accident_insurance` | Berufsgenossenschaft (GuV 6b) | 4138 | 6120 |
|
|
201
|
+
| `pension` | Aufwendungen für Altersversorgung (GuV 6b) | 4165 | 6140 |
|
|
202
|
+
| `pension_flat_tax` | the flat tax on such benefits, Direktversicherungen (GuV 6b) | 4167 | 6147 |
|
|
203
|
+
| `company_car_benefit` | the private use of a company car, 19 % VAT, UStVA Kz 81 (GuV 4) | 8611 | 4947 |
|
|
204
|
+
| `levy_refunds` | Erstattungen nach dem Aufwendungsausgleichsgesetz (GuV 4) | 2749 | 4972 |
|
|
205
|
+
| `levy_refund_claims` | the claim on the health insurer for them | 1520 | 1369 |
|
|
206
|
+
| `net_pay` | the net pay owed | 1740 | 3720 |
|
|
207
|
+
| `wage_tax` | wage tax, church tax and Solidaritätszuschlag owed | 1741 | 3730 |
|
|
208
|
+
| `social_security` | the contributions owed | 1742 | 3740 |
|
|
209
|
+
| `employee_withholdings` | what is withheld from an employee for another (a garnishment) | 1748 | 3725 |
|
|
210
|
+
| `capital_formation_payable` | the savings an employee's contract receives | 1750 | 3770 |
|
|
211
|
+
| `estimated_contributions` | the contributions estimated when due (§ 23 Abs. 1 SGB IV) | 1759 | 3759 |
|
|
212
|
+
| `clearing` | the Lohn- und Gehaltsverrechnungskonto | 1755 | 3790 |
|
|
213
|
+
|
|
214
|
+
In the Anlage EÜR every payroll expense is Zeile 31 (Kz 120), "einschließlich
|
|
215
|
+
der gezahlten Lohnsteuer (auch Pauschalsteuer nach § 37b EStG) und anderer
|
|
216
|
+
Nebenkosten" (Anleitung 2026); the company-car benefit is a taxable receipt
|
|
217
|
+
(Kz 112), the levy refunds a non-taxable one (Kz 103). An individual account
|
|
218
|
+
takes no payroll role: a role names the DATEV account.
|
|
219
|
+
|
|
220
|
+
### Individual accounts
|
|
221
|
+
|
|
222
|
+
The books may set up accounts of their own (Individualkonten) on the numbers
|
|
223
|
+
the DATEV standard chart leaves free after one of its accounts. DATEV leaves
|
|
224
|
+
those numbers for the account's own sub-accounts and passes the account's
|
|
225
|
+
marks for its trade and corporate income tax programs ("G", "K") on to them, up
|
|
226
|
+
to the next account of the standard chart (DATEV Kontenrahmen, Erläuterungen,
|
|
227
|
+
S. 37). So an individual account belongs to the account it follows, its
|
|
228
|
+
parent, and is what the parent is: its type, its HGB position, its UStVA and
|
|
229
|
+
Anlage EÜR mapping, its account role, its tax rule and DATEV's mark "HB".
|
|
230
|
+
Where the declaration changes one of them deliberately (`overrides`: a value
|
|
231
|
+
replaces the parent's, `null` drops it), the provenance names it. It does not
|
|
232
|
+
take the parent's tax automatism or BU-key rules, which DATEV applies only to
|
|
233
|
+
the accounts it marks, nor its securities role, which names one account of a
|
|
234
|
+
chart: an individual account books a securities role only where its
|
|
235
|
+
declaration names one.
|
|
236
|
+
|
|
237
|
+
The declarations are the books'; this package stores none. The validator
|
|
238
|
+
refuses a declaration whose number:
|
|
239
|
+
|
|
240
|
+
- has no four digits;
|
|
241
|
+
- is one DATEV assigns or reserves, or one this package holds;
|
|
242
|
+
- another declaration takes too;
|
|
243
|
+
- has no name;
|
|
244
|
+
- has a parent this package does not hold, or a parent that is itself an
|
|
245
|
+
individual account;
|
|
246
|
+
- has a carry-forward, closing or statistical account as its parent;
|
|
247
|
+
- lies in another account class than its parent;
|
|
248
|
+
- does not follow its parent (another account of the chart stands between
|
|
249
|
+
them);
|
|
250
|
+
- takes a securities role another account books under the same tax rule;
|
|
251
|
+
- or has an override that names no HGB position, or one its type does not
|
|
252
|
+
take (a balance-sheet account has a balance-sheet position, a
|
|
253
|
+
profit-and-loss account an income-statement position), an Anlage EÜR line no
|
|
254
|
+
form year holds or a split line on the other side, or both a line and an
|
|
255
|
+
exclusion.
|
|
256
|
+
|
|
257
|
+
```ts
|
|
258
|
+
import { BooksChart, freeNumbersOf, individualAccountProblemsOf, type IIndividualAccountDeclaration } from '@fin.cx/chartdata';
|
|
259
|
+
|
|
260
|
+
freeNumbersOf('SKR03', 2026, '2700'); // ['2701', '2702', '2703', '2704']
|
|
261
|
+
freeNumbersOf('SKR04', 2026, '4905'); // []: DATEV's 4906 (§ 8b KStG) follows at once
|
|
262
|
+
|
|
263
|
+
const declarations: IIndividualAccountDeclaration[] = [
|
|
264
|
+
{ accountNumber: '2701', accountName: 'Erträge aus Termingeschäften', parentAccountNumber: '2700', securitiesRole: 'termingeschaeft_gain' },
|
|
265
|
+
{ accountNumber: '2301', accountName: 'Verluste aus Termingeschäften', parentAccountNumber: '2300', securitiesRole: 'termingeschaeft_loss' },
|
|
266
|
+
];
|
|
267
|
+
individualAccountProblemsOf('SKR03', 2026, declarations); // []
|
|
268
|
+
|
|
269
|
+
const books = BooksChart.of('SKR03', 2026, declarations); // throws IndividualAccountsError on problems
|
|
270
|
+
books.getSecuritiesAccount('termingeschaeft_gain')?.accountNumber; // '2701'
|
|
271
|
+
books.getAccount('2701')?.hgb275Position; // 'GuV 4', as 2700
|
|
272
|
+
books.getMappingCoverage(usedAccountNumbers, 'hgb275');
|
|
273
|
+
```
|
|
274
|
+
|
|
275
|
+
A `BooksChart` is a chart (`IChart`) with the lookups of the chart over the
|
|
276
|
+
DATEV accounts and the books' own: `getAccount`, `getAccountsByRole`,
|
|
277
|
+
`getAccountsBySecuritiesRole`, `getSecuritiesAccount`, `getPayrollAccount`,
|
|
278
|
+
`getMappingCoverage`, `getEuerLines`.
|
|
279
|
+
|
|
280
|
+
`INDIVIDUAL_ACCOUNT_TEMPLATES` suggests the parent and name of the results the
|
|
281
|
+
law keeps apart: the gains and losses of Termingeschäfte, which § 15 Abs. 4
|
|
282
|
+
Satz 3 EStG offsets only against each other, under the other operating income
|
|
283
|
+
and expenses, and beside them a writer's premium earned, a short sale's gain
|
|
284
|
+
and loss and the compensations paid on securities sold short. SKR 03 leaves
|
|
285
|
+
four numbers after 2700 and six after 2300, enough for all six; SKR 04 leaves
|
|
286
|
+
one after 4830 and one after 6300, so there the Termingeschäfte take them and
|
|
287
|
+
the books decide with their advisor where the others go. DATEV's ranges "zur
|
|
288
|
+
freien Verfügung" (SKR 03 8000–8099, SKR 04 4000–4099 and others) count as
|
|
289
|
+
assigned until this package holds the positions DATEV gives them.
|
|
290
|
+
|
|
145
291
|
### Anlage EÜR
|
|
146
292
|
|
|
147
293
|
Every account of a chart either maps to the Anlage EÜR or is excluded from it
|
|
@@ -227,9 +373,18 @@ on an account without an EÜR decision, and on a Kennzahl or side a form year
|
|
|
227
373
|
does not hold. E-Bilanz (de-gaap-ci) positions are not populated yet —
|
|
228
374
|
`getMappingCoverage(..., 'ebilanz')` reports them as missing by design.
|
|
229
375
|
|
|
376
|
+
The numbers the DATEV Kontenrahmen assigns and reserves (`getDatevNumbers`)
|
|
377
|
+
are read from the text of the SKR 03 and SKR 04 PDFs, gültig für 2026, as
|
|
378
|
+
`pdfjs` lays it out (one line per row, each item with its x position; the
|
|
379
|
+
extraction script and the texts stay in the untracked `.nogit/`): every
|
|
380
|
+
number DATEV lists, with the marks before it, the groups it lists under one
|
|
381
|
+
designation ("AM 8400" … "-09") and the reserved ("R"). The tool checks that
|
|
382
|
+
every account this package holds from the Kontenrahmen is among them.
|
|
383
|
+
|
|
230
384
|
The generation pipeline lives in the unpublished `tools/` directory
|
|
231
|
-
(`tsx tools/generate-charts.ts <oracle csvs
|
|
232
|
-
extend the curated table in the tool, never
|
|
385
|
+
(`tsx tools/generate-charts.ts <oracle csvs>`, `tsx tools/generate-datev-numbers.ts <skr03 text> <skr04 text>`);
|
|
386
|
+
datasets are generated files — extend the curated table in the tool, never
|
|
387
|
+
edit `ts/data/*` by hand.
|
|
233
388
|
|
|
234
389
|
## Development
|
|
235
390
|
|
package/ts/00_commitinfo_data.ts
CHANGED
|
@@ -3,6 +3,6 @@
|
|
|
3
3
|
*/
|
|
4
4
|
export const commitinfo = {
|
|
5
5
|
name: '@fin.cx/chartdata',
|
|
6
|
-
version: '
|
|
6
|
+
version: '3.0.0',
|
|
7
7
|
description: 'Year-versioned German SKR03/SKR04 chart-of-accounts datasets with Automatikkonto functions, BU-key rules, UStVA-Kennzahl and Anlage EÜR mappings'
|
|
8
8
|
}
|
package/ts/chartdata.hgb.ts
CHANGED
|
@@ -65,7 +65,9 @@ export const HGB_275_GKV_POSITIONS: IHgbPosition[] = [
|
|
|
65
65
|
{ key: 'GuV 3', label: 'Andere aktivierte Eigenleistungen' },
|
|
66
66
|
{ key: 'GuV 4', label: 'Sonstige betriebliche Erträge' },
|
|
67
67
|
{ key: 'GuV 5', label: 'Materialaufwand' },
|
|
68
|
-
|
|
68
|
+
// Nr. 6 Personalaufwand, by its two parts
|
|
69
|
+
{ key: 'GuV 6a', label: 'Löhne und Gehälter' },
|
|
70
|
+
{ key: 'GuV 6b', label: 'Soziale Abgaben und Aufwendungen für Altersversorgung und für Unterstützung' },
|
|
69
71
|
{ key: 'GuV 7', label: 'Abschreibungen' },
|
|
70
72
|
{ key: 'GuV 8', label: 'Sonstige betriebliche Aufwendungen' },
|
|
71
73
|
{ key: 'GuV 9', label: 'Erträge aus Beteiligungen' },
|
|
@@ -0,0 +1,321 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
IChart,
|
|
3
|
+
IChartAccount,
|
|
4
|
+
IDatevNumbers,
|
|
5
|
+
IIndividualAccountDeclaration,
|
|
6
|
+
IIndividualAccountProblem,
|
|
7
|
+
IIndividualAccountTemplate,
|
|
8
|
+
IMappingCoverage,
|
|
9
|
+
TAccountRole,
|
|
10
|
+
TChartName,
|
|
11
|
+
TEuerAccountLines,
|
|
12
|
+
TMappingDimension,
|
|
13
|
+
TPayrollRole,
|
|
14
|
+
TSecuritiesRole,
|
|
15
|
+
TTaxRule,
|
|
16
|
+
} from './chartdata.types.js';
|
|
17
|
+
import { euerLinesOf, getEuerFormLine, listEuerFormYears, type IEuerLinesOptions } from './chartdata.euer.js';
|
|
18
|
+
import { HGB_266_POSITIONS, HGB_275_GKV_POSITIONS } from './chartdata.hgb.js';
|
|
19
|
+
import { getChart, mappingCoverageOf } from './chartdata.lookup.js';
|
|
20
|
+
import { DATEV_NUMBERS_SKR03_2026, DATEV_NUMBERS_SKR04_2026 } from './data/datevnumbers.2026.js';
|
|
21
|
+
|
|
22
|
+
const DATEV_NUMBERS: IDatevNumbers[] = [DATEV_NUMBERS_SKR03_2026, DATEV_NUMBERS_SKR04_2026];
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* The numbers the DATEV standard chart of a year assigns and reserves, as read from the DATEV Kontenrahmen. Throws
|
|
26
|
+
* for an unknown chart or year, as `getChart`.
|
|
27
|
+
*/
|
|
28
|
+
export function getDatevNumbers(chart: TChartName, year: number): IDatevNumbers {
|
|
29
|
+
const found = DATEV_NUMBERS.find((numbers) => numbers.chart === chart && numbers.year === year);
|
|
30
|
+
if (!found) {
|
|
31
|
+
throw new Error(`No DATEV numbers of ${chart} for year ${year}`);
|
|
32
|
+
}
|
|
33
|
+
return found;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** Whether DATEV assigns or reserves the number in the chart of the year. */
|
|
37
|
+
export function isDatevNumber(chart: TChartName, year: number, accountNumber: string): boolean {
|
|
38
|
+
return getDatevNumbers(chart, year).assigned.includes(accountNumber);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Suggestions for the individual accounts of the results the law keeps apart, which DATEV's standard charts hold
|
|
43
|
+
* no account of their own for: the results of Termingeschäfte, whose losses § 15 Abs. 4 Satz 3 EStG offsets only
|
|
44
|
+
* against such gains, under the other operating income and expenses; beside them a writer's premium earned, a
|
|
45
|
+
* short sale's gain and loss and the compensations paid on securities sold short, which an advisor should see
|
|
46
|
+
* apart. Each follows its parent in the free numbers DATEV leaves after it (`freeNumbersOf`): SKR 03 leaves four
|
|
47
|
+
* after 2700 and six after 2300, SKR 04 one after 4830 and one after 6300, so in SKR 04 the books give the
|
|
48
|
+
* Termingeschäfte those two and choose with their advisor where the others go. The books choose the numbers.
|
|
49
|
+
*/
|
|
50
|
+
export const INDIVIDUAL_ACCOUNT_TEMPLATES: Readonly<Record<TChartName, readonly IIndividualAccountTemplate[]>> = {
|
|
51
|
+
SKR03: templates('2700', '2300'),
|
|
52
|
+
SKR04: templates('4830', '6300'),
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
function templates(otherIncomeArg: string, otherExpenseArg: string): IIndividualAccountTemplate[] {
|
|
56
|
+
const of = (securitiesRole: TSecuritiesRole, parentAccountNumber: string, accountName: string): IIndividualAccountTemplate => ({ securitiesRole, parentAccountNumber, accountName });
|
|
57
|
+
return [
|
|
58
|
+
of('termingeschaeft_gain', otherIncomeArg, 'Erträge aus Termingeschäften (§ 15 Abs. 4 Satz 3 EStG)'),
|
|
59
|
+
of('termingeschaeft_loss', otherExpenseArg, 'Verluste aus Termingeschäften (§ 15 Abs. 4 Satz 3 EStG)'),
|
|
60
|
+
of('stillhalter_income', otherIncomeArg, 'Erträge aus Stillhalterprämien'),
|
|
61
|
+
of('short_sale_gain', otherIncomeArg, 'Erträge aus Leerverkäufen'),
|
|
62
|
+
of('short_sale_loss', otherExpenseArg, 'Aufwendungen aus Leerverkäufen'),
|
|
63
|
+
of('compensation_paid', otherExpenseArg, 'Kompensationszahlungen aus Leerverkäufen'),
|
|
64
|
+
];
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* The numbers free for an individual account under a parent: those after it in its account class up to the next
|
|
69
|
+
* account of the chart (DATEV's or chartdata's), less those the books' declarations take, in order. Throws for an
|
|
70
|
+
* unknown chart, year or parent.
|
|
71
|
+
*/
|
|
72
|
+
export function freeNumbersOf(
|
|
73
|
+
chart: TChartName,
|
|
74
|
+
year: number,
|
|
75
|
+
parentAccountNumber: string,
|
|
76
|
+
declarations: readonly IIndividualAccountDeclaration[] = [],
|
|
77
|
+
): string[] {
|
|
78
|
+
const standard = getChart(chart, year);
|
|
79
|
+
if (!standard.accounts.some((account) => account.accountNumber === parentAccountNumber)) {
|
|
80
|
+
throw new Error(`${chart} ${year} holds no account ${parentAccountNumber}`);
|
|
81
|
+
}
|
|
82
|
+
const taken = new Set(declarations.map((declaration) => declaration.accountNumber));
|
|
83
|
+
return followingNumbersOf(chart, year, parentAccountNumber).filter((number) => !taken.has(number));
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/** The numbers of the chart's accounts, DATEV's and chartdata's. */
|
|
87
|
+
function occupiedNumbersOf(chartArg: TChartName, yearArg: number): Set<string> {
|
|
88
|
+
return new Set([...getDatevNumbers(chartArg, yearArg).assigned, ...getChart(chartArg, yearArg).accounts.map((account) => account.accountNumber)]);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/** The account of the chart a number above another account follows: the next below it. */
|
|
92
|
+
function precedingNumberOf(chartArg: TChartName, yearArg: number, numberArg: string): string {
|
|
93
|
+
const occupied = occupiedNumbersOf(chartArg, yearArg);
|
|
94
|
+
let value = Number(numberArg) - 1;
|
|
95
|
+
while (!occupied.has(String(value).padStart(4, '0'))) value--;
|
|
96
|
+
return String(value).padStart(4, '0');
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/** The numbers after an account in its class up to the next account of the chart, DATEV's or chartdata's. */
|
|
100
|
+
function followingNumbersOf(chartArg: TChartName, yearArg: number, accountNumberArg: string): string[] {
|
|
101
|
+
const occupied = occupiedNumbersOf(chartArg, yearArg);
|
|
102
|
+
const following: string[] = [];
|
|
103
|
+
for (let value = Number(accountNumberArg) + 1; value <= 9999; value++) {
|
|
104
|
+
const number = String(value).padStart(4, '0');
|
|
105
|
+
if (occupied.has(number) || number[0] !== accountNumberArg[0]) break;
|
|
106
|
+
following.push(number);
|
|
107
|
+
}
|
|
108
|
+
return following;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/** Everything that keeps the books' individual accounts from being accounts of the chart; an empty list means none. */
|
|
112
|
+
export function individualAccountProblemsOf(
|
|
113
|
+
chart: TChartName,
|
|
114
|
+
year: number,
|
|
115
|
+
declarations: readonly IIndividualAccountDeclaration[],
|
|
116
|
+
): IIndividualAccountProblem[] {
|
|
117
|
+
const standard = getChart(chart, year);
|
|
118
|
+
const byNumber = new Map(standard.accounts.map((account) => [account.accountNumber, account]));
|
|
119
|
+
const assigned = new Set(getDatevNumbers(chart, year).assigned);
|
|
120
|
+
const declared = new Map<string, number>();
|
|
121
|
+
for (const declaration of declarations) declared.set(declaration.accountNumber, (declared.get(declaration.accountNumber) ?? 0) + 1);
|
|
122
|
+
const problems: IIndividualAccountProblem[] = [];
|
|
123
|
+
const add = (accountNumber: string, code: IIndividualAccountProblem['code'], message: string): void => {
|
|
124
|
+
problems.push({ accountNumber, code, message });
|
|
125
|
+
};
|
|
126
|
+
// the securities roles and tax rules the chart's own accounts book
|
|
127
|
+
const roles = new Map<string, string>();
|
|
128
|
+
for (const account of standard.accounts) {
|
|
129
|
+
if (account.securitiesRole) roles.set(`${account.securitiesRole}:${account.taxRule ?? ''}`, account.accountNumber);
|
|
130
|
+
}
|
|
131
|
+
for (const declaration of declarations) {
|
|
132
|
+
const number = declaration.accountNumber;
|
|
133
|
+
if (!/^\d{4}$/.test(number)) {
|
|
134
|
+
add(number, 'number_invalid', `${number} is no account number of four digits`);
|
|
135
|
+
continue;
|
|
136
|
+
}
|
|
137
|
+
if (assigned.has(number)) add(number, 'number_assigned', `DATEV assigns or reserves ${number} in ${chart} ${year}`);
|
|
138
|
+
else if (byNumber.has(number)) add(number, 'number_assigned', `${chart} ${year} holds an account ${number}`);
|
|
139
|
+
if ((declared.get(number) ?? 0) > 1) add(number, 'number_duplicate', `${number} is declared more than once`);
|
|
140
|
+
if (declaration.accountName.trim() === '') add(number, 'name_missing', `${number} has no name`);
|
|
141
|
+
const parent = byNumber.get(declaration.parentAccountNumber);
|
|
142
|
+
if (!parent) {
|
|
143
|
+
add(
|
|
144
|
+
number,
|
|
145
|
+
declared.has(declaration.parentAccountNumber) ? 'parent_individual' : 'parent_unknown',
|
|
146
|
+
declared.has(declaration.parentAccountNumber)
|
|
147
|
+
? `the parent ${declaration.parentAccountNumber} is itself an individual account; an individual account belongs to a DATEV account`
|
|
148
|
+
: `${chart} ${year} holds no account ${declaration.parentAccountNumber}`,
|
|
149
|
+
);
|
|
150
|
+
continue;
|
|
151
|
+
}
|
|
152
|
+
if (parent.euerExclusion === 'closing') {
|
|
153
|
+
add(number, 'parent_not_allowed', `the parent ${parent.accountNumber} (${parent.accountName}) is a carry-forward, closing or statistical account`);
|
|
154
|
+
}
|
|
155
|
+
if (number[0] !== parent.accountNumber[0]) {
|
|
156
|
+
add(number, 'class_mismatch', `${number} lies in account class ${number[0]}, its parent ${parent.accountNumber} in class ${parent.accountNumber[0]}`);
|
|
157
|
+
} else if (!assigned.has(number) && !byNumber.has(number) && !followingNumbersOf(chart, year, parent.accountNumber).includes(number)) {
|
|
158
|
+
add(
|
|
159
|
+
number,
|
|
160
|
+
'not_following_parent',
|
|
161
|
+
number < parent.accountNumber
|
|
162
|
+
? `${number} lies before its parent ${parent.accountNumber}; an individual account follows it`
|
|
163
|
+
: `${number} follows ${precedingNumberOf(chart, year, number)}, not its parent ${parent.accountNumber}: DATEV passes the marks of the account a number follows on to it`,
|
|
164
|
+
);
|
|
165
|
+
}
|
|
166
|
+
const overrides = declaration.overrides ?? {};
|
|
167
|
+
const profitAndLoss = parent.accountType === 'revenue' || parent.accountType === 'expense';
|
|
168
|
+
if (typeof overrides.hgb266Position === 'string') {
|
|
169
|
+
if (!HGB_266_POSITIONS.some((position) => position.key === overrides.hgb266Position)) {
|
|
170
|
+
add(number, 'override_invalid', `${overrides.hgb266Position} is no balance-sheet position of § 266 HGB`);
|
|
171
|
+
} else if (profitAndLoss) {
|
|
172
|
+
add(number, 'override_invalid', `a balance-sheet position for ${number}, whose parent ${parent.accountNumber} is a profit-and-loss account (${parent.accountType})`);
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
if (typeof overrides.hgb275Position === 'string') {
|
|
176
|
+
if (!HGB_275_GKV_POSITIONS.some((position) => position.key === overrides.hgb275Position)) {
|
|
177
|
+
add(number, 'override_invalid', `${overrides.hgb275Position} is no income-statement position of § 275 Abs. 2 HGB`);
|
|
178
|
+
} else if (!profitAndLoss) {
|
|
179
|
+
add(number, 'override_invalid', `an income-statement position for ${number}, whose parent ${parent.accountNumber} is a balance-sheet account (${parent.accountType})`);
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
if (overrides.euer !== undefined && overrides.euerExclusion !== undefined) {
|
|
183
|
+
add(number, 'override_invalid', 'an account maps to the Anlage EÜR or is excluded from it, not both');
|
|
184
|
+
}
|
|
185
|
+
if (overrides.euer !== undefined) {
|
|
186
|
+
// a line of every form year; a split's debit on a line that counts debits, its credit on one that counts credits
|
|
187
|
+
const lines: Array<[number, 'debit' | 'credit' | undefined]> = 'kennzahl' in overrides.euer
|
|
188
|
+
? [[overrides.euer.kennzahl, undefined]]
|
|
189
|
+
: [[overrides.euer.debitKennzahl, 'debit'], [overrides.euer.creditKennzahl, 'credit']];
|
|
190
|
+
for (const [kennzahl, side] of lines) {
|
|
191
|
+
for (const formYear of listEuerFormYears()) {
|
|
192
|
+
let line: ReturnType<typeof getEuerFormLine>;
|
|
193
|
+
try {
|
|
194
|
+
line = getEuerFormLine(formYear, kennzahl);
|
|
195
|
+
} catch {
|
|
196
|
+
add(number, 'override_invalid', `Kz ${kennzahl} is no line of the Anlage EÜR ${formYear}`);
|
|
197
|
+
continue;
|
|
198
|
+
}
|
|
199
|
+
if (side !== undefined && line.side !== side) {
|
|
200
|
+
add(number, 'override_invalid', `Kz ${kennzahl} counts ${line.side}s in the Anlage EÜR ${formYear}, not the ${side}s of a split`);
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
if (declaration.securitiesRole !== undefined) {
|
|
206
|
+
const taxRule = overrides.taxRule === undefined ? parent.taxRule : overrides.taxRule ?? undefined;
|
|
207
|
+
const key = `${declaration.securitiesRole}:${taxRule ?? ''}`;
|
|
208
|
+
const holder = roles.get(key);
|
|
209
|
+
if (holder !== undefined) add(number, 'role_taken', `${holder} already books ${declaration.securitiesRole}${taxRule ? ` under ${taxRule}` : ''}`);
|
|
210
|
+
else roles.set(key, number);
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
return problems;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
/** Thrown where the books' individual accounts cannot be accounts of the chart; `problems` says why. */
|
|
217
|
+
export class IndividualAccountsError extends Error {
|
|
218
|
+
constructor(public readonly problems: IIndividualAccountProblem[]) {
|
|
219
|
+
super(`individual accounts refused: ${problems.map((problem) => `${problem.accountNumber} ${problem.code}`).join(', ')}`);
|
|
220
|
+
this.name = 'IndividualAccountsError';
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
/** An individual account as an account of the chart: what its parent is, as its declaration changes it. */
|
|
225
|
+
function individualAccountOf(parentArg: IChartAccount, declarationArg: IIndividualAccountDeclaration, yearArg: number): IChartAccount {
|
|
226
|
+
const overrides = declarationArg.overrides ?? {};
|
|
227
|
+
const pick = <T>(inherited: T | undefined, override: T | null | undefined): T | undefined => (override === undefined ? inherited : override ?? undefined);
|
|
228
|
+
const hgb266Position = pick(parentArg.hgb266Position, overrides.hgb266Position);
|
|
229
|
+
const hgb275Position = pick(parentArg.hgb275Position, overrides.hgb275Position);
|
|
230
|
+
const ustva = pick(parentArg.ustva, overrides.ustva);
|
|
231
|
+
const accountRole = pick<TAccountRole>(parentArg.accountRole, overrides.accountRole);
|
|
232
|
+
const taxRule = pick<TTaxRule>(parentArg.taxRule, overrides.taxRule);
|
|
233
|
+
const commercialOnly = pick<true>(parentArg.commercialOnly, overrides.commercialOnly);
|
|
234
|
+
const euerFromOverride = overrides.euer !== undefined || overrides.euerExclusion !== undefined;
|
|
235
|
+
const euer = euerFromOverride ? overrides.euer : parentArg.euer;
|
|
236
|
+
const euerExclusion = euerFromOverride ? overrides.euerExclusion : parentArg.euerExclusion;
|
|
237
|
+
return {
|
|
238
|
+
accountNumber: declarationArg.accountNumber,
|
|
239
|
+
accountName: declarationArg.accountName,
|
|
240
|
+
accountClass: Number.parseInt(declarationArg.accountNumber[0]!, 10),
|
|
241
|
+
accountType: parentArg.accountType,
|
|
242
|
+
...(accountRole ? { accountRole } : {}),
|
|
243
|
+
...(declarationArg.securitiesRole ? { securitiesRole: declarationArg.securitiesRole } : {}),
|
|
244
|
+
...(taxRule ? { taxRule } : {}),
|
|
245
|
+
...(commercialOnly ? { commercialOnly } : {}),
|
|
246
|
+
...(ustva ? { ustva } : {}),
|
|
247
|
+
...(hgb266Position ? { hgb266Position } : {}),
|
|
248
|
+
...(hgb275Position ? { hgb275Position } : {}),
|
|
249
|
+
...(parentArg.ebilanzPosition ? { ebilanzPosition: parentArg.ebilanzPosition } : {}),
|
|
250
|
+
...(euer ? { euer } : {}),
|
|
251
|
+
...(euerExclusion ? { euerExclusion } : {}),
|
|
252
|
+
parentAccountNumber: parentArg.accountNumber,
|
|
253
|
+
provenance: `individual account of the books under ${parentArg.accountNumber} (${parentArg.accountName})${Object.keys(overrides).length > 0 ? `; overrides ${Object.keys(overrides).sort().join(', ')}` : ''}`,
|
|
254
|
+
validFrom: yearArg,
|
|
255
|
+
};
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
/**
|
|
259
|
+
* A chart of a year with the books' individual accounts: the lookups of the chart over its DATEV accounts and the
|
|
260
|
+
* books' own. Built by `BooksChart.of`, which refuses declarations with problems (`IndividualAccountsError`).
|
|
261
|
+
*/
|
|
262
|
+
export class BooksChart implements IChart {
|
|
263
|
+
public readonly chart: TChartName;
|
|
264
|
+
public readonly year: number;
|
|
265
|
+
public readonly accounts: IChartAccount[];
|
|
266
|
+
public readonly individualAccounts: IChartAccount[];
|
|
267
|
+
private readonly byNumber: Map<string, IChartAccount>;
|
|
268
|
+
|
|
269
|
+
private constructor(chartArg: TChartName, yearArg: number, individualArg: IChartAccount[]) {
|
|
270
|
+
this.chart = chartArg;
|
|
271
|
+
this.year = yearArg;
|
|
272
|
+
this.individualAccounts = individualArg;
|
|
273
|
+
this.accounts = [...getChart(chartArg, yearArg).accounts, ...individualArg].sort((a, b) => a.accountNumber.localeCompare(b.accountNumber));
|
|
274
|
+
this.byNumber = new Map(this.accounts.map((account) => [account.accountNumber, account]));
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
/** The chart of the year with the books' individual accounts; throws `IndividualAccountsError` for declarations with problems. */
|
|
278
|
+
public static of(chartArg: TChartName, yearArg: number, declarationsArg: readonly IIndividualAccountDeclaration[] = []): BooksChart {
|
|
279
|
+
const problems = individualAccountProblemsOf(chartArg, yearArg, declarationsArg);
|
|
280
|
+
if (problems.length > 0) throw new IndividualAccountsError(problems);
|
|
281
|
+
const standard = getChart(chartArg, yearArg);
|
|
282
|
+
const parents = new Map(standard.accounts.map((account) => [account.accountNumber, account]));
|
|
283
|
+
return new BooksChart(
|
|
284
|
+
chartArg,
|
|
285
|
+
yearArg,
|
|
286
|
+
declarationsArg.map((declaration) => individualAccountOf(parents.get(declaration.parentAccountNumber)!, declaration, yearArg)),
|
|
287
|
+
);
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
/** The account of the number, a DATEV account or one of the books; `null` where there is none. */
|
|
291
|
+
public getAccount(accountNumberArg: string): IChartAccount | null {
|
|
292
|
+
return this.byNumber.get(accountNumberArg) ?? null;
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
public getAccountsByRole(roleArg: TAccountRole): IChartAccount[] {
|
|
296
|
+
return this.accounts.filter((account) => account.accountRole === roleArg);
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
public getAccountsBySecuritiesRole(roleArg: TSecuritiesRole): IChartAccount[] {
|
|
300
|
+
return this.accounts.filter((account) => account.securitiesRole === roleArg);
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
/** The one account of the books that books the securities role under the tax rule (none: the account without one), or `null`. */
|
|
304
|
+
public getSecuritiesAccount(roleArg: TSecuritiesRole, taxRuleArg?: TTaxRule): IChartAccount | null {
|
|
305
|
+
return this.getAccountsBySecuritiesRole(roleArg).find((account) => account.taxRule === taxRuleArg) ?? null;
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
/** The one account of the books that books the payroll role, or `null`; an individual account takes none. */
|
|
309
|
+
public getPayrollAccount(roleArg: TPayrollRole): IChartAccount | null {
|
|
310
|
+
return this.accounts.find((account) => account.payrollRole === roleArg) ?? null;
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
public getMappingCoverage(usedAccountsArg: string[], dimensionArg: TMappingDimension): IMappingCoverage {
|
|
314
|
+
return mappingCoverageOf(this.accounts, usedAccountsArg, dimensionArg);
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
public getEuerLines(accountNumberArg: string, formYearArg: number, optionsArg: IEuerLinesOptions = {}): TEuerAccountLines | null {
|
|
318
|
+
const account = this.getAccount(accountNumberArg);
|
|
319
|
+
return account ? euerLinesOf(account, formYearArg, optionsArg) : null;
|
|
320
|
+
}
|
|
321
|
+
}
|
package/ts/chartdata.lookup.ts
CHANGED
|
@@ -7,6 +7,7 @@ import type {
|
|
|
7
7
|
TChartName,
|
|
8
8
|
TEuerAccountLines,
|
|
9
9
|
TMappingDimension,
|
|
10
|
+
TPayrollRole,
|
|
10
11
|
TSecuritiesRole,
|
|
11
12
|
TTaxRule,
|
|
12
13
|
} from './chartdata.types.js';
|
|
@@ -113,6 +114,14 @@ export function getSecuritiesAccount(
|
|
|
113
114
|
return getAccountsBySecuritiesRole(chart, year, role).find((account) => account.taxRule === taxRule) ?? null;
|
|
114
115
|
}
|
|
115
116
|
|
|
117
|
+
/**
|
|
118
|
+
* The one account of a chart that books the payroll role (`IChartAccount.payrollRole`), or `null` where the chart
|
|
119
|
+
* keeps none for it. Throws for unknown chart/year combinations, as `getChart`.
|
|
120
|
+
*/
|
|
121
|
+
export function getPayrollAccount(chart: TChartName, year: number, role: TPayrollRole): IChartAccount | null {
|
|
122
|
+
return getChart(chart, year).accounts.find((account) => account.payrollRole === role) ?? null;
|
|
123
|
+
}
|
|
124
|
+
|
|
116
125
|
function hasMapping(account: IChartAccount, dimension: TMappingDimension): boolean {
|
|
117
126
|
switch (dimension) {
|
|
118
127
|
case 'ustva':
|
|
@@ -138,8 +147,16 @@ export function getMappingCoverage(
|
|
|
138
147
|
usedAccounts: string[],
|
|
139
148
|
dimension: TMappingDimension,
|
|
140
149
|
): IMappingCoverage {
|
|
141
|
-
|
|
142
|
-
|
|
150
|
+
return mappingCoverageOf(getChart(chart, year).accounts, usedAccounts, dimension);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/** `getMappingCoverage` over a list of accounts (a chart's, or a chart's with the books' individual accounts). */
|
|
154
|
+
export function mappingCoverageOf(
|
|
155
|
+
accounts: readonly IChartAccount[],
|
|
156
|
+
usedAccounts: string[],
|
|
157
|
+
dimension: TMappingDimension,
|
|
158
|
+
): IMappingCoverage {
|
|
159
|
+
const byNumber = new Map(accounts.map((account) => [account.accountNumber, account]));
|
|
143
160
|
const missing: string[] = [];
|
|
144
161
|
const unknown: string[] = [];
|
|
145
162
|
let covered = 0;
|