@fin.cx/skr 1.1.0 → 1.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (64) hide show
  1. package/dist_ts/00_commitinfo_data.d.ts +8 -0
  2. package/dist_ts/00_commitinfo_data.js +9 -0
  3. package/dist_ts/index.d.ts +6 -0
  4. package/dist_ts/index.js +7 -1
  5. package/dist_ts/plugins.d.ts +8 -1
  6. package/dist_ts/plugins.js +10 -2
  7. package/dist_ts/skr.api.d.ts +70 -0
  8. package/dist_ts/skr.api.js +348 -1
  9. package/dist_ts/skr.classes.account.d.ts +28 -0
  10. package/dist_ts/skr.classes.account.js +87 -4
  11. package/dist_ts/skr.classes.journalentry.js +56 -4
  12. package/dist_ts/skr.classes.ledger.js +5 -1
  13. package/dist_ts/skr.export.accounts.d.ts +53 -0
  14. package/dist_ts/skr.export.accounts.js +111 -0
  15. package/dist_ts/skr.export.balances.d.ts +59 -0
  16. package/dist_ts/skr.export.balances.js +205 -0
  17. package/dist_ts/skr.export.d.ts +110 -0
  18. package/dist_ts/skr.export.js +315 -0
  19. package/dist_ts/skr.export.ledger.d.ts +95 -0
  20. package/dist_ts/skr.export.ledger.js +164 -0
  21. package/dist_ts/skr.export.pdf.d.ts +82 -0
  22. package/dist_ts/skr.export.pdf.js +548 -0
  23. package/dist_ts/skr.invoice.adapter.d.ts +98 -0
  24. package/dist_ts/skr.invoice.adapter.js +476 -0
  25. package/dist_ts/skr.invoice.booking.d.ts +102 -0
  26. package/dist_ts/skr.invoice.booking.js +578 -0
  27. package/dist_ts/skr.invoice.entity.d.ts +287 -0
  28. package/dist_ts/skr.invoice.entity.js +2 -0
  29. package/dist_ts/skr.invoice.mapper.d.ts +69 -0
  30. package/dist_ts/skr.invoice.mapper.js +401 -0
  31. package/dist_ts/skr.invoice.storage.d.ts +140 -0
  32. package/dist_ts/skr.invoice.storage.js +529 -0
  33. package/dist_ts/skr.postingkeys.d.ts +56 -0
  34. package/dist_ts/skr.postingkeys.js +196 -0
  35. package/dist_ts/skr.security.d.ts +65 -0
  36. package/dist_ts/skr.security.js +319 -0
  37. package/dist_ts/skr.types.d.ts +19 -0
  38. package/dist_ts/skr03.data.js +3 -1
  39. package/dist_ts/skr04.data.js +3 -1
  40. package/package.json +17 -12
  41. package/readme.hints.md +54 -1
  42. package/readme.md +207 -16
  43. package/ts/00_commitinfo_data.ts +8 -0
  44. package/ts/index.ts +6 -0
  45. package/ts/plugins.ts +22 -1
  46. package/ts/skr.api.ts +485 -0
  47. package/ts/skr.classes.account.ts +106 -3
  48. package/ts/skr.classes.journalentry.ts +78 -3
  49. package/ts/skr.classes.ledger.ts +4 -0
  50. package/ts/skr.export.accounts.ts +154 -0
  51. package/ts/skr.export.balances.ts +270 -0
  52. package/ts/skr.export.ledger.ts +249 -0
  53. package/ts/skr.export.pdf.ts +601 -0
  54. package/ts/skr.export.ts +443 -0
  55. package/ts/skr.invoice.adapter.ts +581 -0
  56. package/ts/skr.invoice.booking.ts +760 -0
  57. package/ts/skr.invoice.entity.ts +351 -0
  58. package/ts/skr.invoice.mapper.ts +486 -0
  59. package/ts/skr.invoice.storage.ts +710 -0
  60. package/ts/skr.postingkeys.ts +252 -0
  61. package/ts/skr.security.ts +405 -0
  62. package/ts/skr.types.ts +27 -0
  63. package/ts/skr03.data.ts +2 -0
  64. package/ts/skr04.data.ts +2 -0
@@ -2,6 +2,11 @@ import * as plugins from './plugins.js';
2
2
  import { getDbSync } from './skr.database.js';
