@fin.cx/skr 1.1.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 (46) 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 +348 -1
  7. package/dist_ts/skr.export.accounts.d.ts +53 -0
  8. package/dist_ts/skr.export.accounts.js +111 -0
  9. package/dist_ts/skr.export.balances.d.ts +59 -0
  10. package/dist_ts/skr.export.balances.js +205 -0
  11. package/dist_ts/skr.export.d.ts +110 -0
  12. package/dist_ts/skr.export.js +315 -0
  13. package/dist_ts/skr.export.ledger.d.ts +95 -0
  14. package/dist_ts/skr.export.ledger.js +164 -0
  15. package/dist_ts/skr.export.pdf.d.ts +82 -0
  16. package/dist_ts/skr.export.pdf.js +548 -0
  17. package/dist_ts/skr.invoice.adapter.d.ts +98 -0
  18. package/dist_ts/skr.invoice.adapter.js +476 -0
  19. package/dist_ts/skr.invoice.booking.d.ts +102 -0
  20. package/dist_ts/skr.invoice.booking.js +556 -0
  21. package/dist_ts/skr.invoice.entity.d.ts +287 -0
  22. package/dist_ts/skr.invoice.entity.js +2 -0
  23. package/dist_ts/skr.invoice.mapper.d.ts +69 -0
  24. package/dist_ts/skr.invoice.mapper.js +401 -0
  25. package/dist_ts/skr.invoice.storage.d.ts +140 -0
  26. package/dist_ts/skr.invoice.storage.js +529 -0
  27. package/dist_ts/skr.security.d.ts +65 -0
  28. package/dist_ts/skr.security.js +319 -0
  29. package/dist_ts/skr.types.d.ts +1 -0
  30. package/package.json +17 -12
  31. package/readme.md +207 -16
  32. package/ts/index.ts +6 -0
  33. package/ts/plugins.ts +22 -1
  34. package/ts/skr.api.ts +485 -0
  35. package/ts/skr.export.accounts.ts +154 -0
  36. package/ts/skr.export.balances.ts +270 -0
  37. package/ts/skr.export.ledger.ts +249 -0
  38. package/ts/skr.export.pdf.ts +601 -0
  39. package/ts/skr.export.ts +443 -0
  40. package/ts/skr.invoice.adapter.ts +581 -0
  41. package/ts/skr.invoice.booking.ts +738 -0
  42. package/ts/skr.invoice.entity.ts +351 -0
  43. package/ts/skr.invoice.mapper.ts +486 -0
  44. package/ts/skr.invoice.storage.ts +710 -0
  45. package/ts/skr.security.ts +405 -0
  46. package/ts/skr.types.ts +1 -0
