@fin.cx/skr 1.2.0 → 1.2.2

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 (58) hide show
  1. package/{npmextra.json → .smartconfig.json} +12 -6
  2. package/dist_ts/00_commitinfo_data.d.ts +8 -0
  3. package/dist_ts/00_commitinfo_data.js +9 -0
  4. package/dist_ts/index.d.ts +21 -15
  5. package/dist_ts/index.js +16 -16
  6. package/dist_ts/plugins.d.ts +15 -5
  7. package/dist_ts/plugins.js +42 -6
  8. package/dist_ts/skr.api.js +20 -13
  9. package/dist_ts/skr.classes.account.d.ts +36 -3
  10. package/dist_ts/skr.classes.account.js +366 -210
  11. package/dist_ts/skr.classes.chartofaccounts.d.ts +1 -1
  12. package/dist_ts/skr.classes.chartofaccounts.js +13 -6
  13. package/dist_ts/skr.classes.journalentry.d.ts +9 -4
  14. package/dist_ts/skr.classes.journalentry.js +430 -309
  15. package/dist_ts/skr.classes.ledger.js +7 -1
  16. package/dist_ts/skr.classes.reports.js +4 -1
  17. package/dist_ts/skr.classes.transaction.d.ts +9 -4
  18. package/dist_ts/skr.classes.transaction.js +315 -244
  19. package/dist_ts/skr.export.accounts.js +3 -2
  20. package/dist_ts/skr.export.balances.js +4 -2
  21. package/dist_ts/skr.export.js +6 -3
  22. package/dist_ts/skr.export.ledger.js +4 -3
  23. package/dist_ts/skr.export.pdf.js +6 -3
  24. package/dist_ts/skr.invoice.adapter.d.ts +2 -0
  25. package/dist_ts/skr.invoice.adapter.js +22 -10
  26. package/dist_ts/skr.invoice.booking.js +52 -25
  27. package/dist_ts/skr.invoice.mapper.js +84 -82
  28. package/dist_ts/skr.invoice.storage.js +10 -5
  29. package/dist_ts/skr.postingkeys.d.ts +56 -0
  30. package/dist_ts/skr.postingkeys.js +196 -0
  31. package/dist_ts/skr.security.js +24 -21
  32. package/dist_ts/skr.types.d.ts +18 -0
  33. package/dist_ts/skr03.data.js +3 -1
  34. package/dist_ts/skr04.data.js +3 -1
  35. package/license.md +21 -0
  36. package/package.json +31 -23
  37. package/readme.hints.md +54 -1
  38. package/readme.md +215 -632
  39. package/readme.plan.md +1 -1
  40. package/ts/00_commitinfo_data.ts +8 -0
  41. package/ts/index.ts +43 -15
  42. package/ts/plugins.ts +52 -15
  43. package/ts/skr.api.ts +9 -5
  44. package/ts/skr.classes.account.ts +144 -23
  45. package/ts/skr.classes.chartofaccounts.ts +8 -3
  46. package/ts/skr.classes.journalentry.ts +117 -24
  47. package/ts/skr.classes.ledger.ts +4 -0
  48. package/ts/skr.classes.reports.ts +22 -2
  49. package/ts/skr.classes.transaction.ts +40 -22
  50. package/ts/skr.export.pdf.ts +4 -3
  51. package/ts/skr.invoice.adapter.ts +31 -11
  52. package/ts/skr.invoice.booking.ts +54 -30
  53. package/ts/skr.invoice.storage.ts +3 -2
  54. package/ts/skr.postingkeys.ts +252 -0
  55. package/ts/skr.security.ts +23 -22
  56. package/ts/skr.types.ts +26 -0
  57. package/ts/skr03.data.ts +2 -0
  58. package/ts/skr04.data.ts +2 -0
package/readme.plan.md CHANGED
@@ -18,7 +18,7 @@ TypeScript module implementing SKR03 and SKR04 German accounting standards for d
18
18
  - [ ] @push.rocks/smartdata
19
19
  - [ ] @git.zone/tstest (dev dependency)
20
20
  - [ ] Create tsconfig.json based on @push.rocks/smarthash pattern
