@fin.cx/skr 1.0.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 (44) hide show
  1. package/dist_ts/index.d.ts +10 -0
  2. package/dist_ts/index.js +11 -0
  3. package/dist_ts/plugins.d.ts +5 -0
  4. package/dist_ts/plugins.js +7 -0
  5. package/dist_ts/skr.api.d.ts +181 -0
  6. package/dist_ts/skr.api.js +402 -0
  7. package/dist_ts/skr.classes.account.d.ts +34 -0
  8. package/dist_ts/skr.classes.account.js +224 -0
  9. package/dist_ts/skr.classes.chartofaccounts.d.ts +99 -0
  10. package/dist_ts/skr.classes.chartofaccounts.js +377 -0
  11. package/dist_ts/skr.classes.journalentry.d.ts +33 -0
  12. package/dist_ts/skr.classes.journalentry.js +296 -0
  13. package/dist_ts/skr.classes.ledger.d.ts +61 -0
  14. package/dist_ts/skr.classes.ledger.js +363 -0
  15. package/dist_ts/skr.classes.reports.d.ts +55 -0
  16. package/dist_ts/skr.classes.reports.js +514 -0
  17. package/dist_ts/skr.classes.transaction.d.ts +37 -0
  18. package/dist_ts/skr.classes.transaction.js +263 -0
  19. package/dist_ts/skr.database.d.ts +5 -0
  20. package/dist_ts/skr.database.js +28 -0
  21. package/dist_ts/skr.types.d.ts +126 -0
  22. package/dist_ts/skr.types.js +2 -0
  23. package/dist_ts/skr03.data.d.ts +18 -0
  24. package/dist_ts/skr03.data.js +890 -0
  25. package/dist_ts/skr04.data.d.ts +18 -0
  26. package/dist_ts/skr04.data.js +912 -0
  27. package/npmextra.json +17 -0
  28. package/package.json +61 -0
  29. package/readme.hints.md +3 -0
  30. package/readme.md +409 -0
  31. package/readme.plan.md +243 -0
  32. package/ts/index.ts +10 -0
  33. package/ts/plugins.ts +7 -0
  34. package/ts/skr.api.ts +533 -0
  35. package/ts/skr.classes.account.ts +238 -0
  36. package/ts/skr.classes.chartofaccounts.ts +508 -0
  37. package/ts/skr.classes.journalentry.ts +318 -0
  38. package/ts/skr.classes.ledger.ts +528 -0
  39. package/ts/skr.classes.reports.ts +721 -0
  40. package/ts/skr.classes.transaction.ts +300 -0
  41. package/ts/skr.database.ts +39 -0
  42. package/ts/skr.types.ts +154 -0
  43. package/ts/skr03.data.ts +901 -0
  44. package/ts/skr04.data.ts +923 -0