3
3
  import { Account } from './skr.classes.account.js';
4
4
  import { Transaction } from './skr.classes.transaction.js';
5
+ import {
6
+ validatePostingKey,
7
+ validatePostingKeyConsistency,
8
+ getPostingKeyDescription,
9
+ } from './skr.postingkeys.js';
5
10
  import type {
6
11
  TSKRType,
7
12
  IJournalEntry,
@@ -212,22 +217,91 @@ export class JournalEntry extends SmartDataDbDoc<JournalEntry, JournalEntry> {
212
217
  throw new Error('Journal entry must have at least 2 lines');
213
218
  }
214
219
 
215
- // Validate all accounts exist and are active
220
+ // Validate all accounts exist, are active, and can be posted to
221
+ const validationErrors: string[] = [];
222
+ const validationWarnings: string[] = [];
223
+
224
+ // Check if this journal entry has VAT lines (for smarter posting key validation)
225
+ const hasVATLines = this.lines.some(line =>
226
+ line.accountNumber === '1571' || line.accountNumber === '1771' || line.accountNumber === '1576'
227
+ );
228
+
216
229
  for (const line of this.lines) {
230
+ // Validate posting key is present (REQUIRED)
231
+ if (!line.postingKey) {
232
+ validationErrors.push(
233
+ `Line for account ${line.accountNumber} is missing required posting key (Buchungsschlüssel). ` +
234
+ `Posting keys are mandatory for DATEV compliance.`
235
+ );
236
+ continue; // Skip further validation for this line
237
+ }
238
+
239
+ // Validate account is not an automatic account (Automatikkonto)
240
+ try {
241
+ await Account.validateAccountForPosting(line.accountNumber, this.skrType);
242
+ } catch (error) {
243
+ validationErrors.push(error.message);
244
+ continue; // Skip further validation for this line
245
+ }
246
+
247
+ // Get account for posting key validation
217
248
  const account = await Account.getAccountByNumber(
218
249
  line.accountNumber,
219
250
  this.skrType,
220
251
  );
221
252
 
222
253
  if (!account) {
223
- throw new Error(
254
+ validationErrors.push(
224
255
  `Account ${line.accountNumber} not found for ${this.skrType}`,
225
256
  );
257
+ continue;
226
258
  }
227
259
 
228
260
  if (!account.isActive) {
229
- throw new Error(`Account ${line.accountNumber} is not active`);
261
+ validationErrors.push(`Account ${line.accountNumber} is not active`);
262
+ continue;
263
+ }
264
+
265
+ // Validate posting key for this line
266
+ const amount = line.debit || line.credit || 0;
267
+ // For journal entries with VAT lines, pass amount as vatAmount to satisfy validation
268
+ const postingKeyValidation = validatePostingKey(
269
+ line.postingKey,
270
+ line.accountNumber,
271
+ amount,
272
+ hasVATLines ? amount : undefined // If entry has VAT lines, we consider the validation satisfied
273
+ );
274
+
275
+ if (!postingKeyValidation.isValid) {
276
+ validationErrors.push(...postingKeyValidation.errors);
230
277
  }
278
+
279
+ if (postingKeyValidation.warnings.length > 0) {
280
+ validationWarnings.push(...postingKeyValidation.warnings);
281
+ }
282
+ }
283
+
284
+ // Validate posting key consistency across all lines
285
+ const consistencyValidation = validatePostingKeyConsistency(this.lines);
286
+ if (!consistencyValidation.isValid) {
287
+ validationErrors.push(...consistencyValidation.errors);
288
+ }
289
+ if (consistencyValidation.warnings.length > 0) {
290
+ validationWarnings.push(...consistencyValidation.warnings);
291
+ }
292
+
293
+ // Log warnings but don't fail validation
294
+ if (validationWarnings.length > 0) {
295
+ console.warn('Journal entry validation warnings:');
296
+ validationWarnings.forEach(warning => console.warn(` - ${warning}`));
297
+ }
298
+
299
+ // Throw if any errors
300
+ if (validationErrors.length > 0) {
301
+ throw new Error(
302
+ 'Journal entry validation failed:\n' +
303
+ validationErrors.map(e => ` - ${e}`).join('\n')
304
+ );
231
305
  }
232
306
  }
233
307
 
@@ -325,6 +399,7 @@ export class JournalEntry extends SmartDataDbDoc<JournalEntry, JournalEntry> {
325
399
  credit: line.debit, // Swap
326
400
  description: `Reversal: ${line.description || ''}`,
327
401
  costCenter: line.costCenter,
402
+ postingKey: line.postingKey, // Keep same posting key for reversal
328
403
  }));