21
- - [ ] Create npmextra.json for additional configuration
21
+ - [ ] Create .smartconfig.json for additional configuration
22
22
  - [ ] Create .gitignore file
23
23
  - [ ] Create directory structure
24
24
  - [ ] ts/ directory for source code
@@ -0,0 +1,8 @@
1
+ /**
2
+ * autocreated commitinfo by @push.rocks/commitinfo
3
+ */
4
+ export const commitinfo = {
5
+ name: '@fin.cx/skr',
6
+ version: '1.2.2',
7
+ description: 'SKR03 and SKR04 German accounting standards for double-entry bookkeeping'
8
+ }
package/ts/index.ts CHANGED
@@ -1,16 +1,44 @@
1
1
  export * from './skr.types.js';
2
- export * from './skr.classes.account.js';
3
- export * from './skr.classes.transaction.js';
4
- export * from './skr.classes.journalentry.js';
5
- export * from './skr.classes.chartofaccounts.js';
6
- export * from './skr.classes.ledger.js';
7
- export * from './skr.classes.reports.js';
8
- export * from './skr.api.js';
9
- export * from './skr03.data.js';
10
- export * from './skr04.data.js';
11
- export * from './skr.export.js';
12
- export * from './skr.export.ledger.js';
13
- export * from './skr.export.accounts.js';
14
- export * from './skr.export.balances.js';
15
- export * from './skr.export.pdf.js';
16
- export * from './skr.security.js';
2
+ export { Account } from './skr.classes.account.js';
3
+ export { Transaction } from './skr.classes.transaction.js';
4
+ export { JournalEntry } from './skr.classes.journalentry.js';
5
+ export { ChartOfAccounts } from './skr.classes.chartofaccounts.js';
6
+ export { Ledger } from './skr.classes.ledger.js';
7
+ export { Reports } from './skr.classes.reports.js';
8
+ export { SkrApi } from './skr.api.js';
9
+ export { SKR03_ACCOUNTS, SKR03_ACCOUNT_CLASSES } from './skr03.data.js';
10
+ export { SKR04_ACCOUNTS, SKR04_ACCOUNT_CLASSES } from './skr04.data.js';
11
+ export { SkrExport } from './skr.export.js';
12
+ export type {
13
+ IExportOptions,
14
+ IExportMetadata,
15
+ IBagItManifest,
16
+ IDocumentIndex,
17
+ } from './skr.export.js';
18
+ export { LedgerExporter } from './skr.export.ledger.js';
19
+ export type {
20
+ ITransactionDataExport,
21
+ IJournalEntryExport,
22
+ IJournalEntryLineExport,
23
+ ILedgerEntry,
24
+ ILedgerLine,
25
+ IDocumentRef,
26
+ } from './skr.export.ledger.js';
27
+ export { AccountsExporter } from './skr.export.accounts.js';
28
+ export type {
29
+ IAccountDataExport,
30
+ IAccountExportRow,
31
+ } from './skr.export.accounts.js';
32
+ export { BalancesExporter } from './skr.export.balances.js';
33
+ export type {
34
+ IAccountBalanceExport,
35
+ IBalanceExportRow,
36
+ } from './skr.export.balances.js';
37
+ export { PdfReportGenerator } from './skr.export.pdf.js';
38
+ export type { IPdfReportOptions } from './skr.export.pdf.js';
39
+ export { SecurityManager } from './skr.security.js';
40
+ export type {
41
+ ISigningOptions,
42
+ ISignatureResult,
43
+ ITimestampResponse,
44
+ } from './skr.security.js';
package/ts/plugins.ts CHANGED
@@ -3,26 +3,63 @@ import * as smartdata from '@push.rocks/smartdata';
3
3
  import * as smartunique from '@push.rocks/smartunique';
4
4
  import * as smarttime from '@push.rocks/smarttime';
5
5
  import * as smartlog from '@push.rocks/smartlog';
6
- import * as smartfile from '@push.rocks/smartfile';
6
+ import * as smartfsModule from '@push.rocks/smartfs';
7
7
  import * as smarthash from '@push.rocks/smarthash';
8
8
  import * as smartpath from '@push.rocks/smartpath';
