@fin.cx/skr 1.2.0 → 1.2.1
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/00_commitinfo_data.d.ts +8 -0
- package/dist_ts/00_commitinfo_data.js +9 -0
- package/dist_ts/skr.classes.account.d.ts +28 -0
- package/dist_ts/skr.classes.account.js +87 -4
- package/dist_ts/skr.classes.journalentry.js +56 -4
- package/dist_ts/skr.classes.ledger.js +5 -1
- package/dist_ts/skr.invoice.booking.js +45 -23
- package/dist_ts/skr.postingkeys.d.ts +56 -0
- package/dist_ts/skr.postingkeys.js +196 -0
- 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/package.json +1 -1
- package/readme.hints.md +54 -1
- package/ts/00_commitinfo_data.ts +8 -0
- package/ts/skr.classes.account.ts +106 -3
- package/ts/skr.classes.journalentry.ts +78 -3
- package/ts/skr.classes.ledger.ts +4 -0
- package/ts/skr.invoice.booking.ts +49 -27
- package/ts/skr.postingkeys.ts +252 -0
- 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,
|
|
@@ -196,14 +197,16 @@ export class InvoiceBookingEngine {
|
|
|
196
197
|
lines.push({
|
|
197
198
|
accountNumber,
|
|
198
199
|
credit: Math.abs(amount),
|
|
199
|
-
description: this.getAccountDescription(accountNumber, group)
|
|
200
|
+
description: this.getAccountDescription(accountNumber, group),
|
|
201
|
+
postingKey: 9 // 19% input VAT for expenses
|
|
200
202
|
});
|
|
201
203
|
} else {
|
|
202
204
|
// Regular invoice: debit expense account
|
|
203
205
|
lines.push({
|
|
204
206
|
accountNumber,
|
|
205
207
|
debit: Math.abs(amount),
|
|
206
|
-
description: this.getAccountDescription(accountNumber, group)
|
|
208
|
+
description: this.getAccountDescription(accountNumber, group),
|
|
209
|
+
postingKey: 9 // 19% input VAT for expenses
|
|
207
210
|
});
|
|
208
211
|
}
|
|
209
212
|
}
|
|
@@ -221,14 +224,16 @@ export class InvoiceBookingEngine {
|
|
|
221
224
|
lines.push({
|
|
222
225
|
accountNumber: controlAccount,
|
|
223
226
|
debit: totalAmount,
|
|
224
|
-
description: `${invoice.supplier.name} - Credit Note ${invoice.invoiceNumber}
|
|
227
|
+
description: `${invoice.supplier.name} - Credit Note ${invoice.invoiceNumber}`,
|
|
228
|
+
postingKey: 40 // Tax-free for control account
|
|
225
229
|
});
|
|
226
230
|
} else {
|
|
227
231
|
// Regular invoice: credit vendor account
|
|
228
232
|
lines.push({
|
|
229
233
|
accountNumber: controlAccount,
|
|
230
234
|
credit: totalAmount,
|
|
231
|
-
description: `${invoice.supplier.name} - Invoice ${invoice.invoiceNumber}
|
|
235
|
+
description: `${invoice.supplier.name} - Invoice ${invoice.invoiceNumber}`,
|
|
236
|
+
postingKey: 40 // Tax-free for control account
|
|
232
237
|
});
|
|
233
238
|
}
|
|
234
239
|
|
|
@@ -257,14 +262,16 @@ export class InvoiceBookingEngine {
|
|
|
257
262
|
lines.push({
|
|
258
263
|
accountNumber,
|
|
259
264
|
debit: Math.abs(amount),
|
|
260
|
-
description: this.getAccountDescription(accountNumber, group)
|
|
265
|
+
description: this.getAccountDescription(accountNumber, group),
|
|
266
|
+
postingKey: 9 // 19% output VAT for revenue
|
|
261
267
|
});
|
|
262
268
|
} else {
|
|
263
269
|
// Regular invoice: credit revenue account
|
|
264
270
|
lines.push({
|
|
265
271
|
accountNumber,
|
|
266
272
|
credit: Math.abs(amount),
|
|
267
|
-
description: this.getAccountDescription(accountNumber, group)
|
|
273
|
+
description: this.getAccountDescription(accountNumber, group),
|
|
274
|
+
postingKey: 9 // 19% output VAT for revenue
|
|
268
275
|
});
|
|
269
276
|
}
|
|
270
277
|
}
|
|
@@ -282,14 +289,16 @@ export class InvoiceBookingEngine {
|
|
|
282
289
|
lines.push({
|
|
283
290
|
accountNumber: controlAccount,
|
|
284
291
|
credit: totalAmount,
|
|
285
|
-
description: `${invoice.customer.name} - Credit Note ${invoice.invoiceNumber}
|
|
292
|
+
description: `${invoice.customer.name} - Credit Note ${invoice.invoiceNumber}`,
|
|
293
|
+
postingKey: 40 // Tax-free for control account
|
|
286
294
|
});
|
|
287
295
|
} else {
|
|
288
296
|
// Regular invoice: debit customer account
|
|
289
297
|
lines.push({
|
|
290
298
|
accountNumber: controlAccount,
|
|
291
299
|
debit: totalAmount,
|
|
292
|
-
description: `${invoice.customer.name} - Invoice ${invoice.invoiceNumber}
|
|
300
|
+
description: `${invoice.customer.name} - Invoice ${invoice.invoiceNumber}`,
|
|
301
|
+
postingKey: 40 // Tax-free for control account
|
|
293
302
|
});
|
|
294
303
|
}
|
|
295
304
|
|
|
@@ -325,20 +334,23 @@ export class InvoiceBookingEngine {
|
|
|
325
334
|
|
|
326
335
|
const amount = Math.abs(vatBreak.taxAmount);
|
|
327
336
|
const description = `VAT ${vatBreak.vatCategory.rate}%`;
|
|
328
|
-
|
|
337
|
+
const vatRate = vatBreak.vatCategory.rate;
|
|
338
|
+
// Select posting key based on VAT rate: 8 for 7%, 9 for 19%
|
|
339
|
+
const postingKey = vatRate === 7 ? 8 : 9;
|
|
340
|
+
|
|
329
341
|
if (direction === 'input') {
|
|
330
342
|
// Input VAT (Vorsteuer)
|
|
331
343
|
if (reverseDirection) {
|
|
332
|
-
lines.push({ accountNumber: vatAccount, credit: amount, description });
|
|
344
|
+
lines.push({ accountNumber: vatAccount, credit: amount, description, postingKey });
|
|
333
345
|
} else {
|
|
334
|
-
lines.push({ accountNumber: vatAccount, debit: amount, description });
|
|
346
|
+
lines.push({ accountNumber: vatAccount, debit: amount, description, postingKey });
|
|
335
347
|
}
|
|
336
348
|
} else {
|
|
337
349
|
// Output VAT (Umsatzsteuer)
|
|
338
350
|
if (reverseDirection) {
|
|
339
|
-
lines.push({ accountNumber: vatAccount, debit: amount, description });
|
|
351
|
+
lines.push({ accountNumber: vatAccount, debit: amount, description, postingKey });
|
|
340
352
|
} else {
|
|
341
|
-
lines.push({ accountNumber: vatAccount, credit: amount, description });
|
|
353
|
+
lines.push({ accountNumber: vatAccount, credit: amount, description, postingKey });
|
|
342
354
|
}
|
|
343
355
|
}
|
|
344
356
|
}
|
|
@@ -404,12 +416,14 @@ export class InvoiceBookingEngine {
|
|
|
404
416
|
{
|
|
405
417
|
accountNumber: inputVATAccount,
|
|
406
418
|
debit: amount,
|
|
407
|
-
description: `Reverse charge input VAT ${vatBreak.vatCategory.rate}
|
|
419
|
+
description: `Reverse charge input VAT ${vatBreak.vatCategory.rate}%`,
|
|
420
|
+
postingKey: 94 // Reverse charge posting key
|
|
408
421
|
},
|
|
409
422
|
{
|
|
410
423
|
accountNumber: outputVATAccount,
|
|
411
424
|
credit: amount,
|
|
412
|
-
description: `Reverse charge output VAT ${vatBreak.vatCategory.rate}
|
|
425
|
+
description: `Reverse charge output VAT ${vatBreak.vatCategory.rate}%`,
|
|
426
|
+
postingKey: 94 // Reverse charge posting key
|
|
413
427
|
}
|
|
414
428
|
);
|
|
415
429
|
}
|
|
@@ -462,24 +476,27 @@ export class InvoiceBookingEngine {
|
|
|
462
476
|
{
|
|
463
477
|
accountNumber: controlAccount,
|
|
464
478
|
debit: fullAmount,
|
|
465
|
-
description: `Payment to ${invoice.supplier.name}
|
|
479
|
+
description: `Payment to ${invoice.supplier.name}`,
|
|
480
|
+
postingKey: 3 // Payment with VAT
|
|
466
481
|
},
|
|
467
482
|
{
|
|
468
483
|
accountNumber: '1000', // Bank account (would be configurable)
|
|
469
484
|
credit: paymentAmount,
|
|
470
|
-
description: `Bank payment ${payment.endToEndId || payment.paymentId}
|
|
485
|
+
description: `Bank payment ${payment.endToEndId || payment.paymentId}`,
|
|
486
|
+
postingKey: 40 // Tax-free for bank account
|
|
471
487
|
}
|
|
472
488
|
);
|
|
473
|
-
|
|
489
|
+
|
|
474
490
|
// Book skonto if taken
|
|
475
491
|
if (skontoAmount > 0) {
|
|
476
492
|
const skontoAccounts = this.mapper.getSkontoAccounts(invoice);
|
|
477
493
|
lines.push({
|
|
478
494
|
accountNumber: skontoAccounts.skontoAccount,
|
|
479
495
|
credit: skontoAmount,
|
|
480
|
-
description: `Skonto received
|
|
496
|
+
description: `Skonto received`,
|
|
497
|
+
postingKey: 40 // Tax-free for skonto
|
|
481
498
|
});
|
|
482
|
-
|
|
499
|
+
|
|
483
500
|
// VAT correction for skonto
|
|
484
501
|
if (rules.skontoMethod === 'gross') {
|
|
485
502
|
const effectiveRate = this.calculateEffectiveVATRate(invoice);
|
|
@@ -488,7 +505,8 @@ export class InvoiceBookingEngine {
|
|
|
488
505
|
{
|
|
489
506
|
accountNumber: skontoAccounts.vatCorrectionAccount,
|
|
490
507
|
credit: vatCorrection,
|
|
491
|
-
description: `Skonto VAT correction
|
|
508
|
+
description: `Skonto VAT correction`,
|
|
509
|
+
postingKey: 40 // Tax-free for correction
|
|
492
510
|
}
|
|
493
511
|
);
|
|
494
512
|
}
|
|
@@ -499,24 +517,27 @@ export class InvoiceBookingEngine {
|
|
|
499
517
|
{
|
|
500
518
|
accountNumber: '1000', // Bank account
|
|
501
519
|
debit: paymentAmount,
|
|
502
|
-
description: `Payment from ${invoice.customer.name}
|
|
520
|
+
description: `Payment from ${invoice.customer.name}`,
|
|
521
|
+
postingKey: 40 // Tax-free for bank account
|
|
503
522
|
},
|
|
504
523
|
{
|
|
505
524
|
accountNumber: controlAccount,
|
|
506
525
|
credit: fullAmount,
|
|
507
|
-
description: `Customer payment ${payment.endToEndId || payment.paymentId}
|
|
526
|
+
description: `Customer payment ${payment.endToEndId || payment.paymentId}`,
|
|
527
|
+
postingKey: 3 // Payment with VAT
|
|
508
528
|
}
|
|
509
529
|
);
|
|
510
|
-
|
|
530
|
+
|
|
511
531
|
// Book skonto if granted
|
|
512
532
|
if (skontoAmount > 0) {
|
|
513
533
|
const skontoAccounts = this.mapper.getSkontoAccounts(invoice);
|
|
514
534
|
lines.push({
|
|
515
535
|
accountNumber: skontoAccounts.skontoAccount,
|
|
516
536
|
debit: skontoAmount,
|
|
517
|
-
description: `Skonto granted
|
|
537
|
+
description: `Skonto granted`,
|
|
538
|
+
postingKey: 40 // Tax-free for skonto
|
|
518
539
|
});
|
|
519
|
-
|
|
540
|
+
|
|
520
541
|
// VAT correction for skonto
|
|
521
542
|
if (rules.skontoMethod === 'gross') {
|
|
522
543
|
const effectiveRate = this.calculateEffectiveVATRate(invoice);
|
|
@@ -525,7 +546,8 @@ export class InvoiceBookingEngine {
|
|
|
525
546
|
{
|
|
526
547
|
accountNumber: skontoAccounts.vatCorrectionAccount,
|
|
527
548
|
debit: vatCorrection,
|
|
528
|
-
description: `Skonto VAT correction
|
|
549
|
+
description: `Skonto VAT correction`,
|
|
550
|
+
postingKey: 40 // Tax-free for correction
|
|
529
551
|
}
|
|
530
552
|
);
|
|
531
553
|
}
|
|
@@ -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.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',
|
package/ts/skr04.data.ts
CHANGED
|
@@ -159,6 +159,7 @@ export const SKR04_ACCOUNTS: IAccountData[] = [
|
|
|
159
159
|
accountType: 'asset',
|
|
160
160
|
skrType: 'SKR04',
|
|
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 SKR04_ACCOUNTS: IAccountData[] = [
|
|
|
199
200
|
accountType: 'liability',
|
|
200
201
|
skrType: 'SKR04',
|
|
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',
|