@@ -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
+ }
@@ -0,0 +1,249 @@
1
+ import * as plugins from './plugins.js';
2
+ import * as path from 'path';
3
+ import type { ITransactionData, IJournalEntry, IJournalEntryLine } from './skr.types.js';
4
+ import { createWriteStream, type WriteStream } from 'fs';
5
+
6
+ // Extended interfaces for export with additional tracking fields
7
+ export interface ITransactionDataExport extends ITransactionData {
8
+ _id?: string;
9
+ postingDate?: Date;
10
+ currency?: string;
11
+ createdAt?: Date | string;
12
+ modifiedAt?: Date | string;
13
+ reversalOf?: string;
14
+ reversedBy?: string;
15
+ taxCode?: string;
16
+ project?: string;
17
+ vatAccount?: string;
18
+ }
19
+
20
+ export interface IJournalEntryExport extends IJournalEntry {
21
+ _id?: string;
22
+ postingDate?: Date;
23
+ currency?: string;
24
+ journal?: string;
25
+ createdAt?: Date | string;
26
+ modifiedAt?: Date | string;
27
+ reversalOf?: string;
28
+ reversedBy?: string;
29
+ }
30
+
31
+ export interface IJournalEntryLineExport extends IJournalEntryLine {
32
+ taxCode?: string;
33
+ project?: string;
34
+ }
35
+
36
+ export interface ILedgerEntry {
37
+ schema_version: string;
38
+ entry_id: string;
39
+ booking_date: string;
40
+ posting_date: string;
41
+ period?: string;
42
+ currency: string;
43
+ journal: string;
44
+ description: string;
45
+ reference?: string;
46
+ lines: ILedgerLine[];
47
+ document_refs?: IDocumentRef[];
48
+ created_at: string;
49
+ modified_at?: string;
50
+ user?: string;
51
+ reversal_of?: string;
52
+ reversed_by?: string;
53
+ }
54
+
55
+ export interface ILedgerLine {
56
+ posting_id: string;
57
+ account_code: string;
58
+ debit: string;
59
+ credit: string;
60
+ tax_code?: string;
61
+ cost_center?: string;
62
+ project?: string;
63
+ description?: string;
64
+ }
65
+
66
+ export interface IDocumentRef {
67
+ content_hash: string;
68
+ doc_role: 'invoice' | 'receipt' | 'contract' | 'bank-statement' | 'other';
69
+ doc_mime: string;
70
+ doc_original_name?: string;
71
+ }
72
+
73
+ export class LedgerExporter {
74
+ private exportPath: string;
75
+ private stream: WriteStream | null = null;
76
+ private entryCount: number = 0;
77
+
78
+ constructor(exportPath: string) {
79
+ this.exportPath = exportPath;
80
+ }
81
+
82
+ /**
83
+ * Initializes the NDJSON export stream
84
+ */
85
+ public async initialize(): Promise<void> {
86
+ const ledgerPath = path.join(this.exportPath, 'data', 'accounting', 'ledger.ndjson');
87
+ await plugins.smartfile.fs.ensureDir(path.dirname(ledgerPath));
88
+
89
+ this.stream = createWriteStream(ledgerPath, {
90
+ encoding: 'utf8',
91
+ flags: 'w'
92
+ });
93
+ }
94
+
95
+ /**
96
+ * Exports a transaction as a ledger entry
97
+ */
98
+ public async exportTransaction(transaction: ITransactionDataExport): Promise<void> {
99
+ if (!this.stream) {
100
+ throw new Error('Ledger exporter not initialized');
101
+ }
102
+
103
+ const entry: ILedgerEntry = {
104
+ schema_version: '1.0',
105
+ entry_id: transaction._id || plugins.smartunique.shortId(),
106
+ booking_date: this.formatDate(transaction.date),
107
+ posting_date: this.formatDate(transaction.postingDate || transaction.date),
108
+ currency: transaction.currency || 'EUR',
109
+ journal: 'GL',
110
+ description: transaction.description,
111
+ reference: transaction.reference,
112
+ lines: [],
113
+ created_at: transaction.createdAt ? new Date(transaction.createdAt).toISOString() : new Date().toISOString(),
114
+ modified_at: transaction.modifiedAt ? new Date(transaction.modifiedAt).toISOString() : undefined,
115
+ reversal_of: transaction.reversalOf,
116
+ reversed_by: transaction.reversedBy
117
+ };
118
+
119
+ // Add debit line
120
+ if (transaction.amount > 0) {
121
+ entry.lines.push({
122
+ posting_id: `${entry.entry_id}-1`,
123
+ account_code: transaction.debitAccount,
124
+ debit: transaction.amount.toFixed(2),
125
+ credit: '0.00',
126
+ tax_code: transaction.taxCode,
127
+ cost_center: transaction.costCenter,
128
+ project: transaction.project
129
+ });
130
+
131
+ // Add credit line
132
+ entry.lines.push({
133
+ posting_id: `${entry.entry_id}-2`,
134
+ account_code: transaction.creditAccount,
135
+ debit: '0.00',
136
+ credit: transaction.amount.toFixed(2)
137
+ });
138
+ }
139
+
140
+ // Add VAT lines if applicable
141
+ if (transaction.vatAmount && transaction.vatAmount > 0) {
142
+ entry.lines.push({
143
+ posting_id: `${entry.entry_id}-3`,
144
+ account_code: transaction.vatAccount || '1576', // Default VAT account
145
+ debit: transaction.vatAmount.toFixed(2),
146
+ credit: '0.00',
147
+ description: 'Vorsteuer'
148
+ });
149
+ }
150
+
151
+ await this.writeLine(entry);
152
+ }
153
+
154
+ /**
155
+ * Exports a journal entry
156
+ */
157
+ public async exportJournalEntry(journalEntry: IJournalEntryExport): Promise<void> {
158
+ if (!this.stream) {
159
+ throw new Error('Ledger exporter not initialized');
160
+ }
161
+
162
+ const entry: ILedgerEntry = {
163
+ schema_version: '1.0',
164
+ entry_id: journalEntry._id || plugins.smartunique.shortId(),
165
+ booking_date: this.formatDate(journalEntry.date),
166
+ posting_date: this.formatDate(journalEntry.postingDate || journalEntry.date),
167
+ currency: journalEntry.currency || 'EUR',
168
+ journal: journalEntry.journal || 'GL',
169
+ description: journalEntry.description,
170
+ reference: journalEntry.reference,
171
+ lines: [],
172
+ created_at: journalEntry.createdAt ? new Date(journalEntry.createdAt).toISOString() : new Date().toISOString(),
173
+ modified_at: journalEntry.modifiedAt ? new Date(journalEntry.modifiedAt).toISOString() : undefined,
174
+ reversal_of: journalEntry.reversalOf,
175
+ reversed_by: journalEntry.reversedBy
176
+ };
177
+
178
+ // Convert journal entry lines
179
+ journalEntry.lines.forEach((line, index) => {
180
+ const extLine = line as IJournalEntryLineExport;
181
+ entry.lines.push({
182
+ posting_id: `${entry.entry_id}-${index + 1}`,
183
+ account_code: line.accountNumber,
184
+ debit: (line.debit || 0).toFixed(2),
185
+ credit: (line.credit || 0).toFixed(2),
186
+ tax_code: extLine.taxCode,
187
+ cost_center: line.costCenter,
188
+ project: extLine.project,
189
+ description: line.description
190
+ });
191
+ });
192
+
193
+ await this.writeLine(entry);
194
+ }
195
+
196
+ /**
197
+ * Writes a single NDJSON line
198
+ */
199
+ private async writeLine(entry: ILedgerEntry): Promise<void> {
200
+ return new Promise((resolve, reject) => {
201
+ if (!this.stream) {
202
+ reject(new Error('Stream not initialized'));
203
+ return;
204
+ }
205
+
206
+ const line = JSON.stringify(entry) + '\n';
207
+ this.stream.write(line, (error) => {
208
+ if (error) {
209
+ reject(error);
210
+ } else {
211
+ this.entryCount++;
212
+ resolve();
213
+ }
214
+ });
215
+ });
216
+ }
217
+
218
+ /**
219
+ * Formats a date to ISO date string
220
+ */
221
+ private formatDate(date: Date | string): string {
222
+ if (typeof date === 'string') {
223
+ return date.split('T')[0];
224
+ }
225
+ return date.toISOString().split('T')[0];
226
+ }
227
+
228
+ /**
229
+ * Closes the export stream
230
+ */
231
+ public async close(): Promise<number> {
232
+ return new Promise((resolve) => {
233
+ if (this.stream) {
234
+ this.stream.end(() => {
235
+ resolve(this.entryCount);
236
+ });
237
+ } else {
238
+ resolve(this.entryCount);
239
+ }
240
+ });
241
+ }
242
+
243
+ /**
244
+ * Gets the number of exported entries
245
+ */
246
+ public getEntryCount(): number {
247
+ return this.entryCount;
248
+ }
249
+ }