@fin.cx/chartdata 2.15.0 → 2.17.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.
@@ -0,0 +1,315 @@
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
+ TSecuritiesRole,
14
+ TTaxRule,
15
+ } from './chartdata.types.js';
16
+ import { euerLinesOf, getEuerFormLine, listEuerFormYears, type IEuerLinesOptions } from './chartdata.euer.js';
17
+ import { HGB_266_POSITIONS, HGB_275_GKV_POSITIONS } from './chartdata.hgb.js';
18
+ import { getChart, mappingCoverageOf } from './chartdata.lookup.js';
19
+ import { DATEV_NUMBERS_SKR03_2026, DATEV_NUMBERS_SKR04_2026 } from './data/datevnumbers.2026.js';
20
+
21
+ const DATEV_NUMBERS: IDatevNumbers[] = [DATEV_NUMBERS_SKR03_2026, DATEV_NUMBERS_SKR04_2026];
22
+
23
+ /**
24
+ * The numbers the DATEV standard chart of a year assigns and reserves, as read from the DATEV Kontenrahmen. Throws
25
+ * for an unknown chart or year, as `getChart`.
26
+ */
27
+ export function getDatevNumbers(chart: TChartName, year: number): IDatevNumbers {
28
+ const found = DATEV_NUMBERS.find((numbers) => numbers.chart === chart && numbers.year === year);
29
+ if (!found) {
30
+ throw new Error(`No DATEV numbers of ${chart} for year ${year}`);
31
+ }
32
+ return found;
33
+ }
34
+
35
+ /** Whether DATEV assigns or reserves the number in the chart of the year. */
36
+ export function isDatevNumber(chart: TChartName, year: number, accountNumber: string): boolean {
37
+ return getDatevNumbers(chart, year).assigned.includes(accountNumber);
38
+ }
39
+
40
+ /**
41
+ * Suggestions for the individual accounts of the results the law keeps apart, which DATEV's standard charts hold
42
+ * no account of their own for: the results of Termingeschäfte, whose losses § 15 Abs. 4 Satz 3 EStG offsets only
43
+ * against such gains, under the other operating income and expenses; beside them a writer's premium earned, a
44
+ * short sale's gain and loss and the compensations paid on securities sold short, which an advisor should see
45
+ * apart. Each follows its parent in the free numbers DATEV leaves after it (`freeNumbersOf`): SKR 03 leaves four
46
+ * after 2700 and six after 2300, SKR 04 one after 4830 and one after 6300, so in SKR 04 the books give the
47
+ * Termingeschäfte those two and choose with their advisor where the others go. The books choose the numbers.
48
+ */
49
+ export const INDIVIDUAL_ACCOUNT_TEMPLATES: Readonly<Record<TChartName, readonly IIndividualAccountTemplate[]>> = {
50
+ SKR03: templates('2700', '2300'),
51
+ SKR04: templates('4830', '6300'),
52
+ };
53
+
54
+ function templates(otherIncomeArg: string, otherExpenseArg: string): IIndividualAccountTemplate[] {
55
+ const of = (securitiesRole: TSecuritiesRole, parentAccountNumber: string, accountName: string): IIndividualAccountTemplate => ({ securitiesRole, parentAccountNumber, accountName });
56
+ return [
57
+ of('termingeschaeft_gain', otherIncomeArg, 'Erträge aus Termingeschäften (§ 15 Abs. 4 Satz 3 EStG)'),
58
+ of('termingeschaeft_loss', otherExpenseArg, 'Verluste aus Termingeschäften (§ 15 Abs. 4 Satz 3 EStG)'),
59
+ of('stillhalter_income', otherIncomeArg, 'Erträge aus Stillhalterprämien'),
60
+ of('short_sale_gain', otherIncomeArg, 'Erträge aus Leerverkäufen'),
61
+ of('short_sale_loss', otherExpenseArg, 'Aufwendungen aus Leerverkäufen'),
62
+ of('compensation_paid', otherExpenseArg, 'Kompensationszahlungen aus Leerverkäufen'),
63
+ ];
64
+ }
65
+
66
+ /**
67
+ * The numbers free for an individual account under a parent: those after it in its account class up to the next
68
+ * account of the chart (DATEV's or chartdata's), less those the books' declarations take, in order. Throws for an
69
+ * unknown chart, year or parent.
70
+ */
71
+ export function freeNumbersOf(
72
+ chart: TChartName,
73
+ year: number,
74
+ parentAccountNumber: string,
75
+ declarations: readonly IIndividualAccountDeclaration[] = [],
76
+ ): string[] {
77
+ const standard = getChart(chart, year);
78
+ if (!standard.accounts.some((account) => account.accountNumber === parentAccountNumber)) {
79
+ throw new Error(`${chart} ${year} holds no account ${parentAccountNumber}`);
80
+ }
81
+ const taken = new Set(declarations.map((declaration) => declaration.accountNumber));
82
+ return followingNumbersOf(chart, year, parentAccountNumber).filter((number) => !taken.has(number));
83
+ }
84
+
85
+ /** The numbers of the chart's accounts, DATEV's and chartdata's. */
86
+ function occupiedNumbersOf(chartArg: TChartName, yearArg: number): Set<string> {
87
+ return new Set([...getDatevNumbers(chartArg, yearArg).assigned, ...getChart(chartArg, yearArg).accounts.map((account) => account.accountNumber)]);
88
+ }
89
+
90
+ /** The account of the chart a number above another account follows: the next below it. */
91
+ function precedingNumberOf(chartArg: TChartName, yearArg: number, numberArg: string): string {
92
+ const occupied = occupiedNumbersOf(chartArg, yearArg);
93
+ let value = Number(numberArg) - 1;
94
+ while (!occupied.has(String(value).padStart(4, '0'))) value--;
95
+ return String(value).padStart(4, '0');
96
+ }
97
+
98
+ /** The numbers after an account in its class up to the next account of the chart, DATEV's or chartdata's. */
99
+ function followingNumbersOf(chartArg: TChartName, yearArg: number, accountNumberArg: string): string[] {
100
+ const occupied = occupiedNumbersOf(chartArg, yearArg);
101
+ const following: string[] = [];
102
+ for (let value = Number(accountNumberArg) + 1; value <= 9999; value++) {
103
+ const number = String(value).padStart(4, '0');
104
+ if (occupied.has(number) || number[0] !== accountNumberArg[0]) break;
105
+ following.push(number);
106
+ }
107
+ return following;
108
+ }
109
+
110
+ /** Everything that keeps the books' individual accounts from being accounts of the chart; an empty list means none. */
111
+ export function individualAccountProblemsOf(
112
+ chart: TChartName,
113
+ year: number,
114
+ declarations: readonly IIndividualAccountDeclaration[],
115
+ ): IIndividualAccountProblem[] {
116
+ const standard = getChart(chart, year);
117
+ const byNumber = new Map(standard.accounts.map((account) => [account.accountNumber, account]));
118
+ const assigned = new Set(getDatevNumbers(chart, year).assigned);
119
+ const declared = new Map<string, number>();
120
+ for (const declaration of declarations) declared.set(declaration.accountNumber, (declared.get(declaration.accountNumber) ?? 0) + 1);
121
+ const problems: IIndividualAccountProblem[] = [];
122
+ const add = (accountNumber: string, code: IIndividualAccountProblem['code'], message: string): void => {
123
+ problems.push({ accountNumber, code, message });
124
+ };
125
+ // the securities roles and tax rules the chart's own accounts book
126
+ const roles = new Map<string, string>();
127
+ for (const account of standard.accounts) {
128
+ if (account.securitiesRole) roles.set(`${account.securitiesRole}:${account.taxRule ?? ''}`, account.accountNumber);
129
+ }
130
+ for (const declaration of declarations) {
131
+ const number = declaration.accountNumber;
132
+ if (!/^\d{4}$/.test(number)) {
133
+ add(number, 'number_invalid', `${number} is no account number of four digits`);
134
+ continue;
135
+ }
136
+ if (assigned.has(number)) add(number, 'number_assigned', `DATEV assigns or reserves ${number} in ${chart} ${year}`);
137
+ else if (byNumber.has(number)) add(number, 'number_assigned', `${chart} ${year} holds an account ${number}`);
138
+ if ((declared.get(number) ?? 0) > 1) add(number, 'number_duplicate', `${number} is declared more than once`);
139
+ if (declaration.accountName.trim() === '') add(number, 'name_missing', `${number} has no name`);
140
+ const parent = byNumber.get(declaration.parentAccountNumber);
141
+ if (!parent) {
142
+ add(
143
+ number,
144
+ declared.has(declaration.parentAccountNumber) ? 'parent_individual' : 'parent_unknown',
145
+ declared.has(declaration.parentAccountNumber)
146
+ ? `the parent ${declaration.parentAccountNumber} is itself an individual account; an individual account belongs to a DATEV account`
147
+ : `${chart} ${year} holds no account ${declaration.parentAccountNumber}`,
148
+ );
149
+ continue;
150
+ }
151
+ if (parent.euerExclusion === 'closing') {
152
+ add(number, 'parent_not_allowed', `the parent ${parent.accountNumber} (${parent.accountName}) is a carry-forward, closing or statistical account`);
153
+ }
154
+ if (number[0] !== parent.accountNumber[0]) {
155
+ add(number, 'class_mismatch', `${number} lies in account class ${number[0]}, its parent ${parent.accountNumber} in class ${parent.accountNumber[0]}`);
156
+ } else if (!assigned.has(number) && !byNumber.has(number) && !followingNumbersOf(chart, year, parent.accountNumber).includes(number)) {
157
+ add(
158
+ number,
159
+ 'not_following_parent',
160
+ number < parent.accountNumber
161
+ ? `${number} lies before its parent ${parent.accountNumber}; an individual account follows it`
162
+ : `${number} follows ${precedingNumberOf(chart, year, number)}, not its parent ${parent.accountNumber}: DATEV passes the marks of the account a number follows on to it`,
163
+ );
164
+ }
165
+ const overrides = declaration.overrides ?? {};
166
+ const profitAndLoss = parent.accountType === 'revenue' || parent.accountType === 'expense';
167
+ if (typeof overrides.hgb266Position === 'string') {
168
+ if (!HGB_266_POSITIONS.some((position) => position.key === overrides.hgb266Position)) {
169
+ add(number, 'override_invalid', `${overrides.hgb266Position} is no balance-sheet position of § 266 HGB`);
170
+ } else if (profitAndLoss) {
171
+ add(number, 'override_invalid', `a balance-sheet position for ${number}, whose parent ${parent.accountNumber} is a profit-and-loss account (${parent.accountType})`);
172
+ }
173
+ }
174
+ if (typeof overrides.hgb275Position === 'string') {
175
+ if (!HGB_275_GKV_POSITIONS.some((position) => position.key === overrides.hgb275Position)) {
176
+ add(number, 'override_invalid', `${overrides.hgb275Position} is no income-statement position of § 275 Abs. 2 HGB`);
177
+ } else if (!profitAndLoss) {
178
+ add(number, 'override_invalid', `an income-statement position for ${number}, whose parent ${parent.accountNumber} is a balance-sheet account (${parent.accountType})`);
179
+ }
180
+ }
181
+ if (overrides.euer !== undefined && overrides.euerExclusion !== undefined) {
182
+ add(number, 'override_invalid', 'an account maps to the Anlage EÜR or is excluded from it, not both');
183
+ }
184
+ if (overrides.euer !== undefined) {
185
+ // a line of every form year; a split's debit on a line that counts debits, its credit on one that counts credits
186
+ const lines: Array<[number, 'debit' | 'credit' | undefined]> = 'kennzahl' in overrides.euer
187
+ ? [[overrides.euer.kennzahl, undefined]]
188
+ : [[overrides.euer.debitKennzahl, 'debit'], [overrides.euer.creditKennzahl, 'credit']];
189
+ for (const [kennzahl, side] of lines) {
190
+ for (const formYear of listEuerFormYears()) {
191
+ let line: ReturnType<typeof getEuerFormLine>;
192
+ try {
193
+ line = getEuerFormLine(formYear, kennzahl);
194
+ } catch {
195
+ add(number, 'override_invalid', `Kz ${kennzahl} is no line of the Anlage EÜR ${formYear}`);
196
+ continue;
197
+ }
198
+ if (side !== undefined && line.side !== side) {
199
+ add(number, 'override_invalid', `Kz ${kennzahl} counts ${line.side}s in the Anlage EÜR ${formYear}, not the ${side}s of a split`);
200
+ }
201
+ }
202
+ }
203
+ }
204
+ if (declaration.securitiesRole !== undefined) {
205
+ const taxRule = overrides.taxRule === undefined ? parent.taxRule : overrides.taxRule ?? undefined;
206
+ const key = `${declaration.securitiesRole}:${taxRule ?? ''}`;
207
+ const holder = roles.get(key);
208
+ if (holder !== undefined) add(number, 'role_taken', `${holder} already books ${declaration.securitiesRole}${taxRule ? ` under ${taxRule}` : ''}`);
209
+ else roles.set(key, number);
210
+ }
211
+ }
212
+ return problems;
213
+ }
214
+
215
+ /** Thrown where the books' individual accounts cannot be accounts of the chart; `problems` says why. */
216
+ export class IndividualAccountsError extends Error {
217
+ constructor(public readonly problems: IIndividualAccountProblem[]) {
218
+ super(`individual accounts refused: ${problems.map((problem) => `${problem.accountNumber} ${problem.code}`).join(', ')}`);
219
+ this.name = 'IndividualAccountsError';
220
+ }
221
+ }
222
+
223
+ /** An individual account as an account of the chart: what its parent is, as its declaration changes it. */
224
+ function individualAccountOf(parentArg: IChartAccount, declarationArg: IIndividualAccountDeclaration, yearArg: number): IChartAccount {
225
+ const overrides = declarationArg.overrides ?? {};
226
+ const pick = <T>(inherited: T | undefined, override: T | null | undefined): T | undefined => (override === undefined ? inherited : override ?? undefined);
227
+ const hgb266Position = pick(parentArg.hgb266Position, overrides.hgb266Position);
228
+ const hgb275Position = pick(parentArg.hgb275Position, overrides.hgb275Position);
229
+ const ustva = pick(parentArg.ustva, overrides.ustva);
230
+ const accountRole = pick<TAccountRole>(parentArg.accountRole, overrides.accountRole);
231
+ const taxRule = pick<TTaxRule>(parentArg.taxRule, overrides.taxRule);
232
+ const commercialOnly = pick<true>(parentArg.commercialOnly, overrides.commercialOnly);
233
+ const euerFromOverride = overrides.euer !== undefined || overrides.euerExclusion !== undefined;
234
+ const euer = euerFromOverride ? overrides.euer : parentArg.euer;
235
+ const euerExclusion = euerFromOverride ? overrides.euerExclusion : parentArg.euerExclusion;
236
+ return {
237
+ accountNumber: declarationArg.accountNumber,
238
+ accountName: declarationArg.accountName,
239
+ accountClass: Number.parseInt(declarationArg.accountNumber[0]!, 10),
240
+ accountType: parentArg.accountType,
241
+ ...(accountRole ? { accountRole } : {}),
242
+ ...(declarationArg.securitiesRole ? { securitiesRole: declarationArg.securitiesRole } : {}),
243
+ ...(taxRule ? { taxRule } : {}),
244
+ ...(commercialOnly ? { commercialOnly } : {}),
245
+ ...(ustva ? { ustva } : {}),
246
+ ...(hgb266Position ? { hgb266Position } : {}),
247
+ ...(hgb275Position ? { hgb275Position } : {}),
248
+ ...(parentArg.ebilanzPosition ? { ebilanzPosition: parentArg.ebilanzPosition } : {}),
249
+ ...(euer ? { euer } : {}),
250
+ ...(euerExclusion ? { euerExclusion } : {}),
251
+ parentAccountNumber: parentArg.accountNumber,
252
+ provenance: `individual account of the books under ${parentArg.accountNumber} (${parentArg.accountName})${Object.keys(overrides).length > 0 ? `; overrides ${Object.keys(overrides).sort().join(', ')}` : ''}`,
253
+ validFrom: yearArg,
254
+ };
255
+ }
256
+
257
+ /**
258
+ * A chart of a year with the books' individual accounts: the lookups of the chart over its DATEV accounts and the
259
+ * books' own. Built by `BooksChart.of`, which refuses declarations with problems (`IndividualAccountsError`).
260
+ */
261
+ export class BooksChart implements IChart {
262
+ public readonly chart: TChartName;
263
+ public readonly year: number;
264
+ public readonly accounts: IChartAccount[];
265
+ public readonly individualAccounts: IChartAccount[];
266
+ private readonly byNumber: Map<string, IChartAccount>;
267
+
268
+ private constructor(chartArg: TChartName, yearArg: number, individualArg: IChartAccount[]) {
269
+ this.chart = chartArg;
270
+ this.year = yearArg;
271
+ this.individualAccounts = individualArg;
272
+ this.accounts = [...getChart(chartArg, yearArg).accounts, ...individualArg].sort((a, b) => a.accountNumber.localeCompare(b.accountNumber));
273
+ this.byNumber = new Map(this.accounts.map((account) => [account.accountNumber, account]));
274
+ }
275
+
276
+ /** The chart of the year with the books' individual accounts; throws `IndividualAccountsError` for declarations with problems. */
277
+ public static of(chartArg: TChartName, yearArg: number, declarationsArg: readonly IIndividualAccountDeclaration[] = []): BooksChart {
278
+ const problems = individualAccountProblemsOf(chartArg, yearArg, declarationsArg);
279
+ if (problems.length > 0) throw new IndividualAccountsError(problems);
280
+ const standard = getChart(chartArg, yearArg);
281
+ const parents = new Map(standard.accounts.map((account) => [account.accountNumber, account]));
282
+ return new BooksChart(
283
+ chartArg,
284
+ yearArg,
285
+ declarationsArg.map((declaration) => individualAccountOf(parents.get(declaration.parentAccountNumber)!, declaration, yearArg)),
286
+ );
287
+ }
288
+
289
+ /** The account of the number, a DATEV account or one of the books; `null` where there is none. */
290
+ public getAccount(accountNumberArg: string): IChartAccount | null {
291
+ return this.byNumber.get(accountNumberArg) ?? null;
292
+ }
293
+
294
+ public getAccountsByRole(roleArg: TAccountRole): IChartAccount[] {
295
+ return this.accounts.filter((account) => account.accountRole === roleArg);
296
+ }
297
+
298
+ public getAccountsBySecuritiesRole(roleArg: TSecuritiesRole): IChartAccount[] {
299
+ return this.accounts.filter((account) => account.securitiesRole === roleArg);
300
+ }
301
+
302
+ /** The one account of the books that books the securities role under the tax rule (none: the account without one), or `null`. */
303
+ public getSecuritiesAccount(roleArg: TSecuritiesRole, taxRuleArg?: TTaxRule): IChartAccount | null {
304
+ return this.getAccountsBySecuritiesRole(roleArg).find((account) => account.taxRule === taxRuleArg) ?? null;
305
+ }
306
+
307
+ public getMappingCoverage(usedAccountsArg: string[], dimensionArg: TMappingDimension): IMappingCoverage {
308
+ return mappingCoverageOf(this.accounts, usedAccountsArg, dimensionArg);
309
+ }
310
+
311
+ public getEuerLines(accountNumberArg: string, formYearArg: number, optionsArg: IEuerLinesOptions = {}): TEuerAccountLines | null {
312
+ const account = this.getAccount(accountNumberArg);
313
+ return account ? euerLinesOf(account, formYearArg, optionsArg) : null;
314
+ }
315
+ }
@@ -138,8 +138,16 @@ export function getMappingCoverage(
138
138
  usedAccounts: string[],
139
139
  dimension: TMappingDimension,
140
140
  ): IMappingCoverage {
141
- const dataset = getChart(chart, year);
142
- const byNumber = new Map(dataset.accounts.map((account) => [account.accountNumber, account]));
141
+ return mappingCoverageOf(getChart(chart, year).accounts, usedAccounts, dimension);
142
+ }
143
+
144
+ /** `getMappingCoverage` over a list of accounts (a chart's, or a chart's with the books' individual accounts). */
145
+ export function mappingCoverageOf(
146
+ accounts: readonly IChartAccount[],
147
+ usedAccounts: string[],
148
+ dimension: TMappingDimension,
149
+ ): IMappingCoverage {
150
+ const byNumber = new Map(accounts.map((account) => [account.accountNumber, account]));
143
151
  const missing: string[] = [];
144
152
  const unknown: string[] = [];
145
153
  let covered = 0;
@@ -49,7 +49,33 @@ export type TAccountRole = 'asset_sale_proceeds' | 'disposal_book_value';
49
49
  * deducted), 'foreign_tax_exempt_income' (a foreign tax on income exempt at
50
50
  * home), and 'foreign_tax_refund' (a foreign tax to be refunded);
