@fin.cx/skr 1.0.0 → 1.2.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.
Files changed (54) hide show
  1. package/dist_ts/index.d.ts +6 -0
  2. package/dist_ts/index.js +7 -1
  3. package/dist_ts/plugins.d.ts +8 -1
  4. package/dist_ts/plugins.js +10 -2
  5. package/dist_ts/skr.api.d.ts +70 -0
  6. package/dist_ts/skr.api.js +354 -3
  7. package/dist_ts/skr.classes.journalentry.d.ts +1 -0
  8. package/dist_ts/skr.classes.journalentry.js +55 -15
  9. package/dist_ts/skr.classes.ledger.d.ts +4 -0
  10. package/dist_ts/skr.classes.ledger.js +72 -1
  11. package/dist_ts/skr.classes.reports.js +56 -22
  12. package/dist_ts/skr.export.accounts.d.ts +53 -0
  13. package/dist_ts/skr.export.accounts.js +111 -0
  14. package/dist_ts/skr.export.balances.d.ts +59 -0
  15. package/dist_ts/skr.export.balances.js +205 -0
  16. package/dist_ts/skr.export.d.ts +110 -0
  17. package/dist_ts/skr.export.js +315 -0
  18. package/dist_ts/skr.export.ledger.d.ts +95 -0
  19. package/dist_ts/skr.export.ledger.js +164 -0
  20. package/dist_ts/skr.export.pdf.d.ts +82 -0
  21. package/dist_ts/skr.export.pdf.js +548 -0
  22. package/dist_ts/skr.invoice.adapter.d.ts +98 -0
  23. package/dist_ts/skr.invoice.adapter.js +476 -0
  24. package/dist_ts/skr.invoice.booking.d.ts +102 -0
  25. package/dist_ts/skr.invoice.booking.js +556 -0
  26. package/dist_ts/skr.invoice.entity.d.ts +287 -0
  27. package/dist_ts/skr.invoice.entity.js +2 -0
  28. package/dist_ts/skr.invoice.mapper.d.ts +69 -0
  29. package/dist_ts/skr.invoice.mapper.js +401 -0
  30. package/dist_ts/skr.invoice.storage.d.ts +140 -0
  31. package/dist_ts/skr.invoice.storage.js +529 -0
  32. package/dist_ts/skr.security.d.ts +65 -0
  33. package/dist_ts/skr.security.js +319 -0
  34. package/dist_ts/skr.types.d.ts +1 -0
  35. package/package.json +18 -12
  36. package/readme.md +461 -132
  37. package/ts/index.ts +6 -0
  38. package/ts/plugins.ts +22 -1
  39. package/ts/skr.api.ts +489 -2
  40. package/ts/skr.classes.journalentry.ts +63 -14
  41. package/ts/skr.classes.ledger.ts +85 -0
  42. package/ts/skr.classes.reports.ts +64 -21
  43. package/ts/skr.export.accounts.ts +154 -0
  44. package/ts/skr.export.balances.ts +270 -0
  45. package/ts/skr.export.ledger.ts +249 -0
  46. package/ts/skr.export.pdf.ts +601 -0
  47. package/ts/skr.export.ts +443 -0
  48. package/ts/skr.invoice.adapter.ts +581 -0
  49. package/ts/skr.invoice.booking.ts +738 -0
  50. package/ts/skr.invoice.entity.ts +351 -0
  51. package/ts/skr.invoice.mapper.ts +486 -0
  52. package/ts/skr.invoice.storage.ts +710 -0
  53. package/ts/skr.security.ts +405 -0
  54. package/ts/skr.types.ts +1 -0
@@ -9,6 +9,14 @@ import type {
9
9
  IJournalEntryLine,
10
10
  IAccountBalance,
11
11
  } from './skr.types.js';
12
+ import { SKR03_ACCOUNTS } from './skr03.data.js';
13
+ import { SKR04_ACCOUNTS } from './skr04.data.js';
14
+
15
+ // Module-level Maps for O(1) SKR standard lookups
16
+ const STANDARD_SKR_MAP = {
17
+ SKR03: new Map(SKR03_ACCOUNTS.map(a => [a.accountNumber, a])),
18
+ SKR04: new Map(SKR04_ACCOUNTS.map(a => [a.accountNumber, a])),
19
+ };
12
20
 