329
404
 
330
405
  const reversalEntry = new JournalEntry({
@@ -418,6 +418,7 @@ export class Ledger {
418
418
  accountNumber: account.accountNumber,
419
419
  debit: Math.abs(balance),
420
420
  description: `Closing ${account.accountName}`,
421
+ postingKey: 40, // Tax-free - internal closing entry
421
422
  });
422
423
  totalRevenue += Math.abs(balance);
423
424
  }
@@ -429,6 +430,7 @@ export class Ledger {
429
430
  accountNumber: closingAccountNumber,
430
431
  credit: totalRevenue,
431
432
  description: 'Revenue closing to P&L',
433
+ postingKey: 40, // Tax-free - internal closing entry
432
434
  });
433
435
 
434
436
  const revenueClosingEntry = await this.postJournalEntry({
@@ -458,6 +460,7 @@ export class Ledger {
458
460
  accountNumber: account.accountNumber,
459
461
  credit: Math.abs(balance),
460
462
  description: `Closing ${account.accountName}`,
463
+ postingKey: 40, // Tax-free - internal closing entry
461
464
  });
462
465
  totalExpense += Math.abs(balance);
463
466
  }
@@ -469,6 +472,7 @@ export class Ledger {
469
472
  accountNumber: closingAccountNumber,
470
473
  debit: totalExpense,
471
474
  description: 'Expense closing to P&L',
475
+ postingKey: 40, // Tax-free - internal closing entry
472
476
  });
473
477
 