51
51
  * - 'margin_interest': the interest on a short-term debt, such as a broker's
52
- * margin loan.
52
+ * margin loan;
53
+ * - 'borrow_fees': interest-like expenses (zinsähnliche Aufwendungen), such as
54
+ * the fees of securities borrowed for a short sale;
55
+ * - 'writedown_other_current': the write-down of a current asset that is
56
+ * neither inventory nor a security, at the usual amount (an option bought,
57
+ * a variation margin paid);
58
+ * - 'expected_loss_provision': the provision for an expected loss from a
59
+ * pending transaction (§ 249 Abs. 1 HGB: a written option worth more than
60
+ * its premium), which tax law does not allow (§ 5 Abs. 4a EStG);
61
+ * - 'provision_release': the income of a provision released.
62
+ * The roles DATEV's standard charts hold no account of their own for: the
63
+ * books book them on the account of their position, or on an individual
64
+ * account (`IIndividualAccountDeclaration`) where they keep them apart;
65
+ * `INDIVIDUAL_ACCOUNT_TEMPLATES` suggests the parent of the results the law
66
+ * keeps apart:
67
+ * - balances: 'options_held' (options bought, at their premium),
68
+ * 'variation_margin_paid' and 'variation_margin_received' (a future's or a
69
+ * CFD's daily settlement held until it closes), 'accrued_interest_claim'
70
+ * (Stückzinsen paid, interest accrued), 'lending_claim' (securities lent:
71
+ * the claim to their return), 'written_options' (a writer's obligation at
72
+ * the premium received), 'short_sale_obligations' (securities owed after a
73
+ * short sale), 'collateral_received' (cash collateral to give back);
74
+ * - results kept apart: 'termingeschaeft_gain' and 'termingeschaeft_loss'
75
+ * (§ 15 Abs. 4 Satz 3 EStG offsets the losses only against such gains),
76
+ * 'stillhalter_income' (a writer's premium earned), 'short_sale_gain' and
77
+ * 'short_sale_loss', 'compensation_paid' (payments in place of a dividend
78
+ * on securities sold short).
53
79
  */
54
80
  export type TSecuritiesRole =
55
81
  | 'holding_fixed'
@@ -77,7 +103,25 @@ export type TSecuritiesRole =
77
103
  | 'foreign_withholding_tax'
78
104
  | 'foreign_tax_exempt_income'
79
105
  | 'foreign_tax_refund'
80
- | 'margin_interest';
106
+ | 'margin_interest'
107
+ | 'borrow_fees'
108
+ | 'writedown_other_current'
109
+ | 'expected_loss_provision'
110
+ | 'provision_release'
111
+ | 'options_held'
112
+ | 'variation_margin_paid'
113
+ | 'variation_margin_received'
114
+ | 'accrued_interest_claim'
115
+ | 'lending_claim'
116
+ | 'written_options'
117
+ | 'short_sale_obligations'
118
+ | 'collateral_received'
119
+ | 'termingeschaeft_gain'
120
+ | 'termingeschaeft_loss'
121
+ | 'stillhalter_income'
122
+ | 'short_sale_gain'
123
+ | 'short_sale_loss'
124
+ | 'compensation_paid';
81
125
 
82
126
  /**
83
127
  * A rule of the income tax law an income or expense account is kept for, as
@@ -224,6 +268,13 @@ export interface IChartAccount {
224
268
  securitiesRole?: TSecuritiesRole;
225
269
  /** the income tax rule the account is kept for, as its DATEV name states it; unset = none of its own */
226
270
  taxRule?: TTaxRule;
271
+ /**
272
+ * DATEV's mark "HB": the account is posted in the commercial balance sheet only (Handelsbilanz), its amounts
273
+ * are no part of the tax balance sheet; unset = both
274
+ */
275
+ commercialOnly?: true;
276
+ /** an individual account of the books (`IIndividualAccountDeclaration`): the DATEV account it belongs to; unset = a DATEV account */
277
+ parentAccountNumber?: string;
227
278
  description?: string;
228
279
  automatikFunction?: TAutomatikFunction;
229
280
  /** BU keys allowed on lines of this account ('' = none). Unset = all. */
@@ -289,3 +340,91 @@ export interface IScenarioKennzahlen {
289
340
  inputVatReverseCharge: number; // Kz 67 (Vorsteuer §13b)
290
341
  importVat: number; // Kz 62 (entrichtete EUSt)
291
342
  }
343
+
344
+ /** The numbers a DATEV standard chart of a year assigns (listed accounts and headings), and of them those it reserves ("R"). */
345
+ export interface IDatevNumbers {
346
+ chart: TChartName;
347
+ year: number;
348
+ assigned: readonly string[];
349
+ reserved: readonly string[];
350
+ }
351
+
352
+ /**
353
+ * What an individual account changes deliberately of what it inherits from its parent: a value replaces the
354
+ * parent's, `null` drops it. The Anlage EÜR takes `euer` or `euerExclusion`, not both.
355
+ */
356
+ export interface IIndividualAccountOverrides {
357
+ hgb266Position?: string | null;
358
+ hgb275Position?: string | null;
359
+ ustva?: IUstvaMapping | null;
360
+ euer?: TEuerMapping;
361
+ euerExclusion?: TEuerExclusion;
362
+ accountRole?: TAccountRole | null;
363
+ taxRule?: TTaxRule | null;
364
+ commercialOnly?: true | null;
365
+ }
366
+
367
+ /**
368
+ * An account the books set up themselves (an Individualkonto) on a number the DATEV standard chart leaves free,
369
+ * belonging to one of its accounts (the parent, `parentAccountNumber`): it is what its parent is — its type, its
370
+ * balance-sheet or income-statement position, its UStVA and Anlage EÜR mapping, its account role, its tax rule
371
+ * and DATEV's mark "HB" — unless `overrides` changes one deliberately. It does not take the parent's DATEV tax
372
+ * automatism (DATEV applies one only to the accounts it marks) nor its securities role, which names one account of
373
+ * a chart: an individual account takes one only where its declaration names it. The declarations are the books';
374
+ * chartdata stores none.
375
+ */
376
+ export interface IIndividualAccountDeclaration {
377
+ accountNumber: string;
378
+ accountName: string;
379
+ parentAccountNumber: string;
380
+ securitiesRole?: TSecuritiesRole;
381
+ overrides?: IIndividualAccountOverrides;
382
+ }
383
+
384
+ /**
385
+ * Why an individual account cannot be:
386
+ * - `number_invalid`: its number is no four digits;
387
+ * - `number_assigned`: DATEV assigns or reserves the number in the chart of the year;
388
+ * - `number_duplicate`: two declarations take the number;
389
+ * - `name_missing`: the account has no name;
390
+ * - `parent_unknown`: the chart holds no account of the parent's number;
391
+ * - `parent_individual`: the parent is itself an individual account;
392
+ * - `parent_not_allowed`: the parent is a carry-forward, closing or statistical account, which records no business
393
+ * transaction;
394
+ * - `class_mismatch`: the number lies in another account class (its first digit) than the parent;
395
+ * - `not_following_parent`: the number does not follow its parent: an account of the chart stands between them.
396
+ * DATEV leaves the free numbers after an account for its individual accounts and passes the account's marks for
397
+ * the trade and corporate income tax programs ("G", "K") on to them, up to the next account of the standard chart
398
+ * (DATEV Kontenrahmen SKR 03 and SKR 04, gültig für 2026, Erläuterungen, S. 37), so an individual account
399
+ * elsewhere would be another account's in DATEV's tax programs;
400
+ * - `role_taken`: another account of the chart books the securities role under the same tax rule;
401
+ * - `override_invalid`: an override names no balance-sheet position of § 266 HGB, no income-statement position of
402
+ * § 275 HGB, or one the account's type does not take (a balance-sheet account has a balance-sheet position, a
403
+ * profit-and-loss account an income-statement position); an Anlage EÜR line no form year holds, a split line on
404
+ * the other side; or both a line and an exclusion.
405
+ */
406
+ export type TIndividualAccountProblemCode =
407
+ | 'number_invalid'
408
+ | 'number_assigned'
409
+ | 'number_duplicate'
410
+ | 'name_missing'
411
+ | 'parent_unknown'
412
+ | 'parent_individual'
413
+ | 'parent_not_allowed'
414
+ | 'class_mismatch'
415
+ | 'not_following_parent'
416
+ | 'role_taken'
417
+ | 'override_invalid';
418
+
419
+ export interface IIndividualAccountProblem {
420
+ accountNumber: string;
421
+ code: TIndividualAccountProblemCode;
422
+ message: string;
423
+ }
424
+
425
+ /** A suggestion for an individual account the books need for a securities role: the parent it belongs to and a name. */
426
+ export interface IIndividualAccountTemplate {
427
+ securitiesRole: TSecuritiesRole;
428
+ parentAccountNumber: string;
429
+ accountName: string;
430
+ }