13
21
  export class Ledger {
14
22
  private logger: plugins.smartlog.Smartlog;
@@ -81,6 +89,12 @@ export class Ledger {
81
89
  const accountNumbers = journalData.lines.map((line) => line.accountNumber);
82
90
  await this.validateAccounts(accountNumbers);
83
91
 
92
+ // Validate against SKR standard (warnings only by default)
93
+ await this.validateAccountsAgainstSKR(journalData.lines, {
94
+ strict: false, // Start with warnings only
95
+ warnOnNameMismatch: false // Names vary, don't spam logs
96
+ });
97
+
84
98
  // Validate journal entry is balanced
85
99
  this.validateJournalBalance(journalData.lines);
86
100
 
@@ -139,6 +153,77 @@ export class Ledger {
139
153
  }
140
154
  }
141
155
 
156
+ /**
157
+ * Validate accounts against SKR standard data
158
+ */
159
+ private async validateAccountsAgainstSKR(
160
+ lines: IJournalEntryLine[],
161
+ options?: { strict?: boolean; warnOnNameMismatch?: boolean }
162
+ ): Promise<void> {
163
+ const { strict = false, warnOnNameMismatch = false } = options || {};
164
+ const skrMap = STANDARD_SKR_MAP[this.skrType];
165
+
166
+ if (!skrMap) {
167
+ this.logger.log('warn', `No SKR standard map available for ${this.skrType}`);
168
+ return;
169
+ }
170
+
171
+ const uniqueAccountNumbers = [...new Set(lines.map(line => line.accountNumber))];
172
+
173
+ for (const accountNumber of uniqueAccountNumbers) {
174
+ const standardAccount = skrMap.get(accountNumber);
175
+
176
+ if (!standardAccount) {
177
+ // Special case: SKR04 class 8 is designated for custom accounts ("frei")
178
+ if (this.skrType === 'SKR04' && accountNumber.startsWith('8')) {
179
+ this.logger.log('debug', `Account ${accountNumber} is in SKR04 class 8 (custom accounts allowed)`);
180
+ continue;
181
+ }
182
+
183
+ const message = `Account ${accountNumber} is not a standard ${this.skrType} account`;
184
+ if (strict) {
185
+ throw new Error(message);
186
+ } else {
187
+ this.logger.log('warn', message);
188
+ }
189
+ continue;
190
+ }
191
+
192
+ // Get actual account from database to compare
193
+ const dbAccount = await Account.getAccountByNumber(accountNumber, this.skrType);
194
+ if (!dbAccount) {
195
+ // Account doesn't exist in DB, will be caught by validateAccounts()
196
+ continue;
197
+ }
198
+
199
+ // Validate type and class match SKR standard
200
+ if (dbAccount.accountType !== standardAccount.accountType) {
201
+ const message = `Account ${accountNumber} type mismatch: expected '${standardAccount.accountType}', got '${dbAccount.accountType}'`;
202
+ if (strict) {
203
+ throw new Error(message);
204
+ } else {
205
+ this.logger.log('warn', message);
206
+ }
207
+ }
208
+
209
+ if (dbAccount.accountClass !== standardAccount.accountClass) {
210
+ const message = `Account ${accountNumber} class mismatch: expected ${standardAccount.accountClass}, got ${dbAccount.accountClass}`;
211
+ if (strict) {
212
+ throw new Error(message);
213
+ } else {
214
+ this.logger.log('warn', message);
215
+ }
216
+ }
217
+
218
+ // Warn on name mismatch (common and acceptable in practice)
219
+ if (warnOnNameMismatch && dbAccount.accountName !== standardAccount.accountName) {
220
+ this.logger.log('info',
221
+ `Account ${accountNumber} name differs from SKR standard: '${dbAccount.accountName}' vs '${standardAccount.accountName}'`
222
+ );
223
+ }
224
+ }
225
+ }
226
+
142
227
  /**
143
228
  * Reverse a transaction
144
229
  */
@@ -122,11 +122,11 @@ export class Reports {
122
122
  const entry: IIncomeStatementEntry = {
123
123
  accountNumber: account.accountNumber,
124
124
  accountName: account.accountName,
125
- amount: Math.abs(balance),
125
+ amount: balance, // Keep the sign for correct calculation
126
126
  };
127
127
 
128
128
  revenueEntries.push(entry);
129
- totalRevenue += Math.abs(balance);
129
+ totalRevenue += balance; // Revenue accounts normally have credit balance (positive)
130
130
  }
131
131
  }
