@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.
- package/dist_ts/00_commitinfo_data.d.ts +8 -0
- package/dist_ts/00_commitinfo_data.js +9 -0
- 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 +348 -1
- package/dist_ts/skr.classes.account.d.ts +28 -0
- package/dist_ts/skr.classes.account.js +87 -4
- package/dist_ts/skr.classes.journalentry.js +56 -4
- package/dist_ts/skr.classes.ledger.js +5 -1
- 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 +578 -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.postingkeys.d.ts +56 -0
- package/dist_ts/skr.postingkeys.js +196 -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 +19 -0
- package/dist_ts/skr03.data.js +3 -1
- package/dist_ts/skr04.data.js +3 -1
- package/package.json +17 -12
- package/readme.hints.md +54 -1
- package/readme.md +207 -16
- package/ts/00_commitinfo_data.ts +8 -0
- package/ts/index.ts +6 -0
- package/ts/plugins.ts +22 -1
- package/ts/skr.api.ts +485 -0
- package/ts/skr.classes.account.ts +106 -3
- package/ts/skr.classes.journalentry.ts +78 -3
- package/ts/skr.classes.ledger.ts +4 -0
- 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 +760 -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.postingkeys.ts +252 -0
- package/ts/skr.security.ts +405 -0
- package/ts/skr.types.ts +27 -0
- package/ts/skr03.data.ts +2 -0
- 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
|
|
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
|
-
|
|
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
|
-
|
|
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({
|
package/ts/skr.classes.ledger.ts
CHANGED
|
@@ -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
|
+
}
|