feat: Enhance journal entry and transaction handling with posting keys

- Added posting key support to SKR03 and SKR04 journal entries and transactions to ensure DATEV compliance.
- Implemented validation for posting keys in journal entries, ensuring all lines have a posting key and that they are consistent across the entry.
- Introduced automatic account checks to prevent posting to accounts that cannot be directly posted to (e.g., 1400, 1600).
- Updated account validation to include checks for debtor and creditor ranges.
- Enhanced invoice booking logic to include appropriate posting keys based on VAT rates and scenarios.
- Created a new module for posting key definitions and validation rules, including functions for validating posting keys and suggesting appropriate keys based on transaction parameters.
- Updated tests to cover new posting key functionality and ensure compliance with accounting rules.
This commit is contained in:
2025-10-27 08:34:28 +00:00
parent 73b46f7857
commit 4f1066da2e
13 changed files with 758 additions and 229 deletions

View File

@@ -1,6 +1,7 @@
import * as plugins from './plugins.js';
import { JournalEntry } from './skr.classes.journalentry.js';
import { SKRInvoiceMapper } from './skr.invoice.mapper.js';
import { suggestPostingKey } from './skr.postingkeys.js';
import type { TSKRType, IJournalEntry, IJournalEntryLine } from './skr.types.js';
import type {
IInvoice,
@@ -196,14 +197,16 @@ export class InvoiceBookingEngine {
lines.push({
accountNumber,
credit: Math.abs(amount),
description: this.getAccountDescription(accountNumber, group)
description: this.getAccountDescription(accountNumber, group),
postingKey: 9 // 19% input VAT for expenses
});
} else {
// Regular invoice: debit expense account
lines.push({
accountNumber,
debit: Math.abs(amount),
description: this.getAccountDescription(accountNumber, group)
description: this.getAccountDescription(accountNumber, group),
postingKey: 9 // 19% input VAT for expenses
});
}
}
@@ -221,14 +224,16 @@ export class InvoiceBookingEngine {
lines.push({
accountNumber: controlAccount,
debit: totalAmount,
description: `${invoice.supplier.name} - Credit Note ${invoice.invoiceNumber}`
description: `${invoice.supplier.name} - Credit Note ${invoice.invoiceNumber}`,
postingKey: 40 // Tax-free for control account
});
} else {
// Regular invoice: credit vendor account
lines.push({
accountNumber: controlAccount,
credit: totalAmount,
description: `${invoice.supplier.name} - Invoice ${invoice.invoiceNumber}`
description: `${invoice.supplier.name} - Invoice ${invoice.invoiceNumber}`,
postingKey: 40 // Tax-free for control account
});
}
@@ -257,14 +262,16 @@ export class InvoiceBookingEngine {
lines.push({
accountNumber,
debit: Math.abs(amount),
description: this.getAccountDescription(accountNumber, group)
description: this.getAccountDescription(accountNumber, group),
postingKey: 9 // 19% output VAT for revenue
});
} else {
// Regular invoice: credit revenue account
lines.push({
accountNumber,
credit: Math.abs(amount),
description: this.getAccountDescription(accountNumber, group)
description: this.getAccountDescription(accountNumber, group),
postingKey: 9 // 19% output VAT for revenue
});
}
}
@@ -282,14 +289,16 @@ export class InvoiceBookingEngine {
lines.push({
accountNumber: controlAccount,
credit: totalAmount,
description: `${invoice.customer.name} - Credit Note ${invoice.invoiceNumber}`
description: `${invoice.customer.name} - Credit Note ${invoice.invoiceNumber}`,
postingKey: 40 // Tax-free for control account
});
} else {
// Regular invoice: debit customer account
lines.push({
accountNumber: controlAccount,
debit: totalAmount,
description: `${invoice.customer.name} - Invoice ${invoice.invoiceNumber}`
description: `${invoice.customer.name} - Invoice ${invoice.invoiceNumber}`,
postingKey: 40 // Tax-free for control account
});
}
@@ -325,20 +334,23 @@ export class InvoiceBookingEngine {
const amount = Math.abs(vatBreak.taxAmount);
const description = `VAT ${vatBreak.vatCategory.rate}%`;
const vatRate = vatBreak.vatCategory.rate;
// Select posting key based on VAT rate: 8 for 7%, 9 for 19%
const postingKey = vatRate === 7 ? 8 : 9;
if (direction === 'input') {
// Input VAT (Vorsteuer)
if (reverseDirection) {
lines.push({ accountNumber: vatAccount, credit: amount, description });
lines.push({ accountNumber: vatAccount, credit: amount, description, postingKey });
} else {
lines.push({ accountNumber: vatAccount, debit: amount, description });
lines.push({ accountNumber: vatAccount, debit: amount, description, postingKey });
}
} else {
// Output VAT (Umsatzsteuer)
if (reverseDirection) {
lines.push({ accountNumber: vatAccount, debit: amount, description });
lines.push({ accountNumber: vatAccount, debit: amount, description, postingKey });
} else {
lines.push({ accountNumber: vatAccount, credit: amount, description });
lines.push({ accountNumber: vatAccount, credit: amount, description, postingKey });
}
}
}
@@ -404,12 +416,14 @@ export class InvoiceBookingEngine {
{
accountNumber: inputVATAccount,
debit: amount,
description: `Reverse charge input VAT ${vatBreak.vatCategory.rate}%`
description: `Reverse charge input VAT ${vatBreak.vatCategory.rate}%`,
postingKey: 94 // Reverse charge posting key
},
{
accountNumber: outputVATAccount,
credit: amount,
description: `Reverse charge output VAT ${vatBreak.vatCategory.rate}%`
description: `Reverse charge output VAT ${vatBreak.vatCategory.rate}%`,
postingKey: 94 // Reverse charge posting key
}
);
}
@@ -462,24 +476,27 @@ export class InvoiceBookingEngine {
{
accountNumber: controlAccount,
debit: fullAmount,
description: `Payment to ${invoice.supplier.name}`
description: `Payment to ${invoice.supplier.name}`,
postingKey: 3 // Payment with VAT
},
{
accountNumber: '1000', // Bank account (would be configurable)
credit: paymentAmount,
description: `Bank payment ${payment.endToEndId || payment.paymentId}`
description: `Bank payment ${payment.endToEndId || payment.paymentId}`,
postingKey: 40 // Tax-free for bank account
}
);
// Book skonto if taken
if (skontoAmount > 0) {
const skontoAccounts = this.mapper.getSkontoAccounts(invoice);
lines.push({
accountNumber: skontoAccounts.skontoAccount,
credit: skontoAmount,
description: `Skonto received`
description: `Skonto received`,
postingKey: 40 // Tax-free for skonto
});
// VAT correction for skonto
if (rules.skontoMethod === 'gross') {
const effectiveRate = this.calculateEffectiveVATRate(invoice);
@@ -488,7 +505,8 @@ export class InvoiceBookingEngine {
{
accountNumber: skontoAccounts.vatCorrectionAccount,
credit: vatCorrection,
description: `Skonto VAT correction`
description: `Skonto VAT correction`,
postingKey: 40 // Tax-free for correction
}
);
}
@@ -499,24 +517,27 @@ export class InvoiceBookingEngine {
{
accountNumber: '1000', // Bank account
debit: paymentAmount,
description: `Payment from ${invoice.customer.name}`
description: `Payment from ${invoice.customer.name}`,
postingKey: 40 // Tax-free for bank account
},
{
accountNumber: controlAccount,
credit: fullAmount,
description: `Customer payment ${payment.endToEndId || payment.paymentId}`
description: `Customer payment ${payment.endToEndId || payment.paymentId}`,
postingKey: 3 // Payment with VAT
}
);
// Book skonto if granted
if (skontoAmount > 0) {
const skontoAccounts = this.mapper.getSkontoAccounts(invoice);
lines.push({
accountNumber: skontoAccounts.skontoAccount,
debit: skontoAmount,
description: `Skonto granted`
description: `Skonto granted`,
postingKey: 40 // Tax-free for skonto
});
// VAT correction for skonto
if (rules.skontoMethod === 'gross') {
const effectiveRate = this.calculateEffectiveVATRate(invoice);
@@ -525,7 +546,8 @@ export class InvoiceBookingEngine {
{
accountNumber: skontoAccounts.vatCorrectionAccount,
debit: vatCorrection,
description: `Skonto VAT correction`
description: `Skonto VAT correction`,
postingKey: 40 // Tax-free for correction
}
);
}