132
132
 
@@ -138,23 +138,24 @@ export class Reports {
138
138
  const entry: IIncomeStatementEntry = {
139
139
  accountNumber: account.accountNumber,
140
140
  accountName: account.accountName,
141
- amount: Math.abs(balance),
141
+ amount: balance, // Keep the sign - negative balance reduces expenses
142
142
  };
143
143
 
144
144
  expenseEntries.push(entry);
145
- totalExpenses += Math.abs(balance);
145
+ totalExpenses += balance; // Expense accounts normally have debit balance (positive)
146
+ // But credit balances (negative) reduce total expenses
146
147
  }
147
148
  }
148
149
 
149
- // Calculate percentages
150
+ // Calculate percentages using absolute values to avoid negative percentages
150
151
  revenueEntries.forEach((entry) => {
151
152
  entry.percentage =
152
- totalRevenue > 0 ? (entry.amount / totalRevenue) * 100 : 0;
153
+ totalRevenue !== 0 ? (Math.abs(entry.amount) / Math.abs(totalRevenue)) * 100 : 0;
153
154
  });
154
155
 
155
156
  expenseEntries.forEach((entry) => {
156
157
  entry.percentage =
157
- totalRevenue > 0 ? (entry.amount / totalRevenue) * 100 : 0;
158
+ totalRevenue !== 0 ? (Math.abs(entry.amount) / Math.abs(totalRevenue)) * 100 : 0;
158
159
  });
159
160
 
160
161
  // Sort entries by account number
@@ -214,7 +215,7 @@ export class Reports {
214
215
  const entry: IBalanceSheetEntry = {
215
216
  accountNumber: account.accountNumber,
216
217
  accountName: account.accountName,
217
- amount: Math.abs(balance),
218
+ amount: balance, // Keep the sign for display
218
219
  };
219
220
 
220
221
  // Classify as current or fixed based on account class
@@ -224,7 +225,7 @@ export class Reports {
224
225
  fixedAssets.push(entry);
225
226
  }
226
227
 
227
- totalAssets += Math.abs(balance);
228
+ totalAssets += balance; // Add with sign to get correct total
228
229
  }
229
230
  }
230
231
 
@@ -240,7 +241,7 @@ export class Reports {
240
241
  const entry: IBalanceSheetEntry = {
241
242
  accountNumber: account.accountNumber,
242
243
  accountName: account.accountName,
243
- amount: Math.abs(balance),
244
+ amount: balance, // Keep the sign for display
244
245
  };
245
246
 
246
247
  // Classify as current or long-term based on account number
@@ -253,7 +254,7 @@ export class Reports {
253
254
  longTermLiabilities.push(entry);
254
255
  }
255
256
 
256
- totalLiabilities += Math.abs(balance);
257
+ totalLiabilities += balance; // Add with sign to get correct total
257
258
  }
258
259
  }
259
260
 
@@ -268,23 +269,27 @@ export class Reports {
268
269
  const entry: IBalanceSheetEntry = {
269
270
  accountNumber: account.accountNumber,
270
271
  accountName: account.accountName,
271
- amount: Math.abs(balance),
272
+ amount: balance, // Keep the sign for display
272
273
  };
273
274
 
274
275
  equityEntries.push(entry);
275
- totalEquity += Math.abs(balance);
276
+ totalEquity += balance; // Add with sign to get correct total
276
277
  }
