@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.
@@ -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',