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

@@ -17,6 +17,24 @@ tap.test('should demonstrate complete Jahresabschluss (Annual Financial Statemen
await api.initialize('SKR03'); await api.initialize('SKR03');
expect(api.getSKRType()).toEqual('SKR03'); expect(api.getSKRType()).toEqual('SKR03');
// Create debtor account (customer) - replaces automatic account 1400
await api.createAccount({
accountNumber: '10001',
accountName: 'Kunde Mustermann GmbH',
accountClass: 1,
accountType: 'asset',
skrType: 'SKR03',
});
// Create creditor account (supplier) - replaces automatic account 1600
await api.createAccount({
accountNumber: '70001',
accountName: 'Lieferant Test GmbH',
accountClass: 7,
accountType: 'liability',
skrType: 'SKR03',
});
}); });
tap.test('should set up opening balances (Eröffnungsbilanz)', async () => { tap.test('should set up opening balances (Eröffnungsbilanz)', async () => {
@@ -24,6 +42,9 @@ tap.test('should set up opening balances (Eröffnungsbilanz)', async () => {
// This represents a small GmbH (limited liability company) // This represents a small GmbH (limited liability company)
// Using only accounts that exist in SKR03 // Using only accounts that exist in SKR03
// Note: Opening balance entries use posting key 40 (tax-free) as they are internal closing entries
// Using personal accounts (10001 for debtor, 70001 for creditor) instead of automatic accounts
// Post opening journal entry (Eröffnungsbuchung) // Post opening journal entry (Eröffnungsbuchung)
const openingEntry = await api.postJournalEntry({ const openingEntry = await api.postJournalEntry({
date: new Date('2024-01-01'), date: new Date('2024-01-01'),
@@ -31,20 +52,20 @@ tap.test('should set up opening balances (Eröffnungsbilanz)', async () => {
reference: 'EB-2024', reference: 'EB-2024',
lines: [ lines: [
// Debit all asset accounts // Debit all asset accounts
{ accountNumber: '0200', debit: 45000, description: 'Grundstücke' }, { accountNumber: '0200', debit: 45000, description: 'Grundstücke', postingKey: 40 },
{ accountNumber: '0210', debit: 120000, description: 'Gebäude' }, { accountNumber: '0210', debit: 120000, description: 'Gebäude', postingKey: 40 },
{ accountNumber: '0500', debit: 35000, description: 'Betriebs- und Geschäftsausstattung' }, { accountNumber: '0500', debit: 35000, description: 'Betriebs- und Geschäftsausstattung', postingKey: 40 },
{ accountNumber: '0400', debit: 8000, description: 'Fuhrpark' }, { accountNumber: '0400', debit: 8000, description: 'Fuhrpark', postingKey: 40 },
{ accountNumber: '1200', debit: 25000, description: 'Bank' }, { accountNumber: '1200', debit: 25000, description: 'Bank', postingKey: 40 },
{ accountNumber: '1000', debit: 2500, description: 'Kasse' }, { accountNumber: '1000', debit: 2500, description: 'Kasse', postingKey: 40 },
{ accountNumber: '1400', debit: 18000, description: 'Forderungen' }, { accountNumber: '10001', debit: 18000, description: 'Forderungen Kunde', postingKey: 40 },
{ accountNumber: '3100', debit: 12000, description: 'Warenvorräte' }, { accountNumber: '3100', debit: 12000, description: 'Warenvorräte', postingKey: 40 },
// Credit all liability and equity accounts // Credit all liability and equity accounts
{ accountNumber: '2000', credit: 150000, description: 'Eigenkapital' }, { accountNumber: '2000', credit: 150000, description: 'Eigenkapital', postingKey: 40 },
{ accountNumber: '2900', credit: 35000, description: 'Gewinnrücklagen' }, { accountNumber: '2900', credit: 35000, description: 'Gewinnrücklagen', postingKey: 40 },
{ accountNumber: '1600', credit: 52500, description: 'Verbindlichkeiten L+L' }, { accountNumber: '70001', credit: 52500, description: 'Verbindlichkeiten Lieferant', postingKey: 40 },
{ accountNumber: '3300', credit: 28000, description: 'Verbindlichkeiten Kreditinstitute' }, { accountNumber: '3300', credit: 28000, description: 'Verbindlichkeiten Kreditinstitute', postingKey: 40 },
], ],
skrType: 'SKR03', skrType: 'SKR03',
}); });
@@ -57,28 +78,28 @@ tap.test('should set up opening balances (Eröffnungsbilanz)', async () => {
tap.test('should record Q1 business transactions', async () => { tap.test('should record Q1 business transactions', async () => {
// January - March transactions // January - March transactions
// Sale of goods with 19% VAT // Sale of goods with 19% VAT - using debtor account 10001 instead of automatic 1400
await api.postJournalEntry({ await api.postJournalEntry({
date: new Date('2024-01-15'), date: new Date('2024-01-15'),
description: 'Verkauf Waren auf Rechnung', description: 'Verkauf Waren auf Rechnung',
reference: 'RE-2024-001', reference: 'RE-2024-001',
lines: [ lines: [
{ accountNumber: '1400', debit: 11900, description: 'Forderungen inkl. USt' }, { accountNumber: '10001', debit: 11900, description: 'Forderungen inkl. USt', postingKey: 9 },
{ accountNumber: '8400', credit: 10000, description: 'Erlöse 19% USt' }, { accountNumber: '8400', credit: 10000, description: 'Erlöse 19% USt', postingKey: 40 },
{ accountNumber: '1771', credit: 1900, description: 'Umsatzsteuer 19%' }, { accountNumber: '1771', credit: 1900, description: 'Umsatzsteuer 19%', postingKey: 40 },
], ],
skrType: 'SKR03', skrType: 'SKR03',
}); });
// Purchase of materials with 19% VAT // Purchase of materials with 19% VAT - using creditor account 70001 instead of automatic 1600
await api.postJournalEntry({ await api.postJournalEntry({
date: new Date('2024-01-20'), date: new Date('2024-01-20'),
description: 'Einkauf Material auf Rechnung', description: 'Einkauf Material auf Rechnung',
reference: 'ER-2024-001', reference: 'ER-2024-001',
lines: [ lines: [
{ accountNumber: '5400', debit: 5000, description: 'Wareneingang 19% Vorsteuer' }, { accountNumber: '5400', debit: 5000, description: 'Wareneingang 19% Vorsteuer', postingKey: 40 },
{ accountNumber: '1571', debit: 950, description: 'Vorsteuer 19%' }, { accountNumber: '1571', debit: 950, description: 'Vorsteuer 19%', postingKey: 9 },
{ accountNumber: '1600', credit: 5950, description: 'Verbindlichkeiten' }, { accountNumber: '70001', credit: 5950, description: 'Verbindlichkeiten', postingKey: 9 },
], ],
skrType: 'SKR03', skrType: 'SKR03',
}); });
@@ -89,21 +110,21 @@ tap.test('should record Q1 business transactions', async () => {
description: 'Gehaltszahlung Januar', description: 'Gehaltszahlung Januar',
reference: 'GH-2024-01', reference: 'GH-2024-01',
lines: [ lines: [
{ accountNumber: '6000', debit: 8000, description: 'Löhne und Gehälter' }, { accountNumber: '6000', debit: 8000, description: 'Löhne und Gehälter', postingKey: 40 },
{ accountNumber: '6100', debit: 1600, description: 'Sozialversicherung AG-Anteil' }, { accountNumber: '6100', debit: 1600, description: 'Sozialversicherung AG-Anteil', postingKey: 40 },
{ accountNumber: '1200', credit: 9600, description: 'Banküberweisung' }, { accountNumber: '1200', credit: 9600, description: 'Banküberweisung', postingKey: 40 },
], ],
skrType: 'SKR03', skrType: 'SKR03',
}); });
// Customer payment received // Customer payment received - using debtor account 10001 instead of automatic 1400
await api.postJournalEntry({ await api.postJournalEntry({
date: new Date('2024-02-10'), date: new Date('2024-02-10'),
description: 'Zahlungseingang Kunde', description: 'Zahlungseingang Kunde',
reference: 'ZE-2024-001', reference: 'ZE-2024-001',
lines: [ lines: [
{ accountNumber: '1200', debit: 11900, description: 'Bankgutschrift' }, { accountNumber: '1200', debit: 11900, description: 'Bankgutschrift', postingKey: 40 },
{ accountNumber: '1400', credit: 11900, description: 'Forderungsausgleich' }, { accountNumber: '10001', credit: 11900, description: 'Forderungsausgleich', postingKey: 3 },
], ],
skrType: 'SKR03', skrType: 'SKR03',
}); });
@@ -114,8 +135,8 @@ tap.test('should record Q1 business transactions', async () => {
description: 'Miete Februar', description: 'Miete Februar',
reference: 'MI-2024-02', reference: 'MI-2024-02',
lines: [ lines: [
{ accountNumber: '7100', debit: 2000, description: 'Miete' }, { accountNumber: '7100', debit: 2000, description: 'Miete', postingKey: 40 },
{ accountNumber: '1200', credit: 2000, description: 'Banküberweisung' }, { accountNumber: '1200', credit: 2000, description: 'Banküberweisung', postingKey: 40 },
], ],
skrType: 'SKR03', skrType: 'SKR03',
}); });
@@ -126,9 +147,9 @@ tap.test('should record Q1 business transactions', async () => {
description: 'Büromaterial', description: 'Büromaterial',
reference: 'BM-2024-001', reference: 'BM-2024-001',
lines: [ lines: [
{ accountNumber: '6800', debit: 200, description: 'Bürobedarf' }, { accountNumber: '6800', debit: 200, description: 'Bürobedarf', postingKey: 40 },
{ accountNumber: '1571', debit: 38, description: 'Vorsteuer 19%' }, { accountNumber: '1571', debit: 38, description: 'Vorsteuer 19%', postingKey: 9 },
{ accountNumber: '1200', credit: 238, description: 'Bankzahlung' }, { accountNumber: '1200', credit: 238, description: 'Bankzahlung', postingKey: 40 },
], ],
skrType: 'SKR03', skrType: 'SKR03',
}); });
@@ -139,22 +160,22 @@ tap.test('should record Q1 business transactions', async () => {
description: 'Tankrechnung Firmenfahrzeug', description: 'Tankrechnung Firmenfahrzeug',
reference: 'KFZ-2024-001', reference: 'KFZ-2024-001',
lines: [ lines: [
{ accountNumber: '7400', debit: 150, description: 'Kfz-Kosten' }, { accountNumber: '7400', debit: 150, description: 'Kfz-Kosten', postingKey: 40 },
{ accountNumber: '1571', debit: 28.50, description: 'Vorsteuer 19%' }, { accountNumber: '1571', debit: 28.50, description: 'Vorsteuer 19%', postingKey: 9 },
{ accountNumber: '1200', credit: 178.50, description: 'Bankzahlung' }, { accountNumber: '1200', credit: 178.50, description: 'Bankzahlung', postingKey: 40 },
], ],
skrType: 'SKR03', skrType: 'SKR03',
}); });
// Another sale // Another sale - using debtor account 10001 instead of automatic 1400
await api.postJournalEntry({ await api.postJournalEntry({
date: new Date('2024-03-20'), date: new Date('2024-03-20'),
description: 'Verkauf Dienstleistung', description: 'Verkauf Dienstleistung',
reference: 'RE-2024-002', reference: 'RE-2024-002',
lines: [ lines: [
{ accountNumber: '1400', debit: 7140, description: 'Forderungen inkl. USt' }, { accountNumber: '10001', debit: 7140, description: 'Forderungen inkl. USt', postingKey: 9 },
{ accountNumber: '8400', credit: 6000, description: 'Erlöse 19% USt' }, { accountNumber: '8400', credit: 6000, description: 'Erlöse 19% USt', postingKey: 40 },
{ accountNumber: '1771', credit: 1140, description: 'Umsatzsteuer 19%' }, { accountNumber: '1771', credit: 1140, description: 'Umsatzsteuer 19%', postingKey: 40 },
], ],
skrType: 'SKR03', skrType: 'SKR03',
}); });
@@ -169,35 +190,35 @@ tap.test('should record Q2-Q4 business transactions', async () => {
description: 'Kauf neue Produktionsmaschine', description: 'Kauf neue Produktionsmaschine',
reference: 'INV-2024-001', reference: 'INV-2024-001',
lines: [ lines: [
{ accountNumber: '0500', debit: 25000, description: 'Neue Maschine' }, { accountNumber: '0500', debit: 25000, description: 'Neue Maschine', postingKey: 40 },
{ accountNumber: '1571', debit: 4750, description: 'Vorsteuer 19%' }, { accountNumber: '1571', debit: 4750, description: 'Vorsteuer 19%', postingKey: 9 },
{ accountNumber: '1200', credit: 29750, description: 'Banküberweisung' }, { accountNumber: '1200', credit: 29750, description: 'Banküberweisung', postingKey: 40 },
], ],
skrType: 'SKR03', skrType: 'SKR03',
}); });
// Q2: Large sale // Q2: Large sale - using debtor account 10001
await api.postJournalEntry({ await api.postJournalEntry({
date: new Date('2024-05-10'), date: new Date('2024-05-10'),
description: 'Großauftrag Kunde ABC', description: 'Großauftrag Kunde ABC',
reference: 'RE-2024-003', reference: 'RE-2024-003',
lines: [ lines: [
{ accountNumber: '1400', debit: 35700, description: 'Forderungen inkl. USt' }, { accountNumber: '10001', debit: 35700, description: 'Forderungen inkl. USt', postingKey: 9 },
{ accountNumber: '8400', credit: 30000, description: 'Erlöse 19% USt' }, { accountNumber: '8400', credit: 30000, description: 'Erlöse 19% USt', postingKey: 40 },
{ accountNumber: '1771', credit: 5700, description: 'Umsatzsteuer 19%' }, { accountNumber: '1771', credit: 5700, description: 'Umsatzsteuer 19%', postingKey: 40 },
], ],
skrType: 'SKR03', skrType: 'SKR03',
}); });
// Q3: Marketing expenses // Q3: Marketing expenses - using creditor account 70001
await api.postJournalEntry({ await api.postJournalEntry({
date: new Date('2024-07-10'), date: new Date('2024-07-10'),
description: 'Werbekampagne', description: 'Werbekampagne',
reference: 'WK-2024-001', reference: 'WK-2024-001',
lines: [ lines: [
{ accountNumber: '6600', debit: 5000, description: 'Werbekosten' }, { accountNumber: '6600', debit: 5000, description: 'Werbekosten', postingKey: 40 },
{ accountNumber: '1571', debit: 950, description: 'Vorsteuer 19%' }, { accountNumber: '1571', debit: 950, description: 'Vorsteuer 19%', postingKey: 9 },
{ accountNumber: '1600', credit: 5950, description: 'Verbindlichkeiten' }, { accountNumber: '70001', credit: 5950, description: 'Verbindlichkeiten', postingKey: 9 },
], ],
skrType: 'SKR03', skrType: 'SKR03',
}); });
@@ -208,9 +229,9 @@ tap.test('should record Q2-Q4 business transactions', async () => {
description: 'Steuerberatung', description: 'Steuerberatung',
reference: 'STB-2024-001', reference: 'STB-2024-001',
lines: [ lines: [
{ accountNumber: '6700', debit: 2500, description: 'Steuerberatungskosten' }, { accountNumber: '6700', debit: 2500, description: 'Steuerberatungskosten', postingKey: 40 },
{ accountNumber: '1571', debit: 475, description: 'Vorsteuer 19%' }, { accountNumber: '1571', debit: 475, description: 'Vorsteuer 19%', postingKey: 9 },
{ accountNumber: '1200', credit: 2975, description: 'Banküberweisung' }, { accountNumber: '1200', credit: 2975, description: 'Banküberweisung', postingKey: 40 },
], ],
skrType: 'SKR03', skrType: 'SKR03',
}); });
@@ -221,35 +242,35 @@ tap.test('should record Q2-Q4 business transactions', async () => {
description: 'Jahresbonus Mitarbeiter', description: 'Jahresbonus Mitarbeiter',
reference: 'BON-2024', reference: 'BON-2024',
lines: [ lines: [
{ accountNumber: '6000', debit: 10000, description: 'Tantieme' }, { accountNumber: '6000', debit: 10000, description: 'Tantieme', postingKey: 40 },
{ accountNumber: '6100', debit: 2000, description: 'Sozialversicherung AG-Anteil' }, { accountNumber: '6100', debit: 2000, description: 'Sozialversicherung AG-Anteil', postingKey: 40 },
{ accountNumber: '1200', credit: 12000, description: 'Banküberweisung' }, { accountNumber: '1200', credit: 12000, description: 'Banküberweisung', postingKey: 40 },
], ],
skrType: 'SKR03', skrType: 'SKR03',
}); });
// Q4: Collection of outstanding receivables // Q4: Collection of outstanding receivables - using debtor account 10001
await api.postJournalEntry({ await api.postJournalEntry({
date: new Date('2024-12-15'), date: new Date('2024-12-15'),
description: 'Zahlungseingang Großauftrag', description: 'Zahlungseingang Großauftrag',
reference: 'ZE-2024-003', reference: 'ZE-2024-003',
lines: [ lines: [
{ accountNumber: '1200', debit: 35700, description: 'Bankgutschrift' }, { accountNumber: '1200', debit: 35700, description: 'Bankgutschrift', postingKey: 40 },
{ accountNumber: '1400', credit: 35700, description: 'Forderungsausgleich' }, { accountNumber: '10001', credit: 35700, description: 'Forderungsausgleich', postingKey: 3 },
], ],
skrType: 'SKR03', skrType: 'SKR03',
}); });
}); });
tap.test('should perform year-end adjustments (Jahresabschlussbuchungen)', async () => { tap.test('should perform year-end adjustments (Jahresabschlussbuchungen)', async () => {
// 1. Depreciation (Abschreibungen) // 1. Depreciation (Abschreibungen) - internal adjustments use posting key 40
await api.postJournalEntry({ await api.postJournalEntry({
date: new Date('2024-12-31'), date: new Date('2024-12-31'),
description: 'Abschreibung Gebäude (linear 2%)', description: 'Abschreibung Gebäude (linear 2%)',
reference: 'AFA-2024-001', reference: 'AFA-2024-001',
lines: [ lines: [
{ accountNumber: '7000', debit: 2400, description: 'AfA auf Gebäude' }, { accountNumber: '7000', debit: 2400, description: 'AfA auf Gebäude', postingKey: 40 },
{ accountNumber: '0210', credit: 2400, description: 'Wertberichtigung Gebäude' }, { accountNumber: '0210', credit: 2400, description: 'Wertberichtigung Gebäude', postingKey: 40 },
], ],
skrType: 'SKR03', skrType: 'SKR03',
}); });
@@ -259,8 +280,8 @@ tap.test('should perform year-end adjustments (Jahresabschlussbuchungen)', async
description: 'Abschreibung BGA (linear 10%)', description: 'Abschreibung BGA (linear 10%)',
reference: 'AFA-2024-002', reference: 'AFA-2024-002',
lines: [ lines: [
{ accountNumber: '7000', debit: 6000, description: 'AfA auf BGA' }, // (35000 + 25000) * 10% { accountNumber: '7000', debit: 6000, description: 'AfA auf BGA', postingKey: 40 }, // (35000 + 25000) * 10%
{ accountNumber: '0500', credit: 6000, description: 'Wertberichtigung BGA' }, { accountNumber: '0500', credit: 6000, description: 'Wertberichtigung BGA', postingKey: 40 },
], ],
skrType: 'SKR03', skrType: 'SKR03',
}); });
@@ -270,57 +291,57 @@ tap.test('should perform year-end adjustments (Jahresabschlussbuchungen)', async
description: 'Abschreibung Fuhrpark (linear 20%)', description: 'Abschreibung Fuhrpark (linear 20%)',
reference: 'AFA-2024-003', reference: 'AFA-2024-003',
lines: [ lines: [
{ accountNumber: '7000', debit: 1600, description: 'AfA auf Fuhrpark' }, { accountNumber: '7000', debit: 1600, description: 'AfA auf Fuhrpark', postingKey: 40 },
{ accountNumber: '0400', credit: 1600, description: 'Wertberichtigung Fuhrpark' }, { accountNumber: '0400', credit: 1600, description: 'Wertberichtigung Fuhrpark', postingKey: 40 },
], ],
skrType: 'SKR03', skrType: 'SKR03',
}); });
// 2. Accruals (Rechnungsabgrenzung) // 2. Accruals (Rechnungsabgrenzung) - internal adjustments use posting key 40
await api.postJournalEntry({ await api.postJournalEntry({
date: new Date('2024-12-31'), date: new Date('2024-12-31'),
description: 'Aktive Rechnungsabgrenzung - Vorausbezahlte Versicherung', description: 'Aktive Rechnungsabgrenzung - Vorausbezahlte Versicherung',
reference: 'ARA-2024-001', reference: 'ARA-2024-001',
lines: [ lines: [
{ accountNumber: '1900', debit: 1000, description: 'Aktive Rechnungsabgrenzung' }, { accountNumber: '1900', debit: 1000, description: 'Aktive Rechnungsabgrenzung', postingKey: 40 },
{ accountNumber: '7300', credit: 1000, description: 'Versicherungen' }, { accountNumber: '7300', credit: 1000, description: 'Versicherungen', postingKey: 40 },
], ],
skrType: 'SKR03', skrType: 'SKR03',
}); });
// 3. Provisions (Rückstellungen) // 3. Provisions (Rückstellungen) - internal adjustments use posting key 40
await api.postJournalEntry({ await api.postJournalEntry({
date: new Date('2024-12-31'), date: new Date('2024-12-31'),
description: 'Rückstellung für Jahresabschlusskosten', description: 'Rückstellung für Jahresabschlusskosten',
reference: 'RS-2024-001', reference: 'RS-2024-001',
lines: [ lines: [
{ accountNumber: '6700', debit: 3000, description: 'Rechts- und Beratungskosten' }, { accountNumber: '6700', debit: 3000, description: 'Rechts- und Beratungskosten', postingKey: 40 },
{ accountNumber: '3000', credit: 3000, description: 'Rückstellungen' }, { accountNumber: '3000', credit: 3000, description: 'Rückstellungen', postingKey: 40 },
], ],
skrType: 'SKR03', skrType: 'SKR03',
}); });
// 4. Inventory adjustment // 4. Inventory adjustment - internal adjustments use posting key 40
await api.postJournalEntry({ await api.postJournalEntry({
date: new Date('2024-12-31'), date: new Date('2024-12-31'),
description: 'Bestandsveränderung Waren', description: 'Bestandsveränderung Waren',
reference: 'BV-2024-001', reference: 'BV-2024-001',
lines: [ lines: [
{ accountNumber: '3100', debit: 3000, description: 'Warenbestand Zugang' }, { accountNumber: '3100', debit: 3000, description: 'Warenbestand Zugang', postingKey: 40 },
{ accountNumber: '5900', credit: 3000, description: 'Bestandsveränderungen' }, { accountNumber: '5900', credit: 3000, description: 'Bestandsveränderungen', postingKey: 40 },
], ],
skrType: 'SKR03', skrType: 'SKR03',
}); });
// 5. VAT clearing (Umsatzsteuer-Vorauszahlung) // 5. VAT clearing (Umsatzsteuer-Vorauszahlung) - internal adjustments use posting key 40
await api.postJournalEntry({ await api.postJournalEntry({
date: new Date('2024-12-31'), date: new Date('2024-12-31'),
description: 'USt-Abschluss Q4', description: 'USt-Abschluss Q4',
reference: 'UST-2024-Q4', reference: 'UST-2024-Q4',
lines: [ lines: [
{ accountNumber: '1771', debit: 8740, description: 'USt-Saldo' }, // Total collected VAT { accountNumber: '1771', debit: 8740, description: 'USt-Saldo', postingKey: 40 }, // Total collected VAT
{ accountNumber: '1571', credit: 7191.50, description: 'Vorsteuer-Saldo' }, // Total input VAT { accountNumber: '1571', credit: 7191.50, description: 'Vorsteuer-Saldo', postingKey: 40 }, // Total input VAT
{ accountNumber: '1800', credit: 1548.50, description: 'USt-Zahllast' }, { accountNumber: '1800', credit: 1548.50, description: 'USt-Zahllast', postingKey: 40 },
], ],
skrType: 'SKR03', skrType: 'SKR03',
}); });
@@ -366,41 +387,41 @@ tap.test('should calculate income statement (GuV) before closing', async () => {
tap.test('should perform closing entries (Abschlussbuchungen)', async () => { tap.test('should perform closing entries (Abschlussbuchungen)', async () => {
// Close all income and expense accounts to the profit/loss account // Close all income and expense accounts to the profit/loss account
// Close revenue accounts // Close revenue accounts - year-end closing uses posting key 40
await api.postJournalEntry({ await api.postJournalEntry({
date: new Date('2024-12-31'), date: new Date('2024-12-31'),
description: 'Abschluss Ertragskonten', description: 'Abschluss Ertragskonten',
reference: 'AB-2024-001', reference: 'AB-2024-001',
lines: [ lines: [
{ accountNumber: '8400', debit: 46000, description: 'Erlöse abschließen' }, { accountNumber: '8400', debit: 46000, description: 'Erlöse abschließen', postingKey: 40 },
{ accountNumber: '9400', credit: 46000, description: 'GuV-Konto' }, { accountNumber: '9400', credit: 46000, description: 'GuV-Konto', postingKey: 40 },
], ],
skrType: 'SKR03', skrType: 'SKR03',
}); });
// Close expense accounts // Close expense accounts - year-end closing uses posting key 40
await api.postJournalEntry({ await api.postJournalEntry({
date: new Date('2024-12-31'), date: new Date('2024-12-31'),
description: 'Abschluss Aufwandskonten', description: 'Abschluss Aufwandskonten',
reference: 'AB-2024-002', reference: 'AB-2024-002',
lines: [ lines: [
{ accountNumber: '9400', debit: 45450, description: 'GuV-Konto' }, { accountNumber: '9400', debit: 45450, description: 'GuV-Konto', postingKey: 40 },
{ accountNumber: '7300', debit: 1000, description: 'Versicherung abschließen (credit balance)' }, { accountNumber: '7300', debit: 1000, description: 'Versicherung abschließen (credit balance)', postingKey: 40 },
{ accountNumber: '5900', debit: 3000, description: 'Bestandsveränderungen abschließen (credit balance)' }, { accountNumber: '5900', debit: 3000, description: 'Bestandsveränderungen abschließen (credit balance)', postingKey: 40 },
{ accountNumber: '5400', credit: 5000, description: 'Wareneingang abschließen' }, { accountNumber: '5400', credit: 5000, description: 'Wareneingang abschließen', postingKey: 40 },
{ accountNumber: '6000', credit: 18000, description: 'Löhne und Gehälter abschließen' }, { accountNumber: '6000', credit: 18000, description: 'Löhne und Gehälter abschließen', postingKey: 40 },
{ accountNumber: '6100', credit: 3600, description: 'SV AG-Anteil abschließen' }, { accountNumber: '6100', credit: 3600, description: 'SV AG-Anteil abschließen', postingKey: 40 },
{ accountNumber: '7000', credit: 10000, description: 'AfA abschließen' }, { accountNumber: '7000', credit: 10000, description: 'AfA abschließen', postingKey: 40 },
{ accountNumber: '7100', credit: 2000, description: 'Miete abschließen' }, { accountNumber: '7100', credit: 2000, description: 'Miete abschließen', postingKey: 40 },
{ accountNumber: '7400', credit: 150, description: 'Kfz abschließen' }, { accountNumber: '7400', credit: 150, description: 'Kfz abschließen', postingKey: 40 },
{ accountNumber: '6600', credit: 5000, description: 'Werbung abschließen' }, { accountNumber: '6600', credit: 5000, description: 'Werbung abschließen', postingKey: 40 },
{ accountNumber: '6700', credit: 5500, description: 'Beratung abschließen' }, { accountNumber: '6700', credit: 5500, description: 'Beratung abschließen', postingKey: 40 },
{ accountNumber: '6800', credit: 200, description: 'Bürobedarf abschließen' }, { accountNumber: '6800', credit: 200, description: 'Bürobedarf abschließen', postingKey: 40 },
], ],
skrType: 'SKR03', skrType: 'SKR03',
}); });
// Transfer profit/loss to equity // Transfer profit/loss to equity - year-end closing uses posting key 40
const guv_result = 46000 - 45450; // Profit of 550 const guv_result = 46000 - 45450; // Profit of 550
if (guv_result > 0) { if (guv_result > 0) {
await api.postJournalEntry({ await api.postJournalEntry({
@@ -408,8 +429,8 @@ tap.test('should perform closing entries (Abschlussbuchungen)', async () => {
description: 'Jahresgewinn auf Eigenkapital', description: 'Jahresgewinn auf Eigenkapital',
reference: 'AB-2024-003', reference: 'AB-2024-003',
lines: [ lines: [
{ accountNumber: '9400', debit: guv_result, description: 'GuV-Konto ausgleichen' }, { accountNumber: '9400', debit: guv_result, description: 'GuV-Konto ausgleichen', postingKey: 40 },
{ accountNumber: '2900', credit: guv_result, description: 'Gewinnrücklagen' }, { accountNumber: '2900', credit: guv_result, description: 'Gewinnrücklagen', postingKey: 40 },
], ],
skrType: 'SKR03', skrType: 'SKR03',
}); });
@@ -419,8 +440,8 @@ tap.test('should perform closing entries (Abschlussbuchungen)', async () => {
description: 'Jahresverlust auf Eigenkapital', description: 'Jahresverlust auf Eigenkapital',
reference: 'AB-2024-003', reference: 'AB-2024-003',
lines: [ lines: [
{ accountNumber: '2500', debit: Math.abs(guv_result), description: 'Verlustvortrag' }, { accountNumber: '2500', debit: Math.abs(guv_result), description: 'Verlustvortrag', postingKey: 40 },
{ accountNumber: '9400', credit: Math.abs(guv_result), description: 'GuV-Konto ausgleichen' }, { accountNumber: '9400', credit: Math.abs(guv_result), description: 'GuV-Konto ausgleichen', postingKey: 40 },
], ],
skrType: 'SKR03', skrType: 'SKR03',
}); });

View File

@@ -17,11 +17,30 @@ tap.test('should demonstrate complete Jahresabschluss (Annual Financial Statemen
await api.initialize('SKR04'); await api.initialize('SKR04');
expect(api.getSKRType()).toEqual('SKR04'); expect(api.getSKRType()).toEqual('SKR04');
// Create debtor account (customer) - replaces automatic account 1400
await api.createAccount({
accountNumber: '10001',
accountName: 'Kunde Mustermann GmbH',
accountClass: 1,
accountType: 'asset',
skrType: 'SKR04',
});
// Create creditor account (supplier) - replaces automatic account 1600
await api.createAccount({
accountNumber: '70001',
accountName: 'Lieferant Test GmbH',
accountClass: 7,
accountType: 'liability',
skrType: 'SKR04',
});
}); });
tap.test('should set up opening balances (Eröffnungsbilanz) for SKR04', async () => { tap.test('should set up opening balances (Eröffnungsbilanz) for SKR04', async () => {
// Opening balances from previous year's closing // Opening balances from previous year's closing
// SKR04 uses different account structure than SKR03 // SKR04 uses different account structure than SKR03
// Using personal accounts (10001 for debtor, 70001 for creditor) instead of automatic accounts
// Post opening journal entry (Eröffnungsbuchung) // Post opening journal entry (Eröffnungsbuchung)
const openingEntry = await api.postJournalEntry({ const openingEntry = await api.postJournalEntry({
@@ -30,19 +49,19 @@ tap.test('should set up opening balances (Eröffnungsbilanz) for SKR04', async (
reference: 'EB-2024', reference: 'EB-2024',
lines: [ lines: [
// Debit all asset accounts // Debit all asset accounts
{ accountNumber: '0200', debit: 45000, description: 'Grundstücke' }, { accountNumber: '0200', debit: 45000, description: 'Grundstücke', postingKey: 40 },
{ accountNumber: '0210', debit: 120000, description: 'Gebäude' }, { accountNumber: '0210', debit: 120000, description: 'Gebäude', postingKey: 40 },
{ accountNumber: '0500', debit: 35000, description: 'BGA' }, { accountNumber: '0500', debit: 35000, description: 'BGA', postingKey: 40 },
{ accountNumber: '0400', debit: 8000, description: 'Fuhrpark' }, { accountNumber: '0400', debit: 8000, description: 'Fuhrpark', postingKey: 40 },
{ accountNumber: '1200', debit: 25000, description: 'Bank' }, { accountNumber: '1200', debit: 25000, description: 'Bank', postingKey: 40 },
{ accountNumber: '1000', debit: 2500, description: 'Kasse' }, { accountNumber: '1000', debit: 2500, description: 'Kasse', postingKey: 40 },
{ accountNumber: '1400', debit: 18000, description: 'Forderungen' }, { accountNumber: '10001', debit: 18000, description: 'Forderungen Kunde', postingKey: 40 },
// Credit all liability and equity accounts // Credit all liability and equity accounts
{ accountNumber: '9000', credit: 150000, description: 'Eigenkapital' }, { accountNumber: '9000', credit: 150000, description: 'Eigenkapital', postingKey: 40 },
{ accountNumber: '9300', credit: 35000, description: 'Gewinnrücklagen' }, { accountNumber: '9300', credit: 35000, description: 'Gewinnrücklagen', postingKey: 40 },
{ accountNumber: '1600', credit: 40500, description: 'Verbindlichkeiten L+L' }, { accountNumber: '70001', credit: 40500, description: 'Verbindlichkeiten Lieferant', postingKey: 40 },
{ accountNumber: '1700', credit: 28000, description: 'Sonstige Verbindlichkeiten' }, { accountNumber: '1700', credit: 28000, description: 'Sonstige Verbindlichkeiten', postingKey: 40 },
], ],
skrType: 'SKR04', skrType: 'SKR04',
}); });
@@ -61,9 +80,9 @@ tap.test('should record Q1 business transactions for SKR04', async () => {
description: 'Verkauf Waren auf Rechnung', description: 'Verkauf Waren auf Rechnung',
reference: 'RE-2024-001', reference: 'RE-2024-001',
lines: [ lines: [
{ accountNumber: '1400', debit: 11900, description: 'Forderungen inkl. USt' }, { accountNumber: '10001', debit: 11900, description: 'Forderungen inkl. USt', postingKey: 9 },
{ accountNumber: '4300', credit: 10000, description: 'Erlöse 19% USt' }, { accountNumber: '4300', credit: 10000, description: 'Erlöse 19% USt', postingKey: 40 },
{ accountNumber: '1771', credit: 1900, description: 'Umsatzsteuer 19%' }, { accountNumber: '1771', credit: 1900, description: 'Umsatzsteuer 19%', postingKey: 40 },
], ],
skrType: 'SKR04', skrType: 'SKR04',
}); });
@@ -74,9 +93,9 @@ tap.test('should record Q1 business transactions for SKR04', async () => {
description: 'Einkauf Material auf Rechnung', description: 'Einkauf Material auf Rechnung',
reference: 'ER-2024-001', reference: 'ER-2024-001',
lines: [ lines: [
{ accountNumber: '2100', debit: 5000, description: 'Bezogene Waren' }, { accountNumber: '2100', debit: 5000, description: 'Bezogene Waren', postingKey: 40 },
{ accountNumber: '1571', debit: 950, description: 'Vorsteuer 19%' }, { accountNumber: '1571', debit: 950, description: 'Vorsteuer 19%', postingKey: 9 },
{ accountNumber: '1600', credit: 5950, description: 'Verbindlichkeiten' }, { accountNumber: '70001', credit: 5950, description: 'Verbindlichkeiten', postingKey: 9 },
], ],
skrType: 'SKR04', skrType: 'SKR04',
}); });
@@ -87,9 +106,9 @@ tap.test('should record Q1 business transactions for SKR04', async () => {
description: 'Gehaltszahlung Januar', description: 'Gehaltszahlung Januar',
reference: 'GH-2024-01', reference: 'GH-2024-01',
lines: [ lines: [
{ accountNumber: '2300', debit: 8000, description: 'Löhne' }, { accountNumber: '2300', debit: 8000, description: 'Löhne', postingKey: 40 },
{ accountNumber: '2400', debit: 1600, description: 'Gehälter' }, { accountNumber: '2400', debit: 1600, description: 'Gehälter', postingKey: 40 },
{ accountNumber: '1200', credit: 9600, description: 'Banküberweisung' }, { accountNumber: '1200', credit: 9600, description: 'Banküberweisung', postingKey: 40 },
], ],
skrType: 'SKR04', skrType: 'SKR04',
}); });
@@ -100,8 +119,8 @@ tap.test('should record Q1 business transactions for SKR04', async () => {
description: 'Zahlungseingang Kunde', description: 'Zahlungseingang Kunde',
reference: 'ZE-2024-001', reference: 'ZE-2024-001',
lines: [ lines: [
{ accountNumber: '1200', debit: 11900, description: 'Bankgutschrift' }, { accountNumber: '1200', debit: 11900, description: 'Bankgutschrift', postingKey: 40 },
{ accountNumber: '1400', credit: 11900, description: 'Forderungsausgleich' }, { accountNumber: '10001', credit: 11900, description: 'Forderungsausgleich', postingKey: 3 },
], ],
skrType: 'SKR04', skrType: 'SKR04',
}); });
@@ -112,8 +131,8 @@ tap.test('should record Q1 business transactions for SKR04', async () => {
description: 'Miete Februar', description: 'Miete Februar',
reference: 'MI-2024-02', reference: 'MI-2024-02',
lines: [ lines: [
{ accountNumber: '3000', debit: 2000, description: 'Miete' }, { accountNumber: '3000', debit: 2000, description: 'Miete', postingKey: 40 },
{ accountNumber: '1200', credit: 2000, description: 'Banküberweisung' }, { accountNumber: '1200', credit: 2000, description: 'Banküberweisung', postingKey: 40 },
], ],
skrType: 'SKR04', skrType: 'SKR04',
}); });
@@ -124,9 +143,9 @@ tap.test('should record Q1 business transactions for SKR04', async () => {
description: 'Büromaterial', description: 'Büromaterial',
reference: 'BM-2024-001', reference: 'BM-2024-001',
lines: [ lines: [
{ accountNumber: '3100', debit: 200, description: 'Bürobedarf' }, { accountNumber: '3100', debit: 200, description: 'Bürobedarf', postingKey: 40 },
{ accountNumber: '1571', debit: 38, description: 'Vorsteuer 19%' }, { accountNumber: '1571', debit: 38, description: 'Vorsteuer 19%', postingKey: 9 },
{ accountNumber: '1200', credit: 238, description: 'Bankzahlung' }, { accountNumber: '1200', credit: 238, description: 'Bankzahlung', postingKey: 40 },
], ],
skrType: 'SKR04', skrType: 'SKR04',
}); });
@@ -137,9 +156,9 @@ tap.test('should record Q1 business transactions for SKR04', async () => {
description: 'Tankrechnung Firmenfahrzeug', description: 'Tankrechnung Firmenfahrzeug',
reference: 'KFZ-2024-001', reference: 'KFZ-2024-001',
lines: [ lines: [
{ accountNumber: '3300', debit: 150, description: 'Kfz-Kosten' }, { accountNumber: '3300', debit: 150, description: 'Kfz-Kosten', postingKey: 40 },
{ accountNumber: '1571', debit: 28.50, description: 'Vorsteuer 19%' }, { accountNumber: '1571', debit: 28.50, description: 'Vorsteuer 19%', postingKey: 9 },
{ accountNumber: '1200', credit: 178.50, description: 'Bankzahlung' }, { accountNumber: '1200', credit: 178.50, description: 'Bankzahlung', postingKey: 40 },
], ],
skrType: 'SKR04', skrType: 'SKR04',
}); });
@@ -150,9 +169,9 @@ tap.test('should record Q1 business transactions for SKR04', async () => {
description: 'Verkauf Dienstleistung', description: 'Verkauf Dienstleistung',
reference: 'RE-2024-002', reference: 'RE-2024-002',
lines: [ lines: [
{ accountNumber: '1400', debit: 7140, description: 'Forderungen inkl. USt' }, { accountNumber: '10001', debit: 7140, description: 'Forderungen inkl. USt', postingKey: 9 },
{ accountNumber: '4300', credit: 6000, description: 'Erlöse 19% USt' }, { accountNumber: '4300', credit: 6000, description: 'Erlöse 19% USt', postingKey: 40 },
{ accountNumber: '1771', credit: 1140, description: 'Umsatzsteuer 19%' }, { accountNumber: '1771', credit: 1140, description: 'Umsatzsteuer 19%', postingKey: 40 },
], ],
skrType: 'SKR04', skrType: 'SKR04',
}); });
@@ -167,9 +186,9 @@ tap.test('should record Q2-Q4 business transactions for SKR04', async () => {
description: 'Kauf neue Produktionsmaschine', description: 'Kauf neue Produktionsmaschine',
reference: 'INV-2024-001', reference: 'INV-2024-001',
lines: [ lines: [
{ accountNumber: '0500', debit: 25000, description: 'Neue Maschine' }, { accountNumber: '0500', debit: 25000, description: 'Neue Maschine', postingKey: 40 },
{ accountNumber: '1571', debit: 4750, description: 'Vorsteuer 19%' }, { accountNumber: '1571', debit: 4750, description: 'Vorsteuer 19%', postingKey: 9 },
{ accountNumber: '1200', credit: 29750, description: 'Banküberweisung' }, { accountNumber: '1200', credit: 29750, description: 'Banküberweisung', postingKey: 40 },
], ],
skrType: 'SKR04', skrType: 'SKR04',
}); });
@@ -180,9 +199,9 @@ tap.test('should record Q2-Q4 business transactions for SKR04', async () => {
description: 'Großauftrag Kunde ABC', description: 'Großauftrag Kunde ABC',
reference: 'RE-2024-003', reference: 'RE-2024-003',
lines: [ lines: [
{ accountNumber: '1400', debit: 35700, description: 'Forderungen inkl. USt' }, { accountNumber: '10001', debit: 35700, description: 'Forderungen inkl. USt', postingKey: 9 },
{ accountNumber: '4300', credit: 30000, description: 'Erlöse 19% USt' }, { accountNumber: '4300', credit: 30000, description: 'Erlöse 19% USt', postingKey: 40 },
{ accountNumber: '1771', credit: 5700, description: 'Umsatzsteuer 19%' }, { accountNumber: '1771', credit: 5700, description: 'Umsatzsteuer 19%', postingKey: 40 },
], ],
skrType: 'SKR04', skrType: 'SKR04',
}); });
@@ -193,9 +212,9 @@ tap.test('should record Q2-Q4 business transactions for SKR04', async () => {
description: 'Werbekampagne', description: 'Werbekampagne',
reference: 'WK-2024-001', reference: 'WK-2024-001',
lines: [ lines: [
{ accountNumber: '3400', debit: 5000, description: 'Werbekosten' }, { accountNumber: '3400', debit: 5000, description: 'Werbekosten', postingKey: 40 },
{ accountNumber: '1571', debit: 950, description: 'Vorsteuer 19%' }, { accountNumber: '1571', debit: 950, description: 'Vorsteuer 19%', postingKey: 9 },
{ accountNumber: '1600', credit: 5950, description: 'Verbindlichkeiten' }, { accountNumber: '70001', credit: 5950, description: 'Verbindlichkeiten', postingKey: 9 },
], ],
skrType: 'SKR04', skrType: 'SKR04',
}); });
@@ -206,9 +225,9 @@ tap.test('should record Q2-Q4 business transactions for SKR04', async () => {
description: 'Steuerberatung', description: 'Steuerberatung',
reference: 'STB-2024-001', reference: 'STB-2024-001',
lines: [ lines: [
{ accountNumber: '3500', debit: 2500, description: 'Steuerberatungskosten' }, { accountNumber: '3500', debit: 2500, description: 'Steuerberatungskosten', postingKey: 40 },
{ accountNumber: '1571', debit: 475, description: 'Vorsteuer 19%' }, { accountNumber: '1571', debit: 475, description: 'Vorsteuer 19%', postingKey: 9 },
{ accountNumber: '1200', credit: 2975, description: 'Banküberweisung' }, { accountNumber: '1200', credit: 2975, description: 'Banküberweisung', postingKey: 40 },
], ],
skrType: 'SKR04', skrType: 'SKR04',
}); });
@@ -219,9 +238,9 @@ tap.test('should record Q2-Q4 business transactions for SKR04', async () => {
description: 'Jahresbonus Mitarbeiter', description: 'Jahresbonus Mitarbeiter',
reference: 'BON-2024', reference: 'BON-2024',
lines: [ lines: [
{ accountNumber: '2300', debit: 10000, description: 'Tantieme' }, { accountNumber: '2300', debit: 10000, description: 'Tantieme', postingKey: 40 },
{ accountNumber: '2400', debit: 2000, description: 'Gehälter Bonus' }, { accountNumber: '2400', debit: 2000, description: 'Gehälter Bonus', postingKey: 40 },
{ accountNumber: '1200', credit: 12000, description: 'Banküberweisung' }, { accountNumber: '1200', credit: 12000, description: 'Banküberweisung', postingKey: 40 },
], ],
skrType: 'SKR04', skrType: 'SKR04',
}); });
@@ -232,8 +251,8 @@ tap.test('should record Q2-Q4 business transactions for SKR04', async () => {
description: 'Zahlungseingang Großauftrag', description: 'Zahlungseingang Großauftrag',
reference: 'ZE-2024-003', reference: 'ZE-2024-003',
lines: [ lines: [
{ accountNumber: '1200', debit: 35700, description: 'Bankgutschrift' }, { accountNumber: '1200', debit: 35700, description: 'Bankgutschrift', postingKey: 40 },
{ accountNumber: '1400', credit: 35700, description: 'Forderungsausgleich' }, { accountNumber: '10001', credit: 35700, description: 'Forderungsausgleich', postingKey: 3 },
], ],
skrType: 'SKR04', skrType: 'SKR04',
}); });
@@ -246,8 +265,8 @@ tap.test('should perform year-end adjustments (Jahresabschlussbuchungen) for SKR
description: 'Abschreibung Gebäude (linear 2%)', description: 'Abschreibung Gebäude (linear 2%)',
reference: 'AFA-2024-001', reference: 'AFA-2024-001',
lines: [ lines: [
{ accountNumber: '3700', debit: 2400, description: 'AfA auf Gebäude' }, { accountNumber: '3700', debit: 2400, description: 'AfA auf Gebäude', postingKey: 40 },
{ accountNumber: '0210', credit: 2400, description: 'Wertberichtigung Gebäude' }, { accountNumber: '0210', credit: 2400, description: 'Wertberichtigung Gebäude', postingKey: 40 },
], ],
skrType: 'SKR04', skrType: 'SKR04',
}); });
@@ -257,8 +276,8 @@ tap.test('should perform year-end adjustments (Jahresabschlussbuchungen) for SKR
description: 'Abschreibung BGA (linear 10%)', description: 'Abschreibung BGA (linear 10%)',
reference: 'AFA-2024-002', reference: 'AFA-2024-002',
lines: [ lines: [
{ accountNumber: '3700', debit: 6000, description: 'AfA auf BGA' }, // (35000 + 25000) * 10% { accountNumber: '3700', debit: 6000, description: 'AfA auf BGA', postingKey: 40 }, // (35000 + 25000) * 10%
{ accountNumber: '0500', credit: 6000, description: 'Wertberichtigung BGA' }, { accountNumber: '0500', credit: 6000, description: 'Wertberichtigung BGA', postingKey: 40 },
], ],
skrType: 'SKR04', skrType: 'SKR04',
}); });
@@ -268,8 +287,8 @@ tap.test('should perform year-end adjustments (Jahresabschlussbuchungen) for SKR
description: 'Abschreibung Fuhrpark (linear 20%)', description: 'Abschreibung Fuhrpark (linear 20%)',
reference: 'AFA-2024-003', reference: 'AFA-2024-003',
lines: [ lines: [
{ accountNumber: '3700', debit: 1600, description: 'AfA auf Fuhrpark' }, { accountNumber: '3700', debit: 1600, description: 'AfA auf Fuhrpark', postingKey: 40 },
{ accountNumber: '0400', credit: 1600, description: 'Wertberichtigung Fuhrpark' }, { accountNumber: '0400', credit: 1600, description: 'Wertberichtigung Fuhrpark', postingKey: 40 },
], ],
skrType: 'SKR04', skrType: 'SKR04',
}); });
@@ -280,8 +299,8 @@ tap.test('should perform year-end adjustments (Jahresabschlussbuchungen) for SKR
description: 'Aktive Rechnungsabgrenzung - Vorausbezahlte Versicherung', description: 'Aktive Rechnungsabgrenzung - Vorausbezahlte Versicherung',
reference: 'ARA-2024-001', reference: 'ARA-2024-001',
lines: [ lines: [
{ accountNumber: '1900', debit: 1000, description: 'Aktive Rechnungsabgrenzung' }, { accountNumber: '1900', debit: 1000, description: 'Aktive Rechnungsabgrenzung', postingKey: 40 },
{ accountNumber: '3200', credit: 1000, description: 'Versicherungen' }, { accountNumber: '3200', credit: 1000, description: 'Versicherungen', postingKey: 40 },
], ],
skrType: 'SKR04', skrType: 'SKR04',
}); });
@@ -292,8 +311,8 @@ tap.test('should perform year-end adjustments (Jahresabschlussbuchungen) for SKR
description: 'Rückstellung für Jahresabschlusskosten', description: 'Rückstellung für Jahresabschlusskosten',
reference: 'RS-2024-001', reference: 'RS-2024-001',
lines: [ lines: [
{ accountNumber: '3500', debit: 3000, description: 'Rechts- und Beratungskosten' }, { accountNumber: '3500', debit: 3000, description: 'Rechts- und Beratungskosten', postingKey: 40 },
{ accountNumber: '0800', credit: 3000, description: 'Rückstellungen' }, { accountNumber: '0800', credit: 3000, description: 'Rückstellungen', postingKey: 40 },
], ],
skrType: 'SKR04', skrType: 'SKR04',
}); });
@@ -304,9 +323,9 @@ tap.test('should perform year-end adjustments (Jahresabschlussbuchungen) for SKR
description: 'USt-Abschluss Q4', description: 'USt-Abschluss Q4',
reference: 'UST-2024-Q4', reference: 'UST-2024-Q4',
lines: [ lines: [
{ accountNumber: '1771', debit: 8740, description: 'USt-Saldo' }, // Total collected VAT { accountNumber: '1771', debit: 8740, description: 'USt-Saldo', postingKey: 40 }, // Total collected VAT
{ accountNumber: '1571', credit: 7191.50, description: 'Vorsteuer-Saldo' }, // Total input VAT { accountNumber: '1571', credit: 7191.50, description: 'Vorsteuer-Saldo', postingKey: 40 }, // Total input VAT
{ accountNumber: '1700', credit: 1548.50, description: 'USt-Zahllast' }, { accountNumber: '1700', credit: 1548.50, description: 'USt-Zahllast', postingKey: 40 },
], ],
skrType: 'SKR04', skrType: 'SKR04',
}); });
@@ -360,8 +379,8 @@ tap.test('should perform closing entries (Abschlussbuchungen) for SKR04', async
description: 'Abschluss Ertragskonten', description: 'Abschluss Ertragskonten',
reference: 'AB-2024-001', reference: 'AB-2024-001',
lines: [ lines: [
{ accountNumber: '4300', debit: 46000, description: 'Erlöse abschließen' }, { accountNumber: '4300', debit: 46000, description: 'Erlöse abschließen', postingKey: 40 },
{ accountNumber: '9500', credit: 46000, description: 'GuV-Konto' }, { accountNumber: '9500', credit: 46000, description: 'GuV-Konto', postingKey: 40 },
], ],
skrType: 'SKR04', skrType: 'SKR04',
}); });
@@ -372,17 +391,17 @@ tap.test('should perform closing entries (Abschlussbuchungen) for SKR04', async
description: 'Abschluss Aufwandskonten', description: 'Abschluss Aufwandskonten',
reference: 'AB-2024-002', reference: 'AB-2024-002',
lines: [ lines: [
{ accountNumber: '9500', debit: 48450, description: 'GuV-Konto' }, { accountNumber: '9500', debit: 48450, description: 'GuV-Konto', postingKey: 40 },
{ accountNumber: '3200', debit: 1000, description: 'Versicherung abschließen (credit balance)' }, { accountNumber: '3200', debit: 1000, description: 'Versicherung abschließen (credit balance)', postingKey: 40 },
{ accountNumber: '2100', credit: 5000, description: 'Bezogene Waren abschließen' }, { accountNumber: '2100', credit: 5000, description: 'Bezogene Waren abschließen', postingKey: 40 },
{ accountNumber: '2300', credit: 18000, description: 'Löhne abschließen' }, { accountNumber: '2300', credit: 18000, description: 'Löhne abschließen', postingKey: 40 },
{ accountNumber: '2400', credit: 3600, description: 'Gehälter abschließen' }, { accountNumber: '2400', credit: 3600, description: 'Gehälter abschließen', postingKey: 40 },
{ accountNumber: '3700', credit: 10000, description: 'AfA abschließen' }, { accountNumber: '3700', credit: 10000, description: 'AfA abschließen', postingKey: 40 },
{ accountNumber: '3000', credit: 2000, description: 'Miete abschließen' }, { accountNumber: '3000', credit: 2000, description: 'Miete abschließen', postingKey: 40 },
{ accountNumber: '3300', credit: 150, description: 'Kfz abschließen' }, { accountNumber: '3300', credit: 150, description: 'Kfz abschließen', postingKey: 40 },
{ accountNumber: '3400', credit: 5000, description: 'Werbung abschließen' }, { accountNumber: '3400', credit: 5000, description: 'Werbung abschließen', postingKey: 40 },
{ accountNumber: '3500', credit: 5500, description: 'Beratung abschließen' }, { accountNumber: '3500', credit: 5500, description: 'Beratung abschließen', postingKey: 40 },
{ accountNumber: '3100', credit: 200, description: 'Bürobedarf abschließen' }, { accountNumber: '3100', credit: 200, description: 'Bürobedarf abschließen', postingKey: 40 },
], ],
skrType: 'SKR04', skrType: 'SKR04',
}); });
@@ -395,8 +414,8 @@ tap.test('should perform closing entries (Abschlussbuchungen) for SKR04', async
description: 'Jahresgewinn auf Eigenkapital', description: 'Jahresgewinn auf Eigenkapital',
reference: 'AB-2024-003', reference: 'AB-2024-003',
lines: [ lines: [
{ accountNumber: '9500', debit: guv_result, description: 'GuV-Konto ausgleichen' }, { accountNumber: '9500', debit: guv_result, description: 'GuV-Konto ausgleichen', postingKey: 40 },
{ accountNumber: '9300', credit: guv_result, description: 'Gewinnrücklagen' }, { accountNumber: '9300', credit: guv_result, description: 'Gewinnrücklagen', postingKey: 40 },
], ],
skrType: 'SKR04', skrType: 'SKR04',
}); });
@@ -406,8 +425,8 @@ tap.test('should perform closing entries (Abschlussbuchungen) for SKR04', async
description: 'Jahresverlust auf Eigenkapital', description: 'Jahresverlust auf Eigenkapital',
reference: 'AB-2024-003', reference: 'AB-2024-003',
lines: [ lines: [
{ accountNumber: '9400', debit: Math.abs(guv_result), description: 'Verlustvortrag' }, { accountNumber: '9400', debit: Math.abs(guv_result), description: 'Verlustvortrag', postingKey: 40 },
{ accountNumber: '9500', credit: Math.abs(guv_result), description: 'GuV-Konto ausgleichen' }, { accountNumber: '9500', credit: Math.abs(guv_result), description: 'GuV-Konto ausgleichen', postingKey: 40 },
], ],
skrType: 'SKR04', skrType: 'SKR04',
}); });