277
278
  }
278
279
 
279
- // Add current year profit/loss
280
+ // Add current year profit/loss only if accounts haven't been closed
281
+ // Check if revenue/expense accounts have non-zero balances (indicates not closed)
280
282
  const incomeStatement = await this.getIncomeStatement(params);
281
- if (incomeStatement.netIncome !== 0) {
283
+
284
+ // Only add current year profit/loss if we have unclosed revenue/expense accounts
285
+ // (i.e., the income statement shows non-zero revenue or expenses)
286
+ if (incomeStatement.netIncome !== 0 && (incomeStatement.totalRevenue !== 0 || incomeStatement.totalExpenses !== 0)) {
282
287
  equityEntries.push({
283
288
  accountNumber: '9999',
284
289
  accountName: 'Current Year Profit/Loss',
285
- amount: Math.abs(incomeStatement.netIncome),
290
+ amount: incomeStatement.netIncome, // Keep the sign
286
291
  });
287
- totalEquity += Math.abs(incomeStatement.netIncome);
292
+ totalEquity += incomeStatement.netIncome; // Add with sign
288
293
  }
289
294
 
290
295
  // Sort entries
@@ -344,9 +349,28 @@ export class Reports {
344
349
 
345
350
  // Apply date filter if provided
346
351
  if (params?.dateFrom || params?.dateTo) {
352
+ // Normalize dates for inclusive comparison
353
+ const dateFrom = params.dateFrom ? new Date(params.dateFrom) : null;
354
+ const dateTo = params.dateTo ? new Date(params.dateTo) : null;
355
+
356
+ // Set dateFrom to start of day (00:00:00.000)
357
+ if (dateFrom) {
358
+ dateFrom.setHours(0, 0, 0, 0);
359
+ }
360
+
361
+ // Set dateTo to end of day (23:59:59.999) for inclusive comparison
362
+ if (dateTo) {
363
+ dateTo.setHours(23, 59, 59, 999);
364
+ }
365
+
347
366
  transactions = transactions.filter((transaction) => {
348
- if (params.dateFrom && transaction.date < params.dateFrom) return false;
349
- if (params.dateTo && transaction.date > params.dateTo) return false;
367
+ const txDate = transaction.date instanceof Date
368
+ ? transaction.date
369
+ : new Date(transaction.date);
370
+ const txTime = txDate.getTime();
371
+
372
+ if (dateFrom && txTime < dateFrom.getTime()) return false;
373
+ if (dateTo && txTime > dateTo.getTime()) return false;
350
374
  return true;
351
375
  });
352
376
  }
@@ -453,9 +477,28 @@ export class Reports {
453
477
 
454
478
  // Apply date filter
455
479
  if (params?.dateFrom || params?.dateTo) {
480
+ // Normalize dates for inclusive comparison
481
+ const dateFrom = params.dateFrom ? new Date(params.dateFrom) : null;
482
+ const dateTo = params.dateTo ? new Date(params.dateTo) : null;
483
+
484
+ // Set dateFrom to start of day (00:00:00.000)
485
+ if (dateFrom) {
486
+ dateFrom.setHours(0, 0, 0, 0);
487
+ }
488
+
489
+ // Set dateTo to end of day (23:59:59.999) for inclusive comparison
490
+ if (dateTo) {
491
+ dateTo.setHours(23, 59, 59, 999);
492
+ }
493
+
456
494
  transactions = transactions.filter((transaction) => {
457
- if (params.dateFrom && transaction.date < params.dateFrom) return false;
458
- if (params.dateTo && transaction.date > params.dateTo) return false;
495
+ const txDate = transaction.date instanceof Date
496
+ ? transaction.date
497
+ : new Date(transaction.date);
498
+ const txTime = txDate.getTime();
499
+
500
+ if (dateFrom && txTime < dateFrom.getTime()) return false;
501
+ if (dateTo && txTime > dateTo.getTime()) return false;
459
502
  return true;
460
503
  });
461
504
  }
@@ -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
+ }