@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.
- 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.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 +17 -12
- package/readme.md +207 -16
- package/ts/index.ts +6 -0
- package/ts/plugins.ts +22 -1
- package/ts/skr.api.ts +485 -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 +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
|
@@ -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
|
+
}
|