474
478
  const expenseClosingEntry = await this.postJournalEntry({
@@ -0,0 +1,154 @@
1
+ import * as plugins from './plugins.js';
2
+ import * as path from 'path';
3
+ import type { IAccountData, TSKRType } from './skr.types.js';
4
+
5
+ // Extended interface for export with additional fields
6
+ export interface IAccountDataExport extends IAccountData {
7
+ parentAccount?: string;
8
+ defaultTaxCode?: string;
9
+ activeFrom?: Date | string;
10
+ activeTo?: Date | string;
11
+ }
12
+
13
+ export interface IAccountExportRow {
14
+ account_code: string;
15
+ name: string;
16
+ type: string;
17
+ class: number;
18
+ parent?: string;
19
+ skr_set: TSKRType;
20
+ tax_code_default?: string;
21
+ active_from?: string;
22
+ active_to?: string;
23
+ description?: string;
24
+ is_active: boolean;
25
+ }
26
+
27
+ export class AccountsExporter {
28
+ private exportPath: string;
29
+ private accounts: IAccountExportRow[] = [];
30
+
31
+ constructor(exportPath: string) {
32
+ this.exportPath = exportPath;
33
+ }
34
+
35
+ /**
36
+ * Adds an account to the export
37
+ */
38
+ public addAccount(account: IAccountDataExport): void {
39
+ const exportRow: IAccountExportRow = {
40
+ account_code: account.accountNumber,
41
+ name: account.accountName,
42
+ type: account.accountType,
43
+ class: account.accountClass,
44
+ parent: account.parentAccount,
45
+ skr_set: account.skrType,
46
+ tax_code_default: account.defaultTaxCode,
47
+ active_from: account.activeFrom ? this.formatDate(account.activeFrom) : undefined,
48
+ active_to: account.activeTo ? this.formatDate(account.activeTo) : undefined,
49
+ description: account.description,
50
+ is_active: account.isActive !== false
51
+ };
52
+
53
+ this.accounts.push(exportRow);
54
+ }
55
+
56
+ /**
57
+ * Exports accounts to CSV format
58
+ */
59
+ public async exportToCSV(): Promise<void> {
60
+ const csvPath = path.join(this.exportPath, 'data', 'accounting', 'accounts.csv');
61
+ await plugins.smartfile.fs.ensureDir(path.dirname(csvPath));
62
+
63
+ // Create CSV header
64
+ const headers = [
65
+ 'account_code',
66
+ 'name',
67
+ 'type',
68
+ 'class',
69
+ 'parent',
70
+ 'skr_set',
71
+ 'tax_code_default',
72
+ 'active_from',
73
+ 'active_to',
74
+ 'description',
75
+ 'is_active'
76
+ ];
77
+
78
+ let csvContent = headers.join(',') + '\n';
79
+
80
+ // Add account rows
81
+ for (const account of this.accounts) {
82
+ const row = [
83
+ this.escapeCSV(account.account_code),
84
+ this.escapeCSV(account.name),
85
+ this.escapeCSV(account.type),
86
+ account.class.toString(),
87
+ this.escapeCSV(account.parent || ''),
88
+ this.escapeCSV(account.skr_set),
89
+ this.escapeCSV(account.tax_code_default || ''),
90
+ this.escapeCSV(account.active_from || ''),
91
+ this.escapeCSV(account.active_to || ''),
92
+ this.escapeCSV(account.description || ''),
93
+ account.is_active.toString()
94
+ ];
95
+
96
+ csvContent += row.join(',') + '\n';
97
+ }
98
+
99
+ await plugins.smartfile.memory.toFs(csvContent, csvPath);
100
+ }
101
+
102
+ /**
103
+ * Exports accounts to JSON format (alternative)
104
+ */
105
+ public async exportToJSON(): Promise<void> {
106
+ const jsonPath = path.join(this.exportPath, 'data', 'accounting', 'accounts.json');
107
+ await plugins.smartfile.fs.ensureDir(path.dirname(jsonPath));
108
+
109
+ const jsonData = {
110
+ schema_version: '1.0',
111
+ export_date: new Date().toISOString(),
112
+ accounts: this.accounts
113
+ };
114
+
115
+ await plugins.smartfile.memory.toFs(
116
+ JSON.stringify(jsonData, null, 2),
117
+ jsonPath
118
+ );
119
+ }
120
+
121
+ /**
122
+ * Escapes CSV values
123
+ */
124
+ private escapeCSV(value: string): string {
125
+ if (value.includes(',') || value.includes('"') || value.includes('\n')) {
126
+ return `"${value.replace(/"/g, '""')}"`;
127
+ }
128
+ return value;
129
+ }
130
+
131
+ /**
132
+ * Formats a date to ISO date string
133
+ */
134
+ private formatDate(date: Date | string): string {
135
+ if (typeof date === 'string') {
136
+ return date.split('T')[0];
137
+ }
138
+ return date.toISOString().split('T')[0];
139
+ }
140
+
141
+ /**
142
+ * Gets the number of accounts
143
+ */
144
+ public getAccountCount(): number {
145
+ return this.accounts.length;
146
+ }
147
+
148
+ /**
149
+ * Clears the accounts list
150
+ */
151
+ public clear(): void {
152
+ this.accounts = [];
153
+ }
154
+ }
@@ -0,0 +1,270 @@
1
+ import * as plugins from './plugins.js';
2
+ import * as path from 'path';
3
+ import type { IAccountBalance } from './skr.types.js';
4
+
5
+ // Extended interface for export with additional fields
6
+ export interface IAccountBalanceExport extends IAccountBalance {
7
+ openingBalance?: number;
8
+ transactionCount?: number;
9
+ }
10
+
11
+ export interface IBalanceExportRow {
12
+ account_code: string;
13
+ account_name: string;
14
+ fiscal_year: number;
15
+ period?: string;
16
+ opening_balance: string;
17
+ closing_balance: string;
18
+ debit_sum: string;
19
+ credit_sum: string;
20
+ balance: string;
21
+ transaction_count: number;
22
+ }
23
+
24
+ export class BalancesExporter {
25
+ private exportPath: string;
26
+ private balances: IBalanceExportRow[] = [];
27
+ private fiscalYear: number;
28
+
29
+ constructor(exportPath: string, fiscalYear: number) {
30
+ this.exportPath = exportPath;
31
+ this.fiscalYear = fiscalYear;
32
+ }
33
+
34
+ /**
35
+ * Adds a balance entry to the export
36
+ */
37
+ public addBalance(
38
+ accountCode: string,
39
+ accountName: string,
40
+ balance: IAccountBalanceExport,
41
+ period?: string
42
+ ): void {
43
+ const exportRow: IBalanceExportRow = {
44
+ account_code: accountCode,
45
+ account_name: accountName,
46
+ fiscal_year: this.fiscalYear,
47
+ period: period,
48
+ opening_balance: (balance.openingBalance || 0).toFixed(2),
49
+ closing_balance: balance.balance.toFixed(2),
50
+ debit_sum: balance.debitTotal.toFixed(2),
51
+ credit_sum: balance.creditTotal.toFixed(2),
52
+ balance: balance.balance.toFixed(2),
53
+ transaction_count: balance.transactionCount || 0
54
+ };
55
+
56
+ this.balances.push(exportRow);
57
+ }
58
+
59
+ /**
60
+ * Exports balances to CSV format
61
+ */
62
+ public async exportToCSV(): Promise<void> {
63
+ const csvPath = path.join(this.exportPath, 'data', 'accounting', 'balances.csv');
64
+ await plugins.smartfile.fs.ensureDir(path.dirname(csvPath));
65
+
66
+ // Create CSV header
67
+ const headers = [
68
+ 'account_code',
69
+ 'account_name',
70
+ 'fiscal_year',
71
+ 'period',
72
+ 'opening_balance',
73
+ 'closing_balance',
74
+ 'debit_sum',
75
+ 'credit_sum',
76
+ 'balance',
77
+ 'transaction_count'
78
+ ];
79
+
80
+ let csvContent = headers.join(',') + '\n';
81
+
82
+ // Sort balances by account code
83
+ this.balances.sort((a, b) => a.account_code.localeCompare(b.account_code));
84
+
85
+ // Add balance rows
86
+ for (const balance of this.balances) {
87
+ const row = [
88
+ this.escapeCSV(balance.account_code),
89
+ this.escapeCSV(balance.account_name),
90
+ balance.fiscal_year.toString(),
91
+ this.escapeCSV(balance.period || ''),
92
+ balance.opening_balance,
93
+ balance.closing_balance,
94
+ balance.debit_sum,
95
+ balance.credit_sum,
96
+ balance.balance,
97
+ balance.transaction_count.toString()
98
+ ];
99
+
100
+ csvContent += row.join(',') + '\n';
101
+ }
102
+
103
+ await plugins.smartfile.memory.toFs(csvContent, csvPath);
104
+ }
105
+
106
+ /**
107
+ * Exports trial balance (Summen- und Saldenliste)
108
+ */
109
+ public async exportTrialBalance(): Promise<void> {
110
+ const csvPath = path.join(this.exportPath, 'data', 'accounting', 'trial_balance.csv');
111
+ await plugins.smartfile.fs.ensureDir(path.dirname(csvPath));
112
+
113
+ // Create CSV header for trial balance
114
+ const headers = [
115
+ 'Konto',
116
+ 'Bezeichnung',
117
+ 'Anfangssaldo',
118
+ 'Soll',
119
+ 'Haben',
120
+ 'Saldo',
121
+ 'Endsaldo'
122
+ ];
123
+
124
+ let csvContent = headers.join(',') + '\n';
125
+
126
+ // Add rows with German formatting
127
+ for (const balance of this.balances) {
128
+ const row = [
129
+ this.escapeCSV(balance.account_code),
130
+ this.escapeCSV(balance.account_name),
131
+ this.formatGermanNumber(parseFloat(balance.opening_balance)),
132
+ this.formatGermanNumber(parseFloat(balance.debit_sum)),
133
+ this.formatGermanNumber(parseFloat(balance.credit_sum)),
134
+ this.formatGermanNumber(parseFloat(balance.debit_sum) - parseFloat(balance.credit_sum)),
135
+ this.formatGermanNumber(parseFloat(balance.closing_balance))
136
+ ];
137
+
138
+ csvContent += row.join(',') + '\n';
139
+ }
140
+
141
+ // Add totals row
142
+ const totalDebit = this.balances.reduce((sum, b) => sum + parseFloat(b.debit_sum), 0);
143
+ const totalCredit = this.balances.reduce((sum, b) => sum + parseFloat(b.credit_sum), 0);
144
+
145
+ csvContent += '\n';
146
+ csvContent += [
147
+ 'SUMME',
148
+ '',
149
+ '',
150
+ this.formatGermanNumber(totalDebit),
151
+ this.formatGermanNumber(totalCredit),
152
+ this.formatGermanNumber(totalDebit - totalCredit),
153
+ ''
154
+ ].join(',') + '\n';
155
+
156
+ await plugins.smartfile.memory.toFs(csvContent, csvPath);
157
+ }
158
+
159
+ /**
160
+ * Exports balances to JSON format
161
+ */
162
+ public async exportToJSON(): Promise<void> {
163
+ const jsonPath = path.join(this.exportPath, 'data', 'accounting', 'balances.json');
164
+ await plugins.smartfile.fs.ensureDir(path.dirname(jsonPath));
165
+
166
+ const jsonData = {
167
+ schema_version: '1.0',
168
+ export_date: new Date().toISOString(),
169
+ fiscal_year: this.fiscalYear,
170
+ balances: this.balances,
171
+ totals: {
172
+ total_debit: this.balances.reduce((sum, b) => sum + parseFloat(b.debit_sum), 0).toFixed(2),
173
+ total_credit: this.balances.reduce((sum, b) => sum + parseFloat(b.credit_sum), 0).toFixed(2),
174
+ account_count: this.balances.length
175
+ }
176
+ };
177
+
178
+ await plugins.smartfile.memory.toFs(
179
+ JSON.stringify(jsonData, null, 2),
180
+ jsonPath
181
+ );
182
+ }
183
+
184
+ /**
185
+ * Generates balance summary for specific account classes
186
+ */
187
+ public async exportClassSummary(): Promise<void> {
188
+ const csvPath = path.join(this.exportPath, 'data', 'accounting', 'class_summary.csv');
189
+ await plugins.smartfile.fs.ensureDir(path.dirname(csvPath));
190
+
191
+ // Group balances by account class (first digit of account code)
192
+ const classSummary: { [key: string]: { debit: number; credit: number; balance: number } } = {};
193
+
194
+ for (const balance of this.balances) {
195
+ const accountClass = balance.account_code.charAt(0);
196
+
197
+ if (!classSummary[accountClass]) {
198
+ classSummary[accountClass] = { debit: 0, credit: 0, balance: 0 };
199
+ }
200
+
201
+ classSummary[accountClass].debit += parseFloat(balance.debit_sum);
202
+ classSummary[accountClass].credit += parseFloat(balance.credit_sum);
203
+ classSummary[accountClass].balance += parseFloat(balance.balance);
204
+ }
205
+
206
+ // Create CSV
207
+ let csvContent = 'Kontenklasse,Bezeichnung,Soll,Haben,Saldo\n';
208
+
209
+ const classNames: { [key: string]: string } = {
210
+ '0': 'Anlagevermögen',
211
+ '1': 'Umlaufvermögen',
212
+ '2': 'Eigenkapital',
213
+ '3': 'Fremdkapital',
214
+ '4': 'Betriebliche Erträge',
215
+ '5': 'Materialaufwand',
216
+ '6': 'Betriebsaufwand',
217
+ '7': 'Weitere Aufwendungen',
218
+ '8': 'Erträge',
219
+ '9': 'Abschlusskonten'
220
+ };
221
+
222
+ for (const [classNum, summary] of Object.entries(classSummary)) {
223
+ const row = [
224
+ classNum,
225
+ this.escapeCSV(classNames[classNum] || `Klasse ${classNum}`),
226
+ this.formatGermanNumber(summary.debit),
227
+ this.formatGermanNumber(summary.credit),
228
+ this.formatGermanNumber(summary.balance)
229
+ ];
230
+
231
+ csvContent += row.join(',') + '\n';
232
+ }
233
+
234
+ await plugins.smartfile.memory.toFs(csvContent, csvPath);
235
+ }
236
+
237
+ /**
238
+ * Escapes CSV values
239
+ */
240
+ private escapeCSV(value: string): string {
241
+ if (value.includes(',') || value.includes('"') || value.includes('\n')) {
242
+ return `"${value.replace(/"/g, '""')}"`;
243
+ }
244
+ return value;
245
+ }
246
+
247
+ /**
248
+ * Formats number in German format (1.234,56)
249
+ */
250
+ private formatGermanNumber(value: number): string {
251
+ return value.toLocaleString('de-DE', {
252
+ minimumFractionDigits: 2,
253
+ maximumFractionDigits: 2
254
+ });
255
+ }
256
+
257
+ /**
258
+ * Gets the number of balance entries
259
+ */
260
+ public getBalanceCount(): number {
261
+ return this.balances.length;
262
+ }
263
+
264
+ /**
265
+ * Clears the balances list
266
+ */
267
+ public clear(): void {
268
+ this.balances = [];
269
+ }
270
+ }