@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.
- package/dist_ts/index.d.ts +6 -0
- package/dist_ts/index.js +7 -1
- package/dist_ts/plugins.d.ts +8 -1
- package/dist_ts/plugins.js +10 -2
- package/dist_ts/skr.api.d.ts +70 -0
- package/dist_ts/skr.api.js +354 -3
- package/dist_ts/skr.classes.journalentry.d.ts +1 -0
- package/dist_ts/skr.classes.journalentry.js +55 -15
- package/dist_ts/skr.classes.ledger.d.ts +4 -0
- package/dist_ts/skr.classes.ledger.js +72 -1
- package/dist_ts/skr.classes.reports.js +56 -22
- package/dist_ts/skr.export.accounts.d.ts +53 -0
- package/dist_ts/skr.export.accounts.js +111 -0
- package/dist_ts/skr.export.balances.d.ts +59 -0
- package/dist_ts/skr.export.balances.js +205 -0
- package/dist_ts/skr.export.d.ts +110 -0
- package/dist_ts/skr.export.js +315 -0
- package/dist_ts/skr.export.ledger.d.ts +95 -0
- package/dist_ts/skr.export.ledger.js +164 -0
- package/dist_ts/skr.export.pdf.d.ts +82 -0
- package/dist_ts/skr.export.pdf.js +548 -0
- package/dist_ts/skr.invoice.adapter.d.ts +98 -0
- package/dist_ts/skr.invoice.adapter.js +476 -0
- package/dist_ts/skr.invoice.booking.d.ts +102 -0
- package/dist_ts/skr.invoice.booking.js +556 -0
- package/dist_ts/skr.invoice.entity.d.ts +287 -0
- package/dist_ts/skr.invoice.entity.js +2 -0
- package/dist_ts/skr.invoice.mapper.d.ts +69 -0
- package/dist_ts/skr.invoice.mapper.js +401 -0
- package/dist_ts/skr.invoice.storage.d.ts +140 -0
- package/dist_ts/skr.invoice.storage.js +529 -0
- package/dist_ts/skr.security.d.ts +65 -0
- package/dist_ts/skr.security.js +319 -0
- package/dist_ts/skr.types.d.ts +1 -0
- package/package.json +18 -12
- package/readme.md +461 -132
- package/ts/index.ts +6 -0
- package/ts/plugins.ts +22 -1
- package/ts/skr.api.ts +489 -2
- package/ts/skr.classes.journalentry.ts +63 -14
- package/ts/skr.classes.ledger.ts +85 -0
- package/ts/skr.classes.reports.ts +64 -21
- package/ts/skr.export.accounts.ts +154 -0
- package/ts/skr.export.balances.ts +270 -0
- package/ts/skr.export.ledger.ts +249 -0
- package/ts/skr.export.pdf.ts +601 -0
- package/ts/skr.export.ts +443 -0
- package/ts/skr.invoice.adapter.ts +581 -0
- package/ts/skr.invoice.booking.ts +738 -0
- package/ts/skr.invoice.entity.ts +351 -0
- package/ts/skr.invoice.mapper.ts +486 -0
- package/ts/skr.invoice.storage.ts +710 -0
- package/ts/skr.security.ts +405 -0
- package/ts/skr.types.ts +1 -0
package/ts/skr.classes.ledger.ts
CHANGED
|
@@ -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:
|
|
125
|
+
amount: balance, // Keep the sign for correct calculation
|
|
126
126
|
};
|
|
127
127
|
|
|
128
128
|
revenueEntries.push(entry);
|
|
129
|
-
totalRevenue +=
|
|
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:
|
|
141
|
+
amount: balance, // Keep the sign - negative balance reduces expenses
|
|
142
142
|
};
|
|
143
143
|
|
|
144
144
|
expenseEntries.push(entry);
|
|
145
|
-
totalExpenses +=
|
|
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
|
|
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
|
|
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:
|
|
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 +=
|
|
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:
|
|
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 +=
|
|
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:
|
|
272
|
+
amount: balance, // Keep the sign for display
|
|
272
273
|
};
|
|
273
274
|
|
|
274
275
|
equityEntries.push(entry);
|
|
275
|
-
totalEquity +=
|
|
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
|
-
|
|
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:
|
|
290
|
+
amount: incomeStatement.netIncome, // Keep the sign
|
|
286
291
|
});
|
|
287
|
-
totalEquity +=
|
|
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
|
-
|
|
349
|
-
|
|
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
|
-
|
|
458
|
-
|
|
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
|
+
}
|