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:
@@ -56,6 +56,9 @@ export class Account extends SmartDataDbDoc<Account, Account> {
|
||||
@svDb()
|
||||
public isSystemAccount: boolean;
|
||||
|
||||
@svDb()
|
||||
public isAutomaticAccount: boolean;
|
||||
|
||||
@svDb()
|
||||
public createdAt: Date;
|
||||
|
||||
@@ -90,6 +93,7 @@ export class Account extends SmartDataDbDoc<Account, Account> {
|
||||
this.debitTotal = 0;
|
||||
this.creditTotal = 0;
|
||||
this.isSystemAccount = true;
|
||||
this.isAutomaticAccount = data.isAutomaticAccount || false;
|
||||
this.createdAt = new Date();
|
||||
this.updatedAt = new Date();
|
||||
}
|
||||
@@ -157,6 +161,84 @@ export class Account extends SmartDataDbDoc<Account, Account> {
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if account number is in debtor range (10000-69999)
|
||||
* Debtor accounts (Debitorenkonten) are individual customer accounts
|
||||
*/
|
||||
public static isInDebtorRange(accountNumber: string): boolean {
|
||||
const num = parseInt(accountNumber);
|
||||
return num >= 10000 && num <= 69999;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if account number is in creditor range (70000-99999)
|
||||
* Creditor accounts (Kreditorenkonten) are individual vendor accounts
|
||||
*/
|
||||
public static isInCreditorRange(accountNumber: string): boolean {
|
||||
const num = parseInt(accountNumber);
|
||||
return num >= 70000 && num <= 99999;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if account is an automatic account (Automatikkonto)
|
||||
* Automatic accounts like 1400/1600 cannot be posted to directly
|
||||
*/
|
||||
public static isAutomaticAccount(accountNumber: string, skrType: TSKRType): boolean {
|
||||
// SKR03: 1400 (Forderungen), 1600 (Verbindlichkeiten)
|
||||
// SKR04: 1400 (Forderungen), 1600 (Verbindlichkeiten), 3300 (Alternative Verbindlichkeiten)
|
||||
if (skrType === 'SKR03') {
|
||||
return accountNumber === '1400' || accountNumber === '1600';
|
||||
} else {
|
||||
return accountNumber === '1400' || accountNumber === '1600' || accountNumber === '3300';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate account for posting - throws error if account cannot be posted to
|
||||
*/
|
||||
public static async validateAccountForPosting(
|
||||
accountNumber: string,
|
||||
skrType: TSKRType,
|
||||
): Promise<void> {
|
||||
// Check if automatic account
|
||||
if (Account.isAutomaticAccount(accountNumber, skrType)) {
|
||||
throw new Error(
|
||||
`Account ${accountNumber} is an automatic account (Automatikkonto) and cannot be posted to directly. ` +
|
||||
`Use debtor accounts (10000-69999) or creditor accounts (70000-99999) instead.`
|
||||
);
|
||||
}
|
||||
|
||||
// Get account to verify it exists
|
||||
const account = await Account.getAccountByNumber(accountNumber, skrType);
|
||||
if (!account) {
|
||||
throw new Error(
|
||||
`Account ${accountNumber} not found in ${skrType}. ` +
|
||||
`Please create the account before posting.`
|
||||
);
|
||||
}
|
||||
|
||||
// Check if account is active
|
||||
if (!account.isActive) {
|
||||
throw new Error(
|
||||
`Account ${accountNumber} is inactive and cannot be posted to.`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if this account instance is a debtor account
|
||||
*/
|
||||
public isDebtorAccount(): boolean {
|
||||
return Account.isInDebtorRange(this.accountNumber);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if this account instance is a creditor account
|
||||
*/
|
||||
public isCreditorAccount(): boolean {
|
||||
return Account.isInCreditorRange(this.accountNumber);
|
||||
}
|
||||
|
||||
public async updateBalance(
|
||||
debitAmount: number = 0,
|
||||
creditAmount: number = 0,
|
||||
@@ -209,19 +291,33 @@ export class Account extends SmartDataDbDoc<Account, Account> {
|
||||
|
||||
public async beforeSave(): Promise<void> {
|
||||
// Validate account number format
|
||||
if (!this.accountNumber || this.accountNumber.length !== 4) {
|
||||
const accountLength = this.accountNumber?.length || 0;
|
||||
if (!this.accountNumber || (accountLength !== 4 && accountLength !== 5)) {
|
||||
throw new Error(
|
||||
`Invalid account number format: ${this.accountNumber}. Must be 4 digits.`,
|
||||
`Invalid account number format: ${this.accountNumber}. Must be 4 digits (standard SKR) or 5 digits (debtor/creditor).`,
|
||||
);
|
||||
}
|
||||
|
||||
// Validate account number is numeric
|
||||
if (!/^\d{4}$/.test(this.accountNumber)) {
|
||||
if (!/^\d{4,5}$/.test(this.accountNumber)) {
|
||||
throw new Error(
|
||||
`Account number must contain only digits: ${this.accountNumber}`,
|
||||
);
|
||||
}
|
||||
|
||||
// For 5-digit accounts, validate they are in debtor (10000-69999) or creditor (70000-99999) ranges
|
||||
if (accountLength === 5) {
|
||||
const accountNum = parseInt(this.accountNumber);
|
||||
const isDebtor = accountNum >= 10000 && accountNum <= 69999;
|
||||
const isCreditor = accountNum >= 70000 && accountNum <= 99999;
|
||||
|
||||
if (!isDebtor && !isCreditor) {
|
||||
throw new Error(
|
||||
`5-digit account number ${this.accountNumber} must be in debtor range (10000-69999) or creditor range (70000-99999).`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Validate account class matches first digit
|
||||
const firstDigit = parseInt(this.accountNumber[0]);
|
||||
if (this.accountClass !== firstDigit) {
|
||||
@@ -234,5 +330,11 @@ export class Account extends SmartDataDbDoc<Account, Account> {
|
||||
if (this.skrType !== 'SKR03' && this.skrType !== 'SKR04') {
|
||||
throw new Error(`Invalid SKR type: ${this.skrType}`);
|
||||
}
|
||||
|
||||
// Mark automatic accounts (Automatikkonten)
|
||||
// These are summary accounts that cannot be posted to directly
|
||||
if (Account.isAutomaticAccount(this.accountNumber, this.skrType)) {
|
||||
this.isAutomaticAccount = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user