View File

@@ -91,9 +91,9 @@ tap.test('should post journal entry in SKR03', async () => {
description: 'Test journal entry', description: 'Test journal entry',
reference: 'JE-001', reference: 'JE-001',
lines: [ lines: [
{ accountNumber: '1000', debit: 500 }, // Cash { accountNumber: '1000', debit: 500, postingKey: 40 }, // Cash
{ accountNumber: '1200', debit: 500 }, // Bank { accountNumber: '1200', debit: 500, postingKey: 40 }, // Bank
{ accountNumber: '4000', credit: 1000 }, // Revenue { accountNumber: '4000', credit: 1000, postingKey: 40 }, // Revenue
], ],
skrType: 'SKR03', skrType: 'SKR03',
}); });

View File

@@ -68,10 +68,19 @@ tap.test('should handle Class 8 as free for use in SKR04', async () => {
}); });
tap.test('should post complex transaction in SKR04', async () => { tap.test('should post complex transaction in SKR04', async () => {
// Create creditor account for supplier
await api.createAccount({
accountNumber: '70001',
accountName: 'Lieferant Test GmbH',
accountClass: 7,
accountType: 'liability',
skrType: 'SKR04',
});
const transaction = await api.postTransaction({ const transaction = await api.postTransaction({
date: new Date(), date: new Date(),
debitAccount: '5400', // Goods with 19% VAT debitAccount: '5400', // Goods with 19% VAT
creditAccount: '1600', // Trade payables creditAccount: '70001', // Creditor account (supplier)
amount: 119, amount: 119,
description: 'Purchase with VAT', description: 'Purchase with VAT',
reference: 'BILL-001', reference: 'BILL-001',

View File

@@ -29,8 +29,8 @@ tap.test('should enforce double-entry bookkeeping rules', async () => {
description: 'Unbalanced entry', description: 'Unbalanced entry',
reference: 'TEST-001', reference: 'TEST-001',
lines: [ lines: [
{ accountNumber: '1000', debit: 100 }, { accountNumber: '1000', debit: 100, postingKey: 40 },
{ accountNumber: '4000', credit: 50 }, // Unbalanced! { accountNumber: '4000', credit: 50, postingKey: 40 }, // Unbalanced!
], ],
skrType: 'SKR03', skrType: 'SKR03',
}); });
@@ -99,10 +99,10 @@ tap.test(
description: 'Complex distribution', description: 'Complex distribution',
reference: 'COMPLEX-001', reference: 'COMPLEX-001',
lines: [ lines: [
{ accountNumber: '5000', debit: 500, description: 'Materials' }, { accountNumber: '5000', debit: 500, description: 'Materials', postingKey: 40 },
{ accountNumber: '6000', debit: 300, description: 'Wages' }, { accountNumber: '6000', debit: 300, description: 'Wages', postingKey: 40 },
{ accountNumber: '7100', debit: 200, description: 'Rent' }, { accountNumber: '7100', debit: 200, description: 'Rent', postingKey: 40 },
{ accountNumber: '1200', credit: 1000, description: 'Bank payment' }, { accountNumber: '1200', credit: 1000, description: 'Bank payment', postingKey: 40 },
], ],
skrType: 'SKR03', skrType: 'SKR03',
}); });
@@ -220,10 +220,19 @@ tap.test('should handle batch transaction posting', async () => {
}); });
tap.test('should handle transaction with VAT', async () => { tap.test('should handle transaction with VAT', async () => {
// Create creditor account for supplier
await api.createAccount({
accountNumber: '70001',
accountName: 'Lieferant Test GmbH',
accountClass: 7,
accountType: 'liability',
skrType: 'SKR03',
});
const transaction = await api.postTransaction({ const transaction = await api.postTransaction({
date: new Date(), date: new Date(),
debitAccount: '5400', // Goods with 19% VAT debitAccount: '5400', // Goods with 19% VAT
creditAccount: '1600', // Trade payables creditAccount: '70001', // Creditor account (supplier)
amount: 119, amount: 119,
description: 'Purchase including VAT', description: 'Purchase including VAT',
skrType: 'SKR03', skrType: 'SKR03',

View File

@@ -56,6 +56,9 @@ export class Account extends SmartDataDbDoc<Account, Account> {
@svDb() @svDb()
public isSystemAccount: boolean; public isSystemAccount: boolean;
@svDb()
public isAutomaticAccount: boolean;
@svDb() @svDb()
public createdAt: Date; public createdAt: Date;
@@ -90,6 +93,7 @@ export class Account extends SmartDataDbDoc<Account, Account> {
this.debitTotal = 0; this.debitTotal = 0;
this.creditTotal = 0; this.creditTotal = 0;
this.isSystemAccount = true; this.isSystemAccount = true;
this.isAutomaticAccount = data.isAutomaticAccount || false;
this.createdAt = new Date(); this.createdAt = new Date();
this.updatedAt = 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( public async updateBalance(
debitAmount: number = 0, debitAmount: number = 0,
creditAmount: number = 0, creditAmount: number = 0,
@@ -209,19 +291,33 @@ export class Account extends SmartDataDbDoc<Account, Account> {
public async beforeSave(): Promise<void> { public async beforeSave(): Promise<void> {
// Validate account number format // 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( 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 // Validate account number is numeric
if (!/^\d{4}$/.test(this.accountNumber)) { if (!/^\d{4,5}$/.test(this.accountNumber)) {
throw new Error( throw new Error(
`Account number must contain only digits: ${this.accountNumber}`, `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 // Validate account class matches first digit
const firstDigit = parseInt(this.accountNumber[0]); const firstDigit = parseInt(this.accountNumber[0]);
if (this.accountClass !== firstDigit) { if (this.accountClass !== firstDigit) {
@@ -234,5 +330,11 @@ export class Account extends SmartDataDbDoc<Account, Account> {
if (this.skrType !== 'SKR03' && this.skrType !== 'SKR04') { if (this.skrType !== 'SKR03' && this.skrType !== 'SKR04') {
throw new Error(`Invalid SKR type: ${this.skrType}`); 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;
}
} }
} }

View File

@@ -2,6 +2,11 @@ import * as plugins from './plugins.js';
import { getDbSync } from './skr.database.js'; import { getDbSync } from './skr.database.js';
import { Account } from './skr.classes.account.js'; import { Account } from './skr.classes.account.js';
import { Transaction } from './skr.classes.transaction.js'; import { Transaction } from './skr.classes.transaction.js';
import {
validatePostingKey,
validatePostingKeyConsistency,
getPostingKeyDescription,
} from './skr.postingkeys.js';
import type { import type {
TSKRType, TSKRType,
IJournalEntry, IJournalEntry,
@@ -212,22 +217,84 @@ export class JournalEntry extends SmartDataDbDoc<JournalEntry, JournalEntry> {
throw new Error('Journal entry must have at least 2 lines'); 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) { 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( const account = await Account.getAccountByNumber(
line.accountNumber, line.accountNumber,
this.skrType, this.skrType,
); );
if (!account) { if (!account) {
throw new Error( validationErrors.push(
`Account ${line.accountNumber} not found for ${this.skrType}`, `Account ${line.accountNumber} not found for ${this.skrType}`,
); );
continue;
} }
if (!account.isActive) { 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 credit: line.debit, // Swap
description: `Reversal: ${line.description || ''}`, description: `Reversal: ${line.description || ''}`,
costCenter: line.costCenter, costCenter: line.costCenter,
postingKey: line.postingKey, // Keep same posting key for reversal
})); }));
const reversalEntry = new JournalEntry({ const reversalEntry = new JournalEntry({

View File

@@ -418,6 +418,7 @@ export class Ledger {
accountNumber: account.accountNumber, accountNumber: account.accountNumber,
debit: Math.abs(balance), debit: Math.abs(balance),
description: `Closing ${account.accountName}`, description: `Closing ${account.accountName}`,
postingKey: 40, // Tax-free - internal closing entry
}); });
totalRevenue += Math.abs(balance); totalRevenue += Math.abs(balance);
} }
@@ -429,6 +430,7 @@ export class Ledger {
accountNumber: closingAccountNumber, accountNumber: closingAccountNumber,
credit: totalRevenue, credit: totalRevenue,
description: 'Revenue closing to P&L', description: 'Revenue closing to P&L',
postingKey: 40, // Tax-free - internal closing entry
}); });
const revenueClosingEntry = await this.postJournalEntry({ const revenueClosingEntry = await this.postJournalEntry({
@@ -458,6 +460,7 @@ export class Ledger {
accountNumber: account.accountNumber, accountNumber: account.accountNumber,
credit: Math.abs(balance), credit: Math.abs(balance),
description: `Closing ${account.accountName}`, description: `Closing ${account.accountName}`,
postingKey: 40, // Tax-free - internal closing entry
}); });
totalExpense += Math.abs(balance); totalExpense += Math.abs(balance);
} }
@@ -469,6 +472,7 @@ export class Ledger {
accountNumber: closingAccountNumber, accountNumber: closingAccountNumber,
debit: totalExpense, debit: totalExpense,
description: 'Expense closing to P&L', description: 'Expense closing to P&L',
postingKey: 40, // Tax-free - internal closing entry
}); });
const expenseClosingEntry = await this.postJournalEntry({ const expenseClosingEntry = await this.postJournalEntry({

View File

@@ -1,6 +1,7 @@
import * as plugins from './plugins.js'; import * as plugins from './plugins.js';
import { JournalEntry } from './skr.classes.journalentry.js'; import { JournalEntry } from './skr.classes.journalentry.js';
import { SKRInvoiceMapper } from './skr.invoice.mapper.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 { TSKRType, IJournalEntry, IJournalEntryLine } from './skr.types.js';
import type { import type {
IInvoice, IInvoice,
@@ -196,14 +197,16 @@ export class InvoiceBookingEngine {
lines.push({ lines.push({
accountNumber, accountNumber,
credit: Math.abs(amount), credit: Math.abs(amount),
description: this.getAccountDescription(accountNumber, group) description: this.getAccountDescription(accountNumber, group),
postingKey: 9 // 19% input VAT for expenses
}); });
} else { } else {
// Regular invoice: debit expense account // Regular invoice: debit expense account
lines.push({ lines.push({
accountNumber, accountNumber,
debit: Math.abs(amount), 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({ lines.push({
accountNumber: controlAccount, accountNumber: controlAccount,
debit: totalAmount, 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 { } else {
// Regular invoice: credit vendor account // Regular invoice: credit vendor account
lines.push({ lines.push({
accountNumber: controlAccount, accountNumber: controlAccount,
credit: totalAmount, 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({ lines.push({
accountNumber, accountNumber,
debit: Math.abs(amount), debit: Math.abs(amount),
description: this.getAccountDescription(accountNumber, group) description: this.getAccountDescription(accountNumber, group),
postingKey: 9 // 19% output VAT for revenue
}); });
} else { } else {
// Regular invoice: credit revenue account // Regular invoice: credit revenue account
lines.push({ lines.push({
accountNumber, accountNumber,
credit: Math.abs(amount), 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({ lines.push({
accountNumber: controlAccount, accountNumber: controlAccount,
credit: totalAmount, 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 { } else {
// Regular invoice: debit customer account // Regular invoice: debit customer account
lines.push({ lines.push({
accountNumber: controlAccount, accountNumber: controlAccount,
debit: totalAmount, 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 amount = Math.abs(vatBreak.taxAmount);
const description = `VAT ${vatBreak.vatCategory.rate}%`; 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') { if (direction === 'input') {
// Input VAT (Vorsteuer) // Input VAT (Vorsteuer)
if (reverseDirection) { if (reverseDirection) {
lines.push({ accountNumber: vatAccount, credit: amount, description }); lines.push({ accountNumber: vatAccount, credit: amount, description, postingKey });
} else { } else {
lines.push({ accountNumber: vatAccount, debit: amount, description }); lines.push({ accountNumber: vatAccount, debit: amount, description, postingKey });
} }
} else { } else {
// Output VAT (Umsatzsteuer) // Output VAT (Umsatzsteuer)
if (reverseDirection) { if (reverseDirection) {
lines.push({ accountNumber: vatAccount, debit: amount, description }); lines.push({ accountNumber: vatAccount, debit: amount, description, postingKey });
} else { } 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, accountNumber: inputVATAccount,
debit: amount, 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, accountNumber: outputVATAccount,
credit: amount, 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,12 +476,14 @@ export class InvoiceBookingEngine {
{ {
accountNumber: controlAccount, accountNumber: controlAccount,
debit: fullAmount, 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) accountNumber: '1000', // Bank account (would be configurable)
credit: paymentAmount, credit: paymentAmount,
description: `Bank payment ${payment.endToEndId || payment.paymentId}` description: `Bank payment ${payment.endToEndId || payment.paymentId}`,
postingKey: 40 // Tax-free for bank account
} }
); );
@@ -477,7 +493,8 @@ export class InvoiceBookingEngine {
lines.push({ lines.push({
accountNumber: skontoAccounts.skontoAccount, accountNumber: skontoAccounts.skontoAccount,
credit: skontoAmount, credit: skontoAmount,
description: `Skonto received` description: `Skonto received`,
postingKey: 40 // Tax-free for skonto
}); });
// VAT correction for skonto // VAT correction for skonto
@@ -488,7 +505,8 @@ export class InvoiceBookingEngine {
{ {
accountNumber: skontoAccounts.vatCorrectionAccount, accountNumber: skontoAccounts.vatCorrectionAccount,
credit: vatCorrection, credit: vatCorrection,
description: `Skonto VAT correction` description: `Skonto VAT correction`,
postingKey: 40 // Tax-free for correction
} }
); );
} }
@@ -499,12 +517,14 @@ export class InvoiceBookingEngine {
{ {
accountNumber: '1000', // Bank account accountNumber: '1000', // Bank account
debit: paymentAmount, debit: paymentAmount,
description: `Payment from ${invoice.customer.name}` description: `Payment from ${invoice.customer.name}`,
postingKey: 40 // Tax-free for bank account
}, },
{ {
accountNumber: controlAccount, accountNumber: controlAccount,
credit: fullAmount, credit: fullAmount,
description: `Customer payment ${payment.endToEndId || payment.paymentId}` description: `Customer payment ${payment.endToEndId || payment.paymentId}`,
postingKey: 3 // Payment with VAT
} }
); );
@@ -514,7 +534,8 @@ export class InvoiceBookingEngine {
lines.push({ lines.push({
accountNumber: skontoAccounts.skontoAccount, accountNumber: skontoAccounts.skontoAccount,
debit: skontoAmount, debit: skontoAmount,
description: `Skonto granted` description: `Skonto granted`,
postingKey: 40 // Tax-free for skonto
}); });
// VAT correction for skonto // VAT correction for skonto
@@ -525,7 +546,8 @@ export class InvoiceBookingEngine {
{ {
accountNumber: skontoAccounts.vatCorrectionAccount, accountNumber: skontoAccounts.vatCorrectionAccount,
debit: vatCorrection, debit: vatCorrection,
description: `Skonto VAT correction` description: `Skonto VAT correction`,
postingKey: 40 // Tax-free for correction
} }
); );
} }

245
ts/skr.postingkeys.ts Normal file
View File

@@ -0,0 +1,245 @@
/**
* DATEV Posting Keys (Buchungsschlüssel) for German Accounting
*
* Posting keys control automatic VAT booking and are automatically checked
* in German tax audits (Betriebsprüfungen). Using incorrect posting keys
* can have serious tax consequences.
*
* Reference: DATEV Buchungsschlüssel-Verzeichnis
*/
import type { TPostingKey, IPostingKeyRule } from './skr.types.js';
/**
* Posting key definitions with validation rules
*/
export const POSTING_KEY_RULES: Record<TPostingKey, IPostingKeyRule> = {
3: {
key: 3,
description: 'Zahlungseingang mit 19% Umsatzsteuer',
vatRate: 19,
requiresVAT: true,
disablesVATAutomatism: false,
allowedScenarios: ['domestic_taxed']
},
8: {
key: 8,
description: '7% Vorsteuer',
vatRate: 7,
requiresVAT: true,
disablesVATAutomatism: false,
allowedScenarios: ['domestic_taxed']
},
9: {
key: 9,
description: '19% Vorsteuer',
vatRate: 19,
requiresVAT: true,
disablesVATAutomatism: false,
allowedScenarios: ['domestic_taxed']
},
19: {
key: 19,
description: '19% Vorsteuer bei innergemeinschaftlichen Lieferungen',
vatRate: 19,
requiresVAT: true,
disablesVATAutomatism: false,
allowedScenarios: ['intra_eu']
},
40: {
key: 40,
description: 'Steuerfrei / Aufhebung der Automatik',
vatRate: 0,
requiresVAT: false,
disablesVATAutomatism: true,
allowedScenarios: ['tax_free', 'export', 'reverse_charge']
},
94: {
key: 94,
description: '19% Vorsteuer/Umsatzsteuer bei Erwerb aus EU oder Drittland (Reverse Charge)',
vatRate: 19,
requiresVAT: true,
disablesVATAutomatism: false,
allowedScenarios: ['reverse_charge', 'intra_eu', 'third_country']
}
};
/**
* Validate posting key for a journal entry line
*/
export function validatePostingKey(
postingKey: TPostingKey,
accountNumber: string,
amount: number,
vatAmount?: number,
taxScenario?: string
): { isValid: boolean; errors: string[]; warnings: string[] } {
const errors: string[] = [];
const warnings: string[] = [];
// Get posting key rule
const rule = POSTING_KEY_RULES[postingKey];
if (!rule) {
errors.push(`Invalid posting key: ${postingKey}`);
return { isValid: false, errors, warnings };
}
// Validate VAT requirement
if (rule.requiresVAT && !vatAmount) {
errors.push(
`Posting key ${postingKey} requires VAT amount, but none provided. ` +
`Description: ${rule.description}`
);
}
// Validate VAT rate if specified
if (rule.vatRate && vatAmount && rule.vatRate > 0) {
const expectedVAT = Math.round(amount * rule.vatRate) / 100;
const tolerance = 0.02; // 2 cent tolerance for rounding
if (Math.abs(vatAmount - expectedVAT) > tolerance) {
warnings.push(
`VAT amount ${vatAmount} does not match expected ${expectedVAT.toFixed(2)} ` +
`for posting key ${postingKey} (${rule.vatRate}%)`
);
}
}
// Validate tax scenario
if (rule.allowedScenarios && taxScenario) {
if (!rule.allowedScenarios.includes(taxScenario)) {
errors.push(
`Posting key ${postingKey} is not valid for tax scenario '${taxScenario}'. ` +
`Allowed scenarios: ${rule.allowedScenarios.join(', ')}`
);
}
}
// Validate automatism disabling
if (rule.disablesVATAutomatism && vatAmount && vatAmount > 0) {
warnings.push(
`Posting key ${postingKey} disables VAT automatism but VAT amount is provided. ` +
`This may cause incorrect tax reporting.`
);
}
return {
isValid: errors.length === 0,
errors,
warnings
};
}
/**
* Get posting key description
*/
export function getPostingKeyDescription(postingKey: TPostingKey): string {
const rule = POSTING_KEY_RULES[postingKey];
return rule ? rule.description : `Unknown posting key: ${postingKey}`;
}
/**
* Get appropriate posting key for a transaction
*/
export function suggestPostingKey(params: {
vatRate: number;
taxScenario?: string;
isPayment?: boolean;
}): TPostingKey {
const { vatRate, taxScenario, isPayment } = params;
// Tax-free or reverse charge scenarios
if (taxScenario === 'tax_free' || taxScenario === 'export') {
return 40;
}
// Reverse charge
if (taxScenario === 'reverse_charge' || taxScenario === 'third_country') {
return 94;
}
// Intra-EU with VAT
if (taxScenario === 'intra_eu' && vatRate === 19) {
return 19;
}
// Payment with 19% VAT
if (isPayment && vatRate === 19) {
return 3;
}
// Input VAT based on rate
if (vatRate === 19) {
return 9;
}
if (vatRate === 7) {
return 8;
}
// Default to tax-free if no VAT
if (vatRate === 0) {
return 40;
}
// Fallback to 19% input VAT
return 9;
}
/**
* Validate all posting keys for consistency
*/
export function validatePostingKeyConsistency(lines: Array<{
postingKey: TPostingKey;
accountNumber: string;
debit?: number;
credit?: number;
vatAmount?: number;
}>): { isValid: boolean; errors: string[]; warnings: string[] } {
const errors: string[] = [];
const warnings: string[] = [];
// Check for mixing tax-free and taxed transactions
const hasTaxFree = lines.some(line => line.postingKey === 40);
const hasTaxed = lines.some(line => [3, 8, 9, 19, 94].includes(line.postingKey));
if (hasTaxFree && hasTaxed) {
warnings.push(
'Journal entry mixes tax-free (key 40) and taxed transactions. ' +
'Verify this is intentional.'
);
}
// Check for reverse charge consistency
const hasReverseCharge = lines.some(line => line.postingKey === 94);
if (hasReverseCharge) {
const reverseChargeLines = lines.filter(line => line.postingKey === 94);
if (reverseChargeLines.length % 2 !== 0) {
errors.push(
'Reverse charge (posting key 94) requires both input and output VAT entries. ' +
'Found odd number of reverse charge lines.'
);
}
}
return {
isValid: errors.length === 0,
errors,
warnings
};
}
/**
* Check if posting key requires automatic VAT booking
*/
export function requiresAutomaticVAT(postingKey: TPostingKey): boolean {
const rule = POSTING_KEY_RULES[postingKey];
return rule ? !rule.disablesVATAutomatism : false;
}
/**
* Get all valid posting keys
*/
export function getAllPostingKeys(): TPostingKey[] {
return Object.keys(POSTING_KEY_RULES).map(k => Number(k) as TPostingKey);
}

View File

@@ -9,6 +9,18 @@ export type TSKRType = 'SKR03' | 'SKR04';
export type TTransactionStatus = 'pending' | 'posted' | 'reversed'; export type TTransactionStatus = 'pending' | 'posted' | 'reversed';
/**
* DATEV posting keys (Buchungsschlüssel) for German accounting
* These keys control automatic VAT booking and are checked in tax audits
*/
export type TPostingKey =
| 3 // Payment with 19% VAT
| 8 // 7% input VAT
| 9 // 19% input VAT
| 19 // 19% input VAT (intra-EU)
| 40 // Tax-free (disables VAT automatism)
| 94; // 19% input/output VAT (reverse charge)
export type TReportType = export type TReportType =
| 'trial_balance' | 'trial_balance'
| 'income_statement' | 'income_statement'
@@ -16,6 +28,18 @@ export type TReportType =
| 'general_ledger' | 'general_ledger'
| 'cash_flow'; | 'cash_flow';
/**
* Posting key validation rule
*/
export interface IPostingKeyRule {
key: TPostingKey;
description: string;
vatRate?: number; // Expected VAT rate (if applicable)
requiresVAT: boolean; // Whether VAT entry is required
disablesVATAutomatism: boolean; // Whether this key disables automatic VAT
allowedScenarios?: string[]; // Allowed tax scenarios (e.g., 'reverse_charge')
}
export interface IAccountData { export interface IAccountData {
accountNumber: string; accountNumber: string;
accountName: string; accountName: string;
@@ -25,6 +49,7 @@ export interface IAccountData {
description?: string; description?: string;
vatRate?: number; vatRate?: number;
isActive?: boolean; isActive?: boolean;
isAutomaticAccount?: boolean; // Automatikkonto (e.g., 1400, 1600) - cannot be posted to directly
} }
export interface ITransactionData { export interface ITransactionData {
@@ -53,6 +78,7 @@ export interface IJournalEntryLine {
credit?: number; credit?: number;
description?: string; description?: string;
costCenter?: string; costCenter?: string;
postingKey: TPostingKey; // REQUIRED: DATEV posting key for VAT automation control
} }
export interface ITrialBalanceEntry { export interface ITrialBalanceEntry {

View File

@@ -159,6 +159,7 @@ export const SKR03_ACCOUNTS: IAccountData[] = [
accountType: 'asset', accountType: 'asset',
skrType: 'SKR03', skrType: 'SKR03',
description: 'Trade receivables', description: 'Trade receivables',
isAutomaticAccount: true, // Automatikkonto - cannot be posted to directly, use debtor accounts (10000-69999)
}, },
{ {
accountNumber: '1500', accountNumber: '1500',
@@ -199,6 +200,7 @@ export const SKR03_ACCOUNTS: IAccountData[] = [
accountType: 'liability', accountType: 'liability',
skrType: 'SKR03', skrType: 'SKR03',
description: 'Trade payables', description: 'Trade payables',
isAutomaticAccount: true, // Automatikkonto - cannot be posted to directly, use creditor accounts (70000-99999)
}, },
{ {
accountNumber: '1700', accountNumber: '1700',

View File

@@ -159,6 +159,7 @@ export const SKR04_ACCOUNTS: IAccountData[] = [
accountType: 'asset', accountType: 'asset',
skrType: 'SKR04', skrType: 'SKR04',
description: 'Trade receivables', description: 'Trade receivables',
isAutomaticAccount: true, // Automatikkonto - cannot be posted to directly, use debtor accounts (10000-69999)
}, },
{ {
accountNumber: '1500', accountNumber: '1500',
@@ -199,6 +200,7 @@ export const SKR04_ACCOUNTS: IAccountData[] = [
accountType: 'liability', accountType: 'liability',
skrType: 'SKR04', skrType: 'SKR04',
description: 'Trade payables', description: 'Trade payables',
isAutomaticAccount: true, // Automatikkonto - cannot be posted to directly, use creditor accounts (70000-99999)
}, },
{ {
accountNumber: '1700', accountNumber: '1700',