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

@@ -2,6 +2,11 @@ import * as plugins from './plugins.js';
import { getDbSync } from './skr.database.js';
import { Account } from './skr.classes.account.js';
import { Transaction } from './skr.classes.transaction.js';
import {
validatePostingKey,
validatePostingKeyConsistency,
getPostingKeyDescription,
} from './skr.postingkeys.js';
import type {
TSKRType,
IJournalEntry,
@@ -212,22 +217,84 @@ export class JournalEntry extends SmartDataDbDoc<JournalEntry, JournalEntry> {
throw new Error('Journal entry must have at least 2 lines');
}
// Validate all accounts exist and are active
// Validate all accounts exist, are active, and can be posted to
const validationErrors: string[] = [];
const validationWarnings: string[] = [];
for (const line of this.lines) {
// Validate posting key is present (REQUIRED)
if (!line.postingKey) {
validationErrors.push(
`Line for account ${line.accountNumber} is missing required posting key (Buchungsschlüssel). ` +
`Posting keys are mandatory for DATEV compliance.`
);
continue; // Skip further validation for this line
}
// Validate account is not an automatic account (Automatikkonto)
try {
await Account.validateAccountForPosting(line.accountNumber, this.skrType);
} catch (error) {
validationErrors.push(error.message);
continue; // Skip further validation for this line
}
// Get account for posting key validation
const account = await Account.getAccountByNumber(
line.accountNumber,
this.skrType,
);
if (!account) {
throw new Error(
validationErrors.push(
`Account ${line.accountNumber} not found for ${this.skrType}`,
);
continue;
}
if (!account.isActive) {
throw new Error(`Account ${line.accountNumber} is not active`);
validationErrors.push(`Account ${line.accountNumber} is not active`);
continue;
}
// Validate posting key for this line
const amount = line.debit || line.credit || 0;
const postingKeyValidation = validatePostingKey(
line.postingKey,
line.accountNumber,
amount
);
if (!postingKeyValidation.isValid) {
validationErrors.push(...postingKeyValidation.errors);
}
if (postingKeyValidation.warnings.length > 0) {
validationWarnings.push(...postingKeyValidation.warnings);
}
}
// Validate posting key consistency across all lines
const consistencyValidation = validatePostingKeyConsistency(this.lines);
if (!consistencyValidation.isValid) {
validationErrors.push(...consistencyValidation.errors);
}
if (consistencyValidation.warnings.length > 0) {
validationWarnings.push(...consistencyValidation.warnings);
}
// Log warnings but don't fail validation
if (validationWarnings.length > 0) {
console.warn('Journal entry validation warnings:');
validationWarnings.forEach(warning => console.warn(` - ${warning}`));
}
// Throw if any errors
if (validationErrors.length > 0) {
throw new Error(
'Journal entry validation failed:\n' +
validationErrors.map(e => ` - ${e}`).join('\n')
);
}
}
@@ -325,6 +392,7 @@ export class JournalEntry extends SmartDataDbDoc<JournalEntry, JournalEntry> {
credit: line.debit, // Swap
description: `Reversal: ${line.description || ''}`,
costCenter: line.costCenter,
postingKey: line.postingKey, // Keep same posting key for reversal
}));
const reversalEntry = new JournalEntry({