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:
@@ -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
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user