@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.
- package/dist_ts/index.d.ts +10 -0
- package/dist_ts/index.js +11 -0
- package/dist_ts/plugins.d.ts +5 -0
- package/dist_ts/plugins.js +7 -0
- package/dist_ts/skr.api.d.ts +181 -0
- package/dist_ts/skr.api.js +402 -0
- package/dist_ts/skr.classes.account.d.ts +34 -0
- package/dist_ts/skr.classes.account.js +224 -0
- package/dist_ts/skr.classes.chartofaccounts.d.ts +99 -0
- package/dist_ts/skr.classes.chartofaccounts.js +377 -0
- package/dist_ts/skr.classes.journalentry.d.ts +33 -0
- package/dist_ts/skr.classes.journalentry.js +296 -0
- package/dist_ts/skr.classes.ledger.d.ts +61 -0
- package/dist_ts/skr.classes.ledger.js +363 -0
- package/dist_ts/skr.classes.reports.d.ts +55 -0
- package/dist_ts/skr.classes.reports.js +514 -0
- package/dist_ts/skr.classes.transaction.d.ts +37 -0
- package/dist_ts/skr.classes.transaction.js +263 -0
- package/dist_ts/skr.database.d.ts +5 -0
- package/dist_ts/skr.database.js +28 -0
- package/dist_ts/skr.types.d.ts +126 -0
- package/dist_ts/skr.types.js +2 -0
- package/dist_ts/skr03.data.d.ts +18 -0
- package/dist_ts/skr03.data.js +890 -0
- package/dist_ts/skr04.data.d.ts +18 -0
- package/dist_ts/skr04.data.js +912 -0
- package/npmextra.json +17 -0
- package/package.json +61 -0
- package/readme.hints.md +3 -0
- package/readme.md +409 -0
- package/readme.plan.md +243 -0
- package/ts/index.ts +10 -0
- package/ts/plugins.ts +7 -0
- package/ts/skr.api.ts +533 -0
- package/ts/skr.classes.account.ts +238 -0
- package/ts/skr.classes.chartofaccounts.ts +508 -0
- package/ts/skr.classes.journalentry.ts +318 -0
- package/ts/skr.classes.ledger.ts +528 -0
- package/ts/skr.classes.reports.ts +721 -0
- package/ts/skr.classes.transaction.ts +300 -0
- package/ts/skr.database.ts +39 -0
- package/ts/skr.types.ts +154 -0
- package/ts/skr03.data.ts +901 -0
- 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
|
+
}
|