@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.
- package/{npmextra.json → .smartconfig.json} +12 -6
- package/dist_ts/00_commitinfo_data.d.ts +8 -0
- package/dist_ts/00_commitinfo_data.js +9 -0
- package/dist_ts/index.d.ts +21 -15
- package/dist_ts/index.js +16 -16
- package/dist_ts/plugins.d.ts +15 -5
- package/dist_ts/plugins.js +42 -6
- package/dist_ts/skr.api.js +20 -13
- package/dist_ts/skr.classes.account.d.ts +36 -3
- package/dist_ts/skr.classes.account.js +366 -210
- package/dist_ts/skr.classes.chartofaccounts.d.ts +1 -1
- package/dist_ts/skr.classes.chartofaccounts.js +13 -6
- package/dist_ts/skr.classes.journalentry.d.ts +9 -4
- package/dist_ts/skr.classes.journalentry.js +430 -309
- package/dist_ts/skr.classes.ledger.js +7 -1
- package/dist_ts/skr.classes.reports.js +4 -1
- package/dist_ts/skr.classes.transaction.d.ts +9 -4
- package/dist_ts/skr.classes.transaction.js +315 -244
- package/dist_ts/skr.export.accounts.js +3 -2
- package/dist_ts/skr.export.balances.js +4 -2
- package/dist_ts/skr.export.js +6 -3
- package/dist_ts/skr.export.ledger.js +4 -3
- package/dist_ts/skr.export.pdf.js +6 -3
- package/dist_ts/skr.invoice.adapter.d.ts +2 -0
- package/dist_ts/skr.invoice.adapter.js +22 -10
- package/dist_ts/skr.invoice.booking.js +52 -25
- package/dist_ts/skr.invoice.mapper.js +84 -82
- package/dist_ts/skr.invoice.storage.js +10 -5
- package/dist_ts/skr.postingkeys.d.ts +56 -0
- package/dist_ts/skr.postingkeys.js +196 -0
- package/dist_ts/skr.security.js +24 -21
- package/dist_ts/skr.types.d.ts +18 -0
- package/dist_ts/skr03.data.js +3 -1
- package/dist_ts/skr04.data.js +3 -1
- package/license.md +21 -0
- package/package.json +31 -23
- package/readme.hints.md +54 -1
- package/readme.md +215 -632
- package/readme.plan.md +1 -1
- package/ts/00_commitinfo_data.ts +8 -0
- package/ts/index.ts +43 -15
- package/ts/plugins.ts +52 -15
- package/ts/skr.api.ts +9 -5
- package/ts/skr.classes.account.ts +144 -23
- package/ts/skr.classes.chartofaccounts.ts +8 -3
- package/ts/skr.classes.journalentry.ts +117 -24
- package/ts/skr.classes.ledger.ts +4 -0
- package/ts/skr.classes.reports.ts +22 -2
- package/ts/skr.classes.transaction.ts +40 -22
- package/ts/skr.export.pdf.ts +4 -3
- package/ts/skr.invoice.adapter.ts +31 -11
- package/ts/skr.invoice.booking.ts +54 -30
- package/ts/skr.invoice.storage.ts +3 -2
- package/ts/skr.postingkeys.ts +252 -0
- package/ts/skr.security.ts +23 -22
- package/ts/skr.types.ts +26 -0
- package/ts/skr03.data.ts +2 -0
- package/ts/skr04.data.ts +2 -0
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import * as plugins from './plugins.js';
|
|
2
2
|
import { JournalEntry } from './skr.classes.journalentry.js';
|
|
3
3
|
import { SKRInvoiceMapper } from './skr.invoice.mapper.js';
|
|
4
|
+
import { suggestPostingKey } from './skr.postingkeys.js';
|
|
4
5
|
import type { TSKRType, IJournalEntry, IJournalEntryLine } from './skr.types.js';
|
|
5
6
|
import type {
|
|
6
7
|
IInvoice,
|
|
@@ -130,10 +131,11 @@ export class InvoiceBookingEngine {
|
|
|
130
131
|
};
|
|
131
132
|
} catch (error) {
|
|
132
133
|
this.logger.log('error', `Failed to book invoice: ${error}`);
|
|
134
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
133
135
|
return {
|
|
134
136
|
success: false,
|
|
135
137
|
confidence: 0,
|
|
136
|
-
errors: [`Booking failed: ${
|
|
138
|
+
errors: [`Booking failed: ${errorMessage}`]
|
|
137
139
|
};
|
|
138
140
|
}
|
|
139
141
|
}
|
|
@@ -196,14 +198,16 @@ export class InvoiceBookingEngine {
|
|
|
196
198
|
lines.push({
|
|
197
199
|
accountNumber,
|
|
198
200
|
credit: Math.abs(amount),
|
|
199
|
-
description: this.getAccountDescription(accountNumber, group)
|
|
201
|
+
description: this.getAccountDescription(accountNumber, group),
|
|
202
|
+
postingKey: 9 // 19% input VAT for expenses
|
|
200
203
|
});
|
|
201
204
|
} else {
|
|
202
205
|
// Regular invoice: debit expense account
|
|
203
206
|
lines.push({
|
|
204
207
|
accountNumber,
|
|
205
208
|
debit: Math.abs(amount),
|
|
206
|
-
description: this.getAccountDescription(accountNumber, group)
|
|
209
|
+
description: this.getAccountDescription(accountNumber, group),
|
|
210
|
+
postingKey: 9 // 19% input VAT for expenses
|
|
207
211
|
});
|
|
208
212
|
}
|
|
209
213
|
}
|
|
@@ -221,14 +225,16 @@ export class InvoiceBookingEngine {
|
|
|
221
225
|
lines.push({
|
|
222
226
|
accountNumber: controlAccount,
|
|
223
227
|
debit: totalAmount,
|
|
224
|
-
description: `${invoice.supplier.name} - Credit Note ${invoice.invoiceNumber}
|
|
228
|
+
description: `${invoice.supplier.name} - Credit Note ${invoice.invoiceNumber}`,
|
|
229
|
+
postingKey: 40 // Tax-free for control account
|
|
225
230
|
});
|
|
226
231
|
} else {
|
|
227
232
|
// Regular invoice: credit vendor account
|
|
228
233
|
lines.push({
|
|
229
234
|
accountNumber: controlAccount,
|
|
230
235
|
credit: totalAmount,
|
|
231
|
-
description: `${invoice.supplier.name} - Invoice ${invoice.invoiceNumber}
|
|
236
|
+
description: `${invoice.supplier.name} - Invoice ${invoice.invoiceNumber}`,
|
|
237
|
+
postingKey: 40 // Tax-free for control account
|
|
232
238
|
});
|
|
233
239
|
}
|
|
234
240
|
|
|
@@ -257,14 +263,16 @@ export class InvoiceBookingEngine {
|
|
|
257
263
|
lines.push({
|
|
258
264
|
accountNumber,
|
|
259
265
|
debit: Math.abs(amount),
|
|
260
|
-
description: this.getAccountDescription(accountNumber, group)
|
|
266
|
+
description: this.getAccountDescription(accountNumber, group),
|
|
267
|
+
postingKey: 9 // 19% output VAT for revenue
|
|
261
268
|
});
|
|
262
269
|
} else {
|
|
263
270
|
// Regular invoice: credit revenue account
|
|
264
271
|
lines.push({
|
|
265
272
|
accountNumber,
|
|
266
273
|
credit: Math.abs(amount),
|
|
267
|
-
description: this.getAccountDescription(accountNumber, group)
|
|
274
|
+
description: this.getAccountDescription(accountNumber, group),
|
|
275
|
+
postingKey: 9 // 19% output VAT for revenue
|
|
268
276
|
});
|
|
269
277
|
}
|
|
270
278
|
}
|
|
@@ -282,14 +290,16 @@ export class InvoiceBookingEngine {
|
|
|
282
290
|
lines.push({
|
|
283
291
|
accountNumber: controlAccount,
|
|
284
292
|
credit: totalAmount,
|
|
285
|
-
description: `${invoice.customer.name} - Credit Note ${invoice.invoiceNumber}
|
|
293
|
+
description: `${invoice.customer.name} - Credit Note ${invoice.invoiceNumber}`,
|
|
294
|
+
postingKey: 40 // Tax-free for control account
|
|
286
295
|
});
|
|
287
296
|
} else {
|
|
288
297
|
// Regular invoice: debit customer account
|
|
289
298
|
lines.push({
|
|
290
299
|
accountNumber: controlAccount,
|
|
291
300
|
debit: totalAmount,
|
|
292
|
-
description: `${invoice.customer.name} - Invoice ${invoice.invoiceNumber}
|
|
301
|
+
description: `${invoice.customer.name} - Invoice ${invoice.invoiceNumber}`,
|
|
302
|
+
postingKey: 40 // Tax-free for control account
|
|
293
303
|
});
|
|
294
304
|
}
|
|
295
305
|
|
|
@@ -325,20 +335,23 @@ export class InvoiceBookingEngine {
|
|
|
325
335
|
|
|
326
336
|
const amount = Math.abs(vatBreak.taxAmount);
|
|
327
337
|
const description = `VAT ${vatBreak.vatCategory.rate}%`;
|
|
328
|
-
|
|
338
|
+
const vatRate = vatBreak.vatCategory.rate;
|
|
339
|
+
// Select posting key based on VAT rate: 8 for 7%, 9 for 19%
|
|
340
|
+
const postingKey = vatRate === 7 ? 8 : 9;
|
|
341
|
+
|
|
329
342
|
if (direction === 'input') {
|
|
330
343
|
// Input VAT (Vorsteuer)
|
|
331
344
|
if (reverseDirection) {
|
|
332
|
-
lines.push({ accountNumber: vatAccount, credit: amount, description });
|
|
345
|
+
lines.push({ accountNumber: vatAccount, credit: amount, description, postingKey });
|
|
333
346
|
} else {
|
|
334
|
-
lines.push({ accountNumber: vatAccount, debit: amount, description });
|
|
347
|
+
lines.push({ accountNumber: vatAccount, debit: amount, description, postingKey });
|
|
335
348
|
}
|
|
336
349
|
} else {
|
|
337
350
|
// Output VAT (Umsatzsteuer)
|
|
338
351
|
if (reverseDirection) {
|
|
339
|
-
lines.push({ accountNumber: vatAccount, debit: amount, description });
|
|
352
|
+
lines.push({ accountNumber: vatAccount, debit: amount, description, postingKey });
|
|
340
353
|
} else {
|
|
341
|
-
lines.push({ accountNumber: vatAccount, credit: amount, description });
|
|
354
|
+
lines.push({ accountNumber: vatAccount, credit: amount, description, postingKey });
|
|
342
355
|
}
|
|
343
356
|
}
|
|
344
357
|
}
|
|
@@ -404,12 +417,14 @@ export class InvoiceBookingEngine {
|
|
|
404
417
|
{
|
|
405
418
|
accountNumber: inputVATAccount,
|
|
406
419
|
debit: amount,
|
|
407
|
-
description: `Reverse charge input VAT ${vatBreak.vatCategory.rate}
|
|
420
|
+
description: `Reverse charge input VAT ${vatBreak.vatCategory.rate}%`,
|
|
421
|
+
postingKey: 94 // Reverse charge posting key
|
|
408
422
|
},
|
|
409
423
|
{
|
|
410
424
|
accountNumber: outputVATAccount,
|
|
411
425
|
credit: amount,
|
|
412
|
-
description: `Reverse charge output VAT ${vatBreak.vatCategory.rate}
|
|
426
|
+
description: `Reverse charge output VAT ${vatBreak.vatCategory.rate}%`,
|
|
427
|
+
postingKey: 94 // Reverse charge posting key
|
|
413
428
|
}
|
|
414
429
|
);
|
|
415
430
|
}
|
|
@@ -462,24 +477,27 @@ export class InvoiceBookingEngine {
|
|
|
462
477
|
{
|
|
463
478
|
accountNumber: controlAccount,
|
|
464
479
|
debit: fullAmount,
|
|
465
|
-
description: `Payment to ${invoice.supplier.name}
|
|
480
|
+
description: `Payment to ${invoice.supplier.name}`,
|
|
481
|
+
postingKey: 3 // Payment with VAT
|
|
466
482
|
},
|
|
467
483
|
{
|
|
468
484
|
accountNumber: '1000', // Bank account (would be configurable)
|
|
469
485
|
credit: paymentAmount,
|
|
470
|
-
description: `Bank payment ${payment.endToEndId || payment.paymentId}
|
|
486
|
+
description: `Bank payment ${payment.endToEndId || payment.paymentId}`,
|
|
487
|
+
postingKey: 40 // Tax-free for bank account
|
|
471
488
|
}
|
|
472
489
|
);
|
|
473
|
-
|
|
490
|
+
|
|
474
491
|
// Book skonto if taken
|
|
475
492
|
if (skontoAmount > 0) {
|
|
476
493
|
const skontoAccounts = this.mapper.getSkontoAccounts(invoice);
|
|
477
494
|
lines.push({
|
|
478
495
|
accountNumber: skontoAccounts.skontoAccount,
|
|
479
496
|
credit: skontoAmount,
|
|
480
|
-
description: `Skonto received
|
|
497
|
+
description: `Skonto received`,
|
|
498
|
+
postingKey: 40 // Tax-free for skonto
|
|
481
499
|
});
|
|
482
|
-
|
|
500
|
+
|
|
483
501
|
// VAT correction for skonto
|
|
484
502
|
if (rules.skontoMethod === 'gross') {
|
|
485
503
|
const effectiveRate = this.calculateEffectiveVATRate(invoice);
|
|
@@ -488,7 +506,8 @@ export class InvoiceBookingEngine {
|
|
|
488
506
|
{
|
|
489
507
|
accountNumber: skontoAccounts.vatCorrectionAccount,
|
|
490
508
|
credit: vatCorrection,
|
|
491
|
-
description: `Skonto VAT correction
|
|
509
|
+
description: `Skonto VAT correction`,
|
|
510
|
+
postingKey: 40 // Tax-free for correction
|
|
492
511
|
}
|
|
493
512
|
);
|
|
494
513
|
}
|
|
@@ -499,24 +518,27 @@ export class InvoiceBookingEngine {
|
|
|
499
518
|
{
|
|
500
519
|
accountNumber: '1000', // Bank account
|
|
501
520
|
debit: paymentAmount,
|
|
502
|
-
description: `Payment from ${invoice.customer.name}
|
|
521
|
+
description: `Payment from ${invoice.customer.name}`,
|
|
522
|
+
postingKey: 40 // Tax-free for bank account
|
|
503
523
|
},
|
|
504
524
|
{
|
|
505
525
|
accountNumber: controlAccount,
|
|
506
526
|
credit: fullAmount,
|
|
507
|
-
description: `Customer payment ${payment.endToEndId || payment.paymentId}
|
|
527
|
+
description: `Customer payment ${payment.endToEndId || payment.paymentId}`,
|
|
528
|
+
postingKey: 3 // Payment with VAT
|
|
508
529
|
}
|
|
509
530
|
);
|
|
510
|
-
|
|
531
|
+
|
|
511
532
|
// Book skonto if granted
|
|
512
533
|
if (skontoAmount > 0) {
|
|
513
534
|
const skontoAccounts = this.mapper.getSkontoAccounts(invoice);
|
|
514
535
|
lines.push({
|
|
515
536
|
accountNumber: skontoAccounts.skontoAccount,
|
|
516
537
|
debit: skontoAmount,
|
|
517
|
-
description: `Skonto granted
|
|
538
|
+
description: `Skonto granted`,
|
|
539
|
+
postingKey: 40 // Tax-free for skonto
|
|
518
540
|
});
|
|
519
|
-
|
|
541
|
+
|
|
520
542
|
// VAT correction for skonto
|
|
521
543
|
if (rules.skontoMethod === 'gross') {
|
|
522
544
|
const effectiveRate = this.calculateEffectiveVATRate(invoice);
|
|
@@ -525,7 +547,8 @@ export class InvoiceBookingEngine {
|
|
|
525
547
|
{
|
|
526
548
|
accountNumber: skontoAccounts.vatCorrectionAccount,
|
|
527
549
|
debit: vatCorrection,
|
|
528
|
-
description: `Skonto VAT correction
|
|
550
|
+
description: `Skonto VAT correction`,
|
|
551
|
+
postingKey: 40 // Tax-free for correction
|
|
529
552
|
}
|
|
530
553
|
);
|
|
531
554
|
}
|
|
@@ -552,10 +575,11 @@ export class InvoiceBookingEngine {
|
|
|
552
575
|
};
|
|
553
576
|
} catch (error) {
|
|
554
577
|
this.logger.log('error', `Failed to book payment: ${error}`);
|
|
578
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
555
579
|
return {
|
|
556
580
|
success: false,
|
|
557
581
|
confidence: 0,
|
|
558
|
-
errors: [`Payment booking failed: ${
|
|
582
|
+
errors: [`Payment booking failed: ${errorMessage}`]
|
|
559
583
|
};
|
|
560
584
|
}
|
|
561
585
|
}
|
|
@@ -735,4 +759,4 @@ export class InvoiceBookingEngine {
|
|
|
735
759
|
|
|
736
760
|
return Array.from(accounts);
|
|
737
761
|
}
|
|
738
|
-
}
|
|
762
|
+
}
|
|
@@ -224,7 +224,8 @@ export class InvoiceStorage {
|
|
|
224
224
|
return contentHash;
|
|
225
225
|
} catch (error) {
|
|
226
226
|
this.logger.log('error', `Failed to store invoice: ${error}`);
|
|
227
|
-
|
|
227
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
228
|
+
throw new Error(`Invoice storage failed: ${errorMessage}`);
|
|
228
229
|
}
|
|
229
230
|
}
|
|
230
231
|
|
|
@@ -707,4 +708,4 @@ export class InvoiceStorage {
|
|
|
707
708
|
|
|
708
709
|
this.logger.log('info', `Updated metadata for invoice: ${contentHash}`);
|
|
709
710
|
}
|
|
710
|
-
}
|
|
711
|
+
}
|
|
@@ -0,0 +1,252 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* DATEV Posting Keys (Buchungsschlüssel) for German Accounting
|
|
3
|
+
*
|
|
4
|
+
* Posting keys control automatic VAT booking and are automatically checked
|
|
5
|
+
* in German tax audits (Betriebsprüfungen). Using incorrect posting keys
|
|
6
|
+
* can have serious tax consequences.
|
|
7
|
+
*
|
|
8
|
+
* Reference: DATEV Buchungsschlüssel-Verzeichnis
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import type { TPostingKey, IPostingKeyRule } from './skr.types.js';
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Posting key definitions with validation rules
|
|
15
|
+
*/
|
|
16
|
+
export const POSTING_KEY_RULES: Record<TPostingKey, IPostingKeyRule> = {
|
|
17
|
+
3: {
|
|
18
|
+
key: 3,
|
|
19
|
+
description: 'Zahlungseingang mit 19% Umsatzsteuer',
|
|
20
|
+
vatRate: 19,
|
|
21
|
+
requiresVAT: true,
|
|
22
|
+
disablesVATAutomatism: false,
|
|
23
|
+
allowedScenarios: ['domestic_taxed']
|
|
24
|
+
},
|
|
25
|
+
8: {
|
|
26
|
+
key: 8,
|
|
27
|
+
description: '7% Vorsteuer',
|
|
28
|
+
vatRate: 7,
|
|
29
|
+
requiresVAT: true,
|
|
30
|
+
disablesVATAutomatism: false,
|
|
31
|
+
allowedScenarios: ['domestic_taxed']
|
|
32
|
+
},
|
|
33
|
+
9: {
|
|
34
|
+
key: 9,
|
|
35
|
+
description: '19% Vorsteuer',
|
|
36
|
+
vatRate: 19,
|
|
37
|
+
requiresVAT: true,
|
|
38
|
+
disablesVATAutomatism: false,
|
|
39
|
+
allowedScenarios: ['domestic_taxed']
|
|
40
|
+
},
|
|
41
|
+
19: {
|
|
42
|
+
key: 19,
|
|
43
|
+
description: '19% Vorsteuer bei innergemeinschaftlichen Lieferungen',
|
|
44
|
+
vatRate: 19,
|
|
45
|
+
requiresVAT: true,
|
|
46
|
+
disablesVATAutomatism: false,
|
|
47
|
+
allowedScenarios: ['intra_eu']
|
|
48
|
+
},
|
|
49
|
+
40: {
|
|
50
|
+
key: 40,
|
|
51
|
+
description: 'Steuerfrei / Aufhebung der Automatik',
|
|
52
|
+
vatRate: 0,
|
|
53
|
+
requiresVAT: false,
|
|
54
|
+
disablesVATAutomatism: true,
|
|
55
|
+
allowedScenarios: ['tax_free', 'export', 'reverse_charge']
|
|
56
|
+
},
|
|
57
|
+
94: {
|
|
58
|
+
key: 94,
|
|
59
|
+
description: '19% Vorsteuer/Umsatzsteuer bei Erwerb aus EU oder Drittland (Reverse Charge)',
|
|
60
|
+
vatRate: 19,
|
|
61
|
+
requiresVAT: true,
|
|
62
|
+
disablesVATAutomatism: false,
|
|
63
|
+
allowedScenarios: ['reverse_charge', 'intra_eu', 'third_country']
|
|
64
|
+
}
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Validate posting key for a journal entry line
|
|
69
|
+
*/
|
|
70
|
+
export function validatePostingKey(
|
|
71
|
+
postingKey: TPostingKey,
|
|
72
|
+
accountNumber: string,
|
|
73
|
+
amount: number,
|
|
74
|
+
vatAmount?: number,
|
|
75
|
+
taxScenario?: string
|
|
76
|
+
): { isValid: boolean; errors: string[]; warnings: string[] } {
|
|
77
|
+
const errors: string[] = [];
|
|
78
|
+
const warnings: string[] = [];
|
|
79
|
+
|
|
80
|
+
// Get posting key rule
|
|
81
|
+
const rule = POSTING_KEY_RULES[postingKey];
|
|
82
|
+
if (!rule) {
|
|
83
|
+
errors.push(`Invalid posting key: ${postingKey}`);
|
|
84
|
+
return { isValid: false, errors, warnings };
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// Validate VAT requirement
|
|
88
|
+
// Skip VAT amount requirement if:
|
|
89
|
+
// 1. Posting TO a VAT account (the line itself IS the VAT)
|
|
90
|
+
// 2. Posting TO a debtor/creditor account (receivable/payable settlement - VAT was already recorded)
|
|
91
|
+
const isVATAccount = accountNumber === '1571' || accountNumber === '1771' || accountNumber === '1576';
|
|
92
|
+
const accountNum = parseInt(accountNumber);
|
|
93
|
+
const isDebtorCreditorAccount = (accountNum >= 10000 && accountNum <= 69999) || (accountNum >= 70000 && accountNum <= 99999);
|
|
94
|
+
|
|
95
|
+
if (rule.requiresVAT && !vatAmount && !isVATAccount && !isDebtorCreditorAccount) {
|
|
96
|
+
errors.push(
|
|
97
|
+
`Posting key ${postingKey} requires VAT amount, but none provided. ` +
|
|
98
|
+
`Description: ${rule.description}`
|
|
99
|
+
);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// Validate VAT rate if specified
|
|
103
|
+
if (rule.vatRate && vatAmount && rule.vatRate > 0) {
|
|
104
|
+
const expectedVAT = Math.round(amount * rule.vatRate) / 100;
|
|
105
|
+
const tolerance = 0.02; // 2 cent tolerance for rounding
|
|
106
|
+
|
|
107
|
+
if (Math.abs(vatAmount - expectedVAT) > tolerance) {
|
|
108
|
+
warnings.push(
|
|
109
|
+
`VAT amount ${vatAmount} does not match expected ${expectedVAT.toFixed(2)} ` +
|
|
110
|
+
`for posting key ${postingKey} (${rule.vatRate}%)`
|
|
111
|
+
);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// Validate tax scenario
|
|
116
|
+
if (rule.allowedScenarios && taxScenario) {
|
|
117
|
+
if (!rule.allowedScenarios.includes(taxScenario)) {
|
|
118
|
+
errors.push(
|
|
119
|
+
`Posting key ${postingKey} is not valid for tax scenario '${taxScenario}'. ` +
|
|
120
|
+
`Allowed scenarios: ${rule.allowedScenarios.join(', ')}`
|
|
121
|
+
);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// Validate automatism disabling
|
|
126
|
+
if (rule.disablesVATAutomatism && vatAmount && vatAmount > 0) {
|
|
127
|
+
warnings.push(
|
|
128
|
+
`Posting key ${postingKey} disables VAT automatism but VAT amount is provided. ` +
|
|
129
|
+
`This may cause incorrect tax reporting.`
|
|
130
|
+
);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
return {
|
|
134
|
+
isValid: errors.length === 0,
|
|
135
|
+
errors,
|
|
136
|
+
warnings
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Get posting key description
|
|
142
|
+
*/
|
|
143
|
+
export function getPostingKeyDescription(postingKey: TPostingKey): string {
|
|
144
|
+
const rule = POSTING_KEY_RULES[postingKey];
|
|
145
|
+
return rule ? rule.description : `Unknown posting key: ${postingKey}`;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* Get appropriate posting key for a transaction
|
|
150
|
+
*/
|
|
151
|
+
export function suggestPostingKey(params: {
|
|
152
|
+
vatRate: number;
|
|
153
|
+
taxScenario?: string;
|
|
154
|
+
isPayment?: boolean;
|
|
155
|
+
}): TPostingKey {
|
|
156
|
+
const { vatRate, taxScenario, isPayment } = params;
|
|
157
|
+
|
|
158
|
+
// Tax-free or reverse charge scenarios
|
|
159
|
+
if (taxScenario === 'tax_free' || taxScenario === 'export') {
|
|
160
|
+
return 40;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
// Reverse charge
|
|
164
|
+
if (taxScenario === 'reverse_charge' || taxScenario === 'third_country') {
|
|
165
|
+
return 94;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// Intra-EU with VAT
|
|
169
|
+
if (taxScenario === 'intra_eu' && vatRate === 19) {
|
|
170
|
+
return 19;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
// Payment with 19% VAT
|
|
174
|
+
if (isPayment && vatRate === 19) {
|
|
175
|
+
return 3;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
// Input VAT based on rate
|
|
179
|
+
if (vatRate === 19) {
|
|
180
|
+
return 9;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
if (vatRate === 7) {
|
|
184
|
+
return 8;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
// Default to tax-free if no VAT
|
|
188
|
+
if (vatRate === 0) {
|
|
189
|
+
return 40;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
// Fallback to 19% input VAT
|
|
193
|
+
return 9;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/**
|
|
197
|
+
* Validate all posting keys for consistency
|
|
198
|
+
*/
|
|
199
|
+
export function validatePostingKeyConsistency(lines: Array<{
|
|
200
|
+
postingKey: TPostingKey;
|
|
201
|
+
accountNumber: string;
|
|
202
|
+
debit?: number;
|
|
203
|
+
credit?: number;
|
|
204
|
+
vatAmount?: number;
|
|
205
|
+
}>): { isValid: boolean; errors: string[]; warnings: string[] } {
|
|
206
|
+
const errors: string[] = [];
|
|
207
|
+
const warnings: string[] = [];
|
|
208
|
+
|
|
209
|
+
// Check for mixing tax-free and taxed transactions
|
|
210
|
+
const hasTaxFree = lines.some(line => line.postingKey === 40);
|
|
211
|
+
const hasTaxed = lines.some(line => [3, 8, 9, 19, 94].includes(line.postingKey));
|
|
212
|
+
|
|
213
|
+
if (hasTaxFree && hasTaxed) {
|
|
214
|
+
warnings.push(
|
|
215
|
+
'Journal entry mixes tax-free (key 40) and taxed transactions. ' +
|
|
216
|
+
'Verify this is intentional.'
|
|
217
|
+
);
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
// Check for reverse charge consistency
|
|
221
|
+
const hasReverseCharge = lines.some(line => line.postingKey === 94);
|
|
222
|
+
if (hasReverseCharge) {
|
|
223
|
+
const reverseChargeLines = lines.filter(line => line.postingKey === 94);
|
|
224
|
+
if (reverseChargeLines.length % 2 !== 0) {
|
|
225
|
+
errors.push(
|
|
226
|
+
'Reverse charge (posting key 94) requires both input and output VAT entries. ' +
|
|
227
|
+
'Found odd number of reverse charge lines.'
|
|
228
|
+
);
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
return {
|
|
233
|
+
isValid: errors.length === 0,
|
|
234
|
+
errors,
|
|
235
|
+
warnings
|
|
236
|
+
};
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
/**
|
|
240
|
+
* Check if posting key requires automatic VAT booking
|
|
241
|
+
*/
|
|
242
|
+
export function requiresAutomaticVAT(postingKey: TPostingKey): boolean {
|
|
243
|
+
const rule = POSTING_KEY_RULES[postingKey];
|
|
244
|
+
return rule ? !rule.disablesVATAutomatism : false;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
/**
|
|
248
|
+
* Get all valid posting keys
|
|
249
|
+
*/
|
|
250
|
+
export function getAllPostingKeys(): TPostingKey[] {
|
|
251
|
+
return Object.keys(POSTING_KEY_RULES).map(k => Number(k) as TPostingKey);
|
|
252
|
+
}
|
package/ts/skr.security.ts
CHANGED
|
@@ -2,6 +2,7 @@ import * as plugins from './plugins.js';
|
|
|
2
2
|
import * as path from 'path';
|
|
3
3
|
import * as crypto from 'crypto';
|
|
4
4
|
import * as https from 'https';
|
|
5
|
+
import * as nodeForge from 'node-forge';
|
|
5
6
|
|
|
6
7
|
export interface ISigningOptions {
|
|
7
8
|
certificatePem?: string;
|
|
@@ -57,19 +58,19 @@ export class SecurityManager {
|
|
|
57
58
|
|
|
58
59
|
try {
|
|
59
60
|
// Parse certificate and key
|
|
60
|
-
const certificate =
|
|
61
|
+
const certificate = nodeForge.pki.certificateFromPem(cert);
|
|
61
62
|
const privateKey = this.options.privateKeyPassphrase
|
|
62
|
-
?
|
|
63
|
-
:
|
|
63
|
+
? nodeForge.pki.decryptRsaPrivateKey(key, this.options.privateKeyPassphrase)
|
|
64
|
+
: nodeForge.pki.privateKeyFromPem(key);
|
|
64
65
|
|
|
65
66
|
// Create PKCS#7 signed data (CMS)
|
|
66
|
-
const p7 =
|
|
67
|
+
const p7 = nodeForge.pkcs7.createSignedData();
|
|
67
68
|
|
|
68
69
|
// Add content
|
|
69
70
|
if (typeof data === 'string') {
|
|
70
|
-
p7.content =
|
|
71
|
+
p7.content = nodeForge.util.createBuffer(data, 'utf8');
|
|
71
72
|
} else {
|
|
72
|
-
p7.content =
|
|
73
|
+
p7.content = nodeForge.util.createBuffer(data.toString('latin1'));
|
|
73
74
|
}
|
|
74
75
|
|
|
75
76
|
// Add certificate
|
|
@@ -79,17 +80,17 @@ export class SecurityManager {
|
|
|
79
80
|
p7.addSigner({
|
|
80
81
|
key: privateKey,
|
|
81
82
|
certificate: certificate,
|
|
82
|
-
digestAlgorithm:
|
|
83
|
+
digestAlgorithm: nodeForge.pki.oids.sha256,
|
|
83
84
|
authenticatedAttributes: [
|
|
84
85
|
{
|
|
85
|
-
type:
|
|
86
|
-
value:
|
|
86
|
+
type: nodeForge.pki.oids.contentType,
|
|
87
|
+
value: nodeForge.pki.oids.data
|
|
87
88
|
},
|
|
88
89
|
{
|
|
89
|
-
type:
|
|
90
|
+
type: nodeForge.pki.oids.messageDigest
|
|
90
91
|
},
|
|
91
92
|
{
|
|
92
|
-
type:
|
|
93
|
+
type: nodeForge.pki.oids.signingTime,
|
|
93
94
|
value: new Date().toISOString()
|
|
94
95
|
}
|
|
95
96
|
]
|
|
@@ -99,7 +100,7 @@ export class SecurityManager {
|
|
|
99
100
|
p7.sign({ detached: true });
|
|
100
101
|
|
|
101
102
|
// Convert to PEM
|
|
102
|
-
const pem =
|
|
103
|
+
const pem = nodeForge.pkcs7.messageToPem(p7);
|
|
103
104
|
|
|
104
105
|
// Extract base64 signature
|
|
105
106
|
const signature = pem
|
|
@@ -237,14 +238,14 @@ export class SecurityManager {
|
|
|
237
238
|
}
|
|
238
239
|
|
|
239
240
|
// Parse the PKCS#7 message
|
|
240
|
-
const p7 =
|
|
241
|
+
const p7 = nodeForge.pkcs7.messageFromPem(pemSignature);
|
|
241
242
|
|
|
242
243
|
// Prepare content for verification
|
|
243
|
-
let content:
|
|
244
|
+
let content: nodeForge.util.ByteStringBuffer;
|
|
244
245
|
if (typeof data === 'string') {
|
|
245
|
-
content =
|
|
246
|
+
content = nodeForge.util.createBuffer(data, 'utf8');
|
|
246
247
|
} else {
|
|
247
|
-
content =
|
|
248
|
+
content = nodeForge.util.createBuffer(data.toString('latin1'));
|
|
248
249
|
}
|
|
249
250
|
|
|
250
251
|
// Verify the signature
|
|
@@ -267,8 +268,8 @@ export class SecurityManager {
|
|
|
267
268
|
commonName: string = 'SKR Export System',
|
|
268
269
|
validDays: number = 365
|
|
269
270
|
): Promise<{ certificate: string; privateKey: string }> {
|
|
270
|
-
const keys =
|
|
271
|
-
const cert =
|
|
271
|
+
const keys = nodeForge.pki.rsa.generateKeyPair(2048);
|
|
272
|
+
const cert = nodeForge.pki.createCertificate();
|
|
272
273
|
|
|
273
274
|
cert.publicKey = keys.publicKey;
|
|
274
275
|
cert.serialNumber = '01';
|
|
@@ -326,11 +327,11 @@ export class SecurityManager {
|
|
|
326
327
|
]);
|
|
327
328
|
|
|
328
329
|
// Self-sign certificate
|
|
329
|
-
cert.sign(keys.privateKey,
|
|
330
|
+
cert.sign(keys.privateKey, nodeForge.md.sha256.create());
|
|
330
331
|
|
|
331
332
|
// Convert to PEM
|
|
332
|
-
const certificatePem =
|
|
333
|
-
const privateKeyPem =
|
|
333
|
+
const certificatePem = nodeForge.pki.certificateToPem(cert);
|
|
334
|
+
const privateKeyPem = nodeForge.pki.privateKeyToPem(keys.privateKey);
|
|
334
335
|
|
|
335
336
|
return {
|
|
336
337
|
certificate: certificatePem,
|
|
@@ -402,4 +403,4 @@ export class SecurityManager {
|
|
|
402
403
|
|
|
403
404
|
return ltv;
|
|
404
405
|
}
|
|
405
|
-
}
|
|
406
|
+
}
|
package/ts/skr.types.ts
CHANGED
|
@@ -9,6 +9,18 @@ export type TSKRType = 'SKR03' | 'SKR04';
|
|
|
9
9
|
|
|
10
10
|
export type TTransactionStatus = 'pending' | 'posted' | 'reversed';
|
|
11
11
|
|
|
12
|
+
/**
|
|
13
|
+
* DATEV posting keys (Buchungsschlüssel) for German accounting
|
|
14
|
+
* These keys control automatic VAT booking and are checked in tax audits
|
|
15
|
+
*/
|
|
16
|
+
export type TPostingKey =
|
|
17
|
+
| 3 // Payment with 19% VAT
|
|
18
|
+
| 8 // 7% input VAT
|
|
19
|
+
| 9 // 19% input VAT
|
|
20
|
+
| 19 // 19% input VAT (intra-EU)
|
|
21
|
+
| 40 // Tax-free (disables VAT automatism)
|
|
22
|
+
| 94; // 19% input/output VAT (reverse charge)
|
|
23
|
+
|
|
12
24
|
export type TReportType =
|
|
13
25
|
| 'trial_balance'
|
|
14
26
|
| 'income_statement'
|
|
@@ -16,6 +28,18 @@ export type TReportType =
|
|
|
16
28
|
| 'general_ledger'
|
|
17
29
|
| 'cash_flow';
|
|
18
30
|
|
|
31
|
+
/**
|
|
32
|
+
* Posting key validation rule
|
|
33
|
+
*/
|
|
34
|
+
export interface IPostingKeyRule {
|
|
35
|
+
key: TPostingKey;
|
|
36
|
+
description: string;
|
|
37
|
+
vatRate?: number; // Expected VAT rate (if applicable)
|
|
38
|
+
requiresVAT: boolean; // Whether VAT entry is required
|
|
39
|
+
disablesVATAutomatism: boolean; // Whether this key disables automatic VAT
|
|
40
|
+
allowedScenarios?: string[]; // Allowed tax scenarios (e.g., 'reverse_charge')
|
|
41
|
+
}
|
|
42
|
+
|
|
19
43
|
export interface IAccountData {
|
|
20
44
|
accountNumber: string;
|
|
21
45
|
accountName: string;
|
|
@@ -25,6 +49,7 @@ export interface IAccountData {
|
|
|
25
49
|
description?: string;
|
|
26
50
|
vatRate?: number;
|
|
27
51
|
isActive?: boolean;
|
|
52
|
+
isAutomaticAccount?: boolean; // Automatikkonto (e.g., 1400, 1600) - cannot be posted to directly
|
|
28
53
|
}
|
|
29
54
|
|
|
30
55
|
export interface ITransactionData {
|
|
@@ -53,6 +78,7 @@ export interface IJournalEntryLine {
|
|
|
53
78
|
credit?: number;
|
|
54
79
|
description?: string;
|
|
55
80
|
costCenter?: string;
|
|
81
|
+
postingKey: TPostingKey; // REQUIRED: DATEV posting key for VAT automation control
|
|
56
82
|
}
|
|
57
83
|
|
|
58
84
|
export interface ITrialBalanceEntry {
|
package/ts/skr03.data.ts
CHANGED
|
@@ -159,6 +159,7 @@ export const SKR03_ACCOUNTS: IAccountData[] = [
|
|
|
159
159
|
accountType: 'asset',
|
|
160
160
|
skrType: 'SKR03',
|
|
161
161
|
description: 'Trade receivables',
|
|
162
|
+
isAutomaticAccount: true, // Automatikkonto - cannot be posted to directly, use debtor accounts (10000-69999)
|
|
162
163
|
},
|
|
163
164
|
{
|
|
164
165
|
accountNumber: '1500',
|
|
@@ -199,6 +200,7 @@ export const SKR03_ACCOUNTS: IAccountData[] = [
|
|
|
199
200
|
accountType: 'liability',
|
|
200
201
|
skrType: 'SKR03',
|
|
201
202
|
description: 'Trade payables',
|
|
203
|
+
isAutomaticAccount: true, // Automatikkonto - cannot be posted to directly, use creditor accounts (70000-99999)
|
|
202
204
|
},
|
|
203
205
|
{
|
|
204
206
|
accountNumber: '1700',
|