@@ -0,0 +1,318 @@
1
+ import * as plugins from './plugins.js';
2
+ import { getDbSync } from './skr.database.js';
3
+ import { Account } from './skr.classes.account.js';
4
+ import { Transaction } from './skr.classes.transaction.js';
5
+ import type {
6
+ TSKRType,
7
+ IJournalEntry,
8
+ IJournalEntryLine,
9
+ } from './skr.types.js';
10
+
11
+ const { SmartDataDbDoc, svDb, unI, index, searchable } = plugins.smartdata;
12
+
13
+ @plugins.smartdata.Collection(() => getDbSync())
14
+ export class JournalEntry extends SmartDataDbDoc<JournalEntry, JournalEntry> {
15
+ @unI()
16
+ public id: string;
17
+
18
+ @svDb()
19
+ @index()
20
+ public journalNumber: string;
21
+
22
+ @svDb()
23
+ @index()
24
+ public date: Date;
25
+
26
+ @svDb()
27
+ @searchable()
28
+ public description: string;
29
+
30
+ @svDb()
31
+ @index()
32
+ public reference: string;
33
+
34
+ @svDb()
35
+ public lines: IJournalEntryLine[];
36
+
37
+ @svDb()
38
+ @index()
39
+ public skrType: TSKRType;
40
+
41
+ @svDb()
42
+ public totalDebits: number;
43
+
44
+ @svDb()
45
+ public totalCredits: number;
46
+
47
+ @svDb()
48
+ public isBalanced: boolean;
49
+
50
+ @svDb()
51
+ @index()
52
+ public status: 'draft' | 'posted' | 'reversed';
53
+
54
+ @svDb()
55
+ public transactionIds: string[];
56
+
57
+ @svDb()
58
+ @index()
59
+ public period: string;
60
+
61
+ @svDb()
62
+ public fiscalYear: number;
63
+
64
+ @svDb()
65
+ public createdAt: Date;
66
+
67
+ @svDb()
68
+ public postedAt: Date;
69
+
70
+ @svDb()
71
+ public createdBy: string;
72
+
73
+ constructor(data?: Partial<IJournalEntry>) {
74
+ super();
75
+
76
+ if (data) {
77
+ this.id = plugins.smartunique.shortId();
78
+ this.journalNumber = this.generateJournalNumber();
79
+ this.date = data.date || new Date();
80
+ this.description = data.description || '';
81
+ this.reference = data.reference || '';
82
+ this.lines = data.lines || [];
83
+ this.skrType = data.skrType || 'SKR03';
84
+ this.totalDebits = 0;
85
+ this.totalCredits = 0;
86
+ this.isBalanced = false;
87
+ this.status = 'draft';
88
+ this.transactionIds = [];
89
+
90
+ // Set period and fiscal year
91
+ const entryDate = new Date(this.date);
92
+ this.period = `${entryDate.getFullYear()}-${String(entryDate.getMonth() + 1).padStart(2, '0')}`;
93
+ this.fiscalYear = entryDate.getFullYear();
94
+
95
+ this.createdAt = new Date();
96
+ this.postedAt = null;
97
+ this.createdBy = 'system';
98
+
99
+ // Calculate totals
100
+ this.calculateTotals();
101
+ }
102
+ }
103
+
104
+ private generateJournalNumber(): string {
105
+ const timestamp = Date.now();
106
+ const random = Math.floor(Math.random() * 1000);
107
+ return `JE-${timestamp}-${random}`;
108
+ }
109
+
110
+ private calculateTotals(): void {
111
+ this.totalDebits = 0;
112
+ this.totalCredits = 0;
113
+
114
+ for (const line of this.lines) {
115
+ if (line.debit) {
116
+ this.totalDebits += line.debit;
117
+ }
118
+ if (line.credit) {
119
+ this.totalCredits += line.credit;
120
+ }
121
+ }
122
+
123
+ // Check if balanced (allowing for small rounding differences)
124
+ const difference = Math.abs(this.totalDebits - this.totalCredits);
125
+ this.isBalanced = difference < 0.01;
126
+ }
127
+
128
+ public static async createJournalEntry(
129
+ data: IJournalEntry,
130
+ ): Promise<JournalEntry> {
131
+ const journalEntry = new JournalEntry(data);
132
+ await journalEntry.validate();
133
+ await journalEntry.save();
134
+ return journalEntry;
135
+ }
136
+
137
+ public addLine(line: IJournalEntryLine): void {
138
+ // Validate line
139
+ if (!line.accountNumber) {
140
+ throw new Error('Account number is required for journal entry line');
141
+ }
142
+
143
+ if (!line.debit && !line.credit) {
144
+ throw new Error('Either debit or credit amount is required');
145
+ }
146
+
147
+ if (line.debit && line.credit) {
148
+ throw new Error('A line cannot have both debit and credit amounts');
149
+ }
150
+
151
+ if (line.debit && line.debit < 0) {
152
+ throw new Error('Debit amount must be positive');
153
+ }
154
+
155
+ if (line.credit && line.credit < 0) {
156
+ throw new Error('Credit amount must be positive');
157
+ }
158
+
159
+ this.lines.push(line);
160
+ this.calculateTotals();
161
+ }
162
+
163
+ public removeLine(index: number): void {
164
+ if (index >= 0 && index < this.lines.length) {
165
+ this.lines.splice(index, 1);
166
+ this.calculateTotals();
167
+ }
168
+ }
169
+
170
+ public async validate(): Promise<void> {
171
+ // Check if entry is balanced
172
+ if (!this.isBalanced) {
173
+ throw new Error(
174
+ `Journal entry is not balanced. Debits: ${this.totalDebits}, Credits: ${this.totalCredits}`,
175
+ );
176
+ }
177
+
178
+ // Check minimum lines
179
+ if (this.lines.length < 2) {
180
+ throw new Error('Journal entry must have at least 2 lines');
181
+ }
182
+
183
+ // Validate all accounts exist and are active
184
+ for (const line of this.lines) {
185
+ const account = await Account.getAccountByNumber(
186
+ line.accountNumber,
187
+ this.skrType,
188
+ );
189
+
190
+ if (!account) {
191
+ throw new Error(
192
+ `Account ${line.accountNumber} not found for ${this.skrType}`,
193
+ );
194
+ }
195
+
196
+ if (!account.isActive) {
197
+ throw new Error(`Account ${line.accountNumber} is not active`);
198
+ }
199
+ }
200
+ }
201
+
202
+ public async post(): Promise<void> {
203
+ if (this.status === 'posted') {
204
+ throw new Error('Journal entry is already posted');
205
+ }
206
+
207
+ // Validate before posting
208
+ await this.validate();
209
+
210
+ // Create individual transactions for each debit-credit pair
211
+ const transactions: Transaction[] = [];
212
+
213
+ // Simple posting logic: match debits with credits
214
+ // For complex entries, this could be enhanced with specific pairing logic
215
+ const debitLines = this.lines.filter((l) => l.debit);
216
+ const creditLines = this.lines.filter((l) => l.credit);
217
+
218
+ if (debitLines.length === 1 && creditLines.length === 1) {
219
+ // Simple entry: one debit, one credit
220
+ const transaction = await Transaction.createTransaction({
221
+ date: this.date,
222
+ debitAccount: debitLines[0].accountNumber,
223
+ creditAccount: creditLines[0].accountNumber,
224
+ amount: debitLines[0].debit,
225
+ description: this.description,
226
+ reference: this.reference,
227
+ skrType: this.skrType,
228
+ costCenter: debitLines[0].costCenter,
229
+ });
230
+ transactions.push(transaction);
231
+ } else {
232
+ // Complex entry: multiple debits and/or credits
233
+ // Create transactions to balance the entry
234
+ for (const debitLine of debitLines) {
235
+ for (const creditLine of creditLines) {
236
+ const amount = Math.min(debitLine.debit || 0, creditLine.credit || 0);
237
+
238
+ if (amount > 0) {
239
+ const transaction = await Transaction.createTransaction({
240
+ date: this.date,
241
+ debitAccount: debitLine.accountNumber,
242
+ creditAccount: creditLine.accountNumber,
243
+ amount: amount,
244
+ description: `${this.description} - ${debitLine.description || creditLine.description || ''}`,
245
+ reference: this.reference,
246
+ skrType: this.skrType,
247
+ costCenter: debitLine.costCenter || creditLine.costCenter,
248
+ });
249
+ transactions.push(transaction);
250
+
251
+ // Reduce amounts for tracking
252
+ if (debitLine.debit) debitLine.debit -= amount;
253
+ if (creditLine.credit) creditLine.credit -= amount;
254
+ }
255
+ }
256
+ }
257
+ }
258
+
259
+ // Store transaction IDs
260
+ this.transactionIds = transactions.map((t) => t.id);
261
+
262
+ // Update status
263
+ this.status = 'posted';
264
+ this.postedAt = new Date();
265
+
266
+ await this.save();
267
+ }
268
+
269
+ public async reverse(): Promise<JournalEntry> {
270
+ if (this.status !== 'posted') {
271
+ throw new Error('Can only reverse posted journal entries');
272
+ }
273
+
274
+ // Create reversal entry with swapped debits and credits
275
+ const reversalLines: IJournalEntryLine[] = this.lines.map((line) => ({
276
+ accountNumber: line.accountNumber,
277
+ debit: line.credit, // Swap
278
+ credit: line.debit, // Swap
279
+ description: `Reversal: ${line.description || ''}`,
280
+ costCenter: line.costCenter,
281
+ }));
282
+
283
+ const reversalEntry = new JournalEntry({
284
+ date: new Date(),
285
+ description: `Reversal of ${this.journalNumber}: ${this.description}`,
286
+ reference: `REV-${this.journalNumber}`,
287
+ lines: reversalLines,
288
+ skrType: this.skrType,
289
+ });
290
+
291
+ await reversalEntry.validate();
292
+ await reversalEntry.post();
293
+
294
+ // Update original entry status
295
+ this.status = 'reversed';
296
+ await this.save();
297
+
298
+ return reversalEntry;
299
+ }
300
+
301
+ public async beforeSave(): Promise<void> {
302
+ // Recalculate totals before saving
303
+ this.calculateTotals();
304
+
305
+ // Validate required fields
306
+ if (!this.date) {
307
+ throw new Error('Journal entry date is required');
308
+ }
309
+
310
+ if (!this.description) {
311
+ throw new Error('Journal entry description is required');
312
+ }
313
+
314
+ if (this.lines.length === 0) {
315
+ throw new Error('Journal entry must have at least one line');
316
+ }
317
+ }
318
+ }