9
- import * as smartpdf from '@push.rocks/smartpdf';
9
+ import * as path from 'path';
10
10
 
11
11
  // third party
12
- import * as nodeForge from 'node-forge';
13
12
  import { MerkleTree } from 'merkletreejs';
14
- import * as einvoice from '@fin.cx/einvoice';
15
13
 
16
- export {
17
- smartdata,
18
- smartunique,
19
- smarttime,
20
- smartlog,
21
- smartfile,
22
- smarthash,
23
- smartpath,
24
- smartpdf,
25
- nodeForge,
14
+ const smartfs = new smartfsModule.SmartFs(
15
+ new smartfsModule.SmartFsProviderNode(),
16
+ );
17
+
18
+ const smartfile = {
19
+ fs: {
20
+ ensureDir: async (dirPath: string): Promise<void> => {
21
+ await smartfs.directory(dirPath).create();
22
+ },
23
+ toBuffer: async (filePath: string): Promise<Buffer> => {
24
+ return (await smartfs.file(filePath).read()) as Buffer;
25
+ },
26
+ toStringSync: async (filePath: string): Promise<string> => {
27
+ return (await smartfs.file(filePath).encoding('utf8').read()) as string;
28
+ },
29
+ fileExists: async (filePath: string): Promise<boolean> => {
30
+ return await smartfs.file(filePath).exists();
31
+ },
32
+ listFileTree: async (dirPath: string, pattern: string): Promise<string[]> => {
33
+ const suffix = pattern.replace(/^\*\*\/\*/, '');
34
+ try {
35
+ const entries = await smartfs.directory(dirPath).recursive().list();
36
+ return entries
37
+ .filter((entry) => entry.isFile && entry.path.endsWith(suffix))
38
+ .map((entry) => path.relative(dirPath, entry.path));
39
+ } catch (error) {
40
+ if (error instanceof Error && error.message.includes('ENOENT')) {
41
+ return [];
42
+ }
43
+ throw error;
44
+ }
45
+ },
46
+ },
47
+ memory: {
48
+ toFs: async (content: string | Buffer, filePath: string): Promise<void> => {
49
+ await smartfs.directory(path.dirname(filePath)).create();
50
+ await smartfs.file(filePath).write(content);
51
+ },
52
+ },
53
+ };
54
+
55
+ export {
56
+ smartdata,
57
+ smartunique,
58
+ smarttime,
59
+ smartlog,
60
+ smartfs,
61
+ smartfile,
62
+ smarthash,
63
+ smartpath,
26
64
  MerkleTree,
27
- einvoice
28
65
  };
package/ts/skr.api.ts CHANGED
@@ -534,6 +534,10 @@ export class SkrApi {
534
534
  */
535
535
  private async generatePdfReports(exporter: SkrExport, options: IExportOptions): Promise<void> {
536
536
  if (!this.reports) throw new Error('Reports not initialized');
537
+ const skrType = this.currentSKRType;
538
+ if (!skrType) {
539
+ throw new Error('API not initialized. Call initialize() first.');
540
+ }
537
541
 
538
542
  const pdfOptions: IPdfReportOptions = {
539
543
  companyName: options.companyInfo?.name || 'Unternehmen',
@@ -554,19 +558,19 @@ export class SkrApi {
554
558
  const trialBalance = await this.reports.getTrialBalance({
555
559
  dateFrom: options.dateFrom,
556
560
  dateTo: options.dateTo,
557
- skrType: this.currentSKRType
561
+ skrType,
558
562
  });
559
563
 
560
564
  const incomeStatement = await this.reports.getIncomeStatement({
561
565
  dateFrom: options.dateFrom,
562
566
  dateTo: options.dateTo,
563
- skrType: this.currentSKRType
567
+ skrType,
564
568
  });
565
569
 
566
570
  const balanceSheet = await this.reports.getBalanceSheet({
567
571
  dateFrom: options.dateFrom,
568
572
  dateTo: options.dateTo,
569
- skrType: this.currentSKRType
573
+ skrType,
570
574
  });
571
575
 
572
576
  // Generate PDFs
@@ -702,7 +706,7 @@ export class SkrApi {
702
706
  const transaction = await this.postTransaction(transactions[i]);
703
707
  results.push(transaction);
704
708
  } catch (error) {
705
- errors.push({ index: i, error: error.message });
709
+ errors.push({ index: i, error: error instanceof Error ? error.message : String(error) });
706
710
  }
707
711
  }
708
712
 
@@ -735,7 +739,7 @@ export class SkrApi {
735
739
  const account = await this.createAccount(accounts[i]);
736
740
  results.push(account);
737
741
  } catch (error) {
738
- errors.push({ index: i, error: error.message });
742
+ errors.push({ index: i, error: error instanceof Error ? error.message : String(error) });
739
743
  }
740
744
  }
741
745
 
@@ -2,65 +2,86 @@ import * as plugins from './plugins.js';
2
2
  import { getDb, getDbSync } from './skr.database.js';
3
3
  import type { TAccountType, TSKRType, IAccountData } from './skr.types.js';
4
4
 
5
- const { SmartDataDbDoc, svDb, unI, index, searchable } = plugins.smartdata;
5
+ declare abstract class SmartDataDbDocBase {
6
+ public save(): Promise<void>;
7
+ public delete(): Promise<void>;
8
+ public static getInstance<T>(
9
+ this: new (...args: any[]) => T,
10
+ query: Record<string, any>,
11
+ ): Promise<T | null>;
12
+ public static getInstances<T>(
13
+ this: new (...args: any[]) => T,
14
+ query: Record<string, any>,
15
+ ): Promise<T[]>;
16
+ }
17
+
18
+ const SmartDataDbDoc = plugins.smartdata.SmartDataDbDoc as unknown as typeof SmartDataDbDocBase;
19
+ const Collection = plugins.smartdata.Collection as any;
20
+ const svDb = plugins.smartdata.svDb as any;
21
+ const unI = plugins.smartdata.unI as any;
22
+ const index = plugins.smartdata.index as any;
23
+ const searchable = plugins.smartdata.searchable as any;
6
24
 
7
- @plugins.smartdata.Collection(() => getDbSync())
8
- export class Account extends SmartDataDbDoc<Account, Account> {
25
+ @Collection(() => getDbSync())
26
+ export class Account extends SmartDataDbDoc {
9
27
  @unI()
10
- public id: string;
28
+ public id!: string;
11
29
 
12
30
  @svDb()
13
31
  @index()
14
- public accountNumber: string;
32
+ public accountNumber!: string;
15
33
 
16
34
  @svDb()
17
35
  @searchable()
18
- public accountName: string;
36
+ public accountName!: string;
19
37
 
20
38
  @svDb()
21
39
  @index()
22
- public accountClass: number;
40
+ public accountClass!: number;
23
41
 
24
42
  @svDb()
25
- public accountGroup: number;
43
+ public accountGroup!: number;
26
44
 
27
45
  @svDb()
28
- public accountSubgroup: number;
46
+ public accountSubgroup!: number;
29
47
 
30
48
  @svDb()
31
- public accountType: TAccountType;
49
+ public accountType!: TAccountType;
32
50
 
33
51
  @svDb()
34
52
  @index()
35
- public skrType: TSKRType;
53
+ public skrType!: TSKRType;
36
54
 
37
55
  @svDb()
38
56
  @searchable()
39
- public description: string;
57
+ public description!: string;
40
58
 
41
59
  @svDb()
42
- public vatRate: number;
60
+ public vatRate!: number;
43
61
 
44
62
  @svDb()
45
- public balance: number;
63
+ public balance!: number;
46
64
 
47
65
  @svDb()
48
- public debitTotal: number;
66
+ public debitTotal!: number;
49
67
 
50
68
  @svDb()
51
- public creditTotal: number;
69
+ public creditTotal!: number;
52
70
 
53
71
  @svDb()
54
- public isActive: boolean;
72
+ public isActive!: boolean;
55
73
 
56
74
  @svDb()
57
- public isSystemAccount: boolean;
75
+ public isSystemAccount!: boolean;
58
76
 
59
77
  @svDb()
60
- public createdAt: Date;
78
+ public isAutomaticAccount!: boolean;
61
79
 
62
80
  @svDb()
63
- public updatedAt: Date;
81
+ public createdAt!: Date;
82
+
83
+ @svDb()
84
+ public updatedAt!: Date;
64
85
 
65
86
  constructor(data?: Partial<IAccountData>) {
66
87
  super();
@@ -90,6 +111,7 @@ export class Account extends SmartDataDbDoc<Account, Account> {
90
111
  this.debitTotal = 0;
91
112
  this.creditTotal = 0;
92
113
  this.isSystemAccount = true;
114
+ this.isAutomaticAccount = data.isAutomaticAccount || false;
93
115
  this.createdAt = new Date();
94
116
  this.updatedAt = new Date();
95
117
  }
@@ -157,6 +179,85 @@ export class Account extends SmartDataDbDoc<Account, Account> {
157
179
  );
158
180
  }
159
181
 
182
+ /**
183
+ * Check if account number is in debtor range (10000-69999)
184
+ * Debtor accounts (Debitorenkonten) are individual customer accounts
185
+ */
186
+ public static isInDebtorRange(accountNumber: string): boolean {
187
+ const num = parseInt(accountNumber);
188
+ return num >= 10000 && num <= 69999;
189
+ }
190
+
191
+ /**
192
+ * Check if account number is in creditor range (70000-99999)
193
+ * Creditor accounts (Kreditorenkonten) are individual vendor accounts
194
+ */
195
+ public static isInCreditorRange(accountNumber: string): boolean {
196
+ const num = parseInt(accountNumber);
197
+ return num >= 70000 && num <= 99999;
198
+ }
199
+
200
+ /**
201
+ * Check if account is an automatic account (Automatikkonto)
202
+ * Automatic accounts like 1400/1600 cannot be posted to directly
203
+ */
204
+ public static isAutomaticAccount(accountNumber: string, skrType: TSKRType): boolean {
205
+ // SKR03: 1400 (Forderungen), 1600 (Verbindlichkeiten)
206
+ // SKR04: 1400 (Forderungen), 1600 (Verbindlichkeiten)
207
+ // Note: In SKR04, 3300 is "Fahrzeugkosten" (vehicle costs), NOT an automatic account
208
+ if (skrType === 'SKR03') {
209
+ return accountNumber === '1400' || accountNumber === '1600';
210
+ } else {
211
+ return accountNumber === '1400' || accountNumber === '1600';
212
+ }
213
+ }
214
+
215
+ /**
216
+ * Validate account for posting - throws error if account cannot be posted to
217
+ */
218
+ public static async validateAccountForPosting(
219
+ accountNumber: string,
220
+ skrType: TSKRType,
221
+ ): Promise<void> {
222
+ // Check if automatic account
223
+ if (Account.isAutomaticAccount(accountNumber, skrType)) {
224
+ throw new Error(
225
+ `Account ${accountNumber} is an automatic account (Automatikkonto) and cannot be posted to directly. ` +
226
+ `Use debtor accounts (10000-69999) or creditor accounts (70000-99999) instead.`
227
+ );
228
+ }
229
+
230
+ // Get account to verify it exists
231
+ const account = await Account.getAccountByNumber(accountNumber, skrType);
232
+ if (!account) {
233
+ throw new Error(
234
+ `Account ${accountNumber} not found in ${skrType}. ` +
235
+ `Please create the account before posting.`
236
+ );
237
+ }
238
+
239
+ // Check if account is active
240
+ if (!account.isActive) {
241
+ throw new Error(
242
+ `Account ${accountNumber} is inactive and cannot be posted to.`
243
+ );
244
+ }
245
+ }
246
+
247
+ /**
248
+ * Check if this account instance is a debtor account
249
+ */
250
+ public isDebtorAccount(): boolean {
251
+ return Account.isInDebtorRange(this.accountNumber);
252
+ }
253
+
254
+ /**
255
+ * Check if this account instance is a creditor account
256
+ */
257
+ public isCreditorAccount(): boolean {
258
+ return Account.isInCreditorRange(this.accountNumber);
259
+ }
260
+
160
261
  public async updateBalance(
161
262
  debitAmount: number = 0,
162
263
  creditAmount: number = 0,
@@ -209,19 +310,33 @@ export class Account extends SmartDataDbDoc<Account, Account> {
209
310
 
210
311
  public async beforeSave(): Promise<void> {
211
312
  // Validate account number format
212
- if (!this.accountNumber || this.accountNumber.length !== 4) {
313
+ const accountLength = this.accountNumber?.length || 0;
314
+ if (!this.accountNumber || (accountLength !== 4 && accountLength !== 5)) {
213
315
  throw new Error(
214
- `Invalid account number format: ${this.accountNumber}. Must be 4 digits.`,
316
+ `Invalid account number format: ${this.accountNumber}. Must be 4 digits (standard SKR) or 5 digits (debtor/creditor).`,
215
317
  );
216
318
  }
217
319
 
218
320
  // Validate account number is numeric
219
- if (!/^\d{4}$/.test(this.accountNumber)) {
321
+ if (!/^\d{4,5}$/.test(this.accountNumber)) {
220
322
  throw new Error(
221
323
  `Account number must contain only digits: ${this.accountNumber}`,
222
324
  );
223
325
  }
224
326
 
327
+ // For 5-digit accounts, validate they are in debtor (10000-69999) or creditor (70000-99999) ranges
328
+ if (accountLength === 5) {
329
+ const accountNum = parseInt(this.accountNumber);
330
+ const isDebtor = accountNum >= 10000 && accountNum <= 69999;
331
+ const isCreditor = accountNum >= 70000 && accountNum <= 99999;
332
+
333
+ if (!isDebtor && !isCreditor) {
334
+ throw new Error(
335
+ `5-digit account number ${this.accountNumber} must be in debtor range (10000-69999) or creditor range (70000-99999).`,
336
+ );
337
+ }
338
+ }
339
+
225
340
  // Validate account class matches first digit
226
341
  const firstDigit = parseInt(this.accountNumber[0]);
227
342
  if (this.accountClass !== firstDigit) {
@@ -234,5 +349,11 @@ export class Account extends SmartDataDbDoc<Account, Account> {
234
349
  if (this.skrType !== 'SKR03' && this.skrType !== 'SKR04') {
235
350
  throw new Error(`Invalid SKR type: ${this.skrType}`);
236
351
  }
352
+
353
+ // Mark automatic accounts (Automatikkonten)
354
+ // These are summary accounts that cannot be posted to directly
355
+ if (Account.isAutomaticAccount(this.accountNumber, this.skrType)) {
356
+ this.isAutomaticAccount = true;
357
+ }
237
358
  }
238
359
  }
@@ -262,6 +262,9 @@ export class ChartOfAccounts {
262
262
  * Search accounts
263
263
  */
264
264
  public async searchAccounts(searchTerm: string): Promise<Account[]> {
265
+ if (!this.skrType) {
266
+ throw new Error('SKR type not set. Initialize SKR03 or SKR04 first.');
267
+ }
265
268
  return await Account.searchAccounts(searchTerm, this.skrType);
266
269
  }
267
270
 
@@ -287,10 +290,11 @@ export class ChartOfAccounts {
287
290
 
288
291
  // Apply text search if provided
289
292
  if (filter?.searchTerm) {
290
- const lowerSearchTerm = filter.searchTerm.toLowerCase();
293
+ const searchTerm = filter.searchTerm;
294
+ const lowerSearchTerm = searchTerm.toLowerCase();
291
295
  return accounts.filter(
292
296
  (account) =>
293
- account.accountNumber.includes(filter.searchTerm) ||
297
+ account.accountNumber.includes(searchTerm) ||
294
298
  account.accountName.toLowerCase().includes(lowerSearchTerm) ||
295
299
  account.description.toLowerCase().includes(lowerSearchTerm),
296
300
  );
@@ -468,9 +472,10 @@ export class ChartOfAccounts {
468
472
  await this.createCustomAccount(accountData);
469
473
  importedCount++;
470
474
  } catch (error) {
475
+ const errorMessage = error instanceof Error ? error.message : String(error);
471
476
  this.logger.log(
472
477
  'warn',
473
- `Failed to import account ${parts[0]}: ${error.message}`,
478
+ `Failed to import account ${parts[0]}: ${errorMessage}`,
474
479
  );
475
480
  }
476
481
  }