From 4f1066da2e4a03190f56e68cb40d5a02e982e5f3 Mon Sep 17 00:00:00 2001 From: Juergen Kunz Date: Mon, 27 Oct 2025 08:34:28 +0000 Subject: [PATCH] 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. --- test/test.jahresabschluss.skr03.ts | 225 ++++++++++++++------------ test/test.jahresabschluss.skr04.ts | 185 ++++++++++++---------- test/test.skr03.ts | 6 +- test/test.skr04.ts | 11 +- test/test.transactions.ts | 23 ++- ts/skr.classes.account.ts | 108 ++++++++++++- ts/skr.classes.journalentry.ts | 74 ++++++++- ts/skr.classes.ledger.ts | 4 + ts/skr.invoice.booking.ts | 76 +++++---- ts/skr.postingkeys.ts | 245 +++++++++++++++++++++++++++++ ts/skr.types.ts | 26 +++ ts/skr03.data.ts | 2 + ts/skr04.data.ts | 2 + 13 files changed, 758 insertions(+), 229 deletions(-) create mode 100644 ts/skr.postingkeys.ts diff --git a/test/test.jahresabschluss.skr03.ts b/test/test.jahresabschluss.skr03.ts index 281b7b9..b8f4129 100644 --- a/test/test.jahresabschluss.skr03.ts +++ b/test/test.jahresabschluss.skr03.ts @@ -7,7 +7,7 @@ let testConfig: Awaited>; tap.test('should demonstrate complete Jahresabschluss (Annual Financial Statement) for SKR03', async () => { testConfig = await getTestConfig(); - + // Use timestamp to ensure unique database for each test run const timestamp = Date.now(); api = new skr.SkrApi({ @@ -17,13 +17,34 @@ tap.test('should demonstrate complete Jahresabschluss (Annual Financial Statemen await api.initialize('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 () => { // Opening balances from previous year's closing // This represents a small GmbH (limited liability company) // 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) const openingEntry = await api.postJournalEntry({ date: new Date('2024-01-01'), @@ -31,20 +52,20 @@ tap.test('should set up opening balances (Eröffnungsbilanz)', async () => { reference: 'EB-2024', lines: [ // Debit all asset accounts - { accountNumber: '0200', debit: 45000, description: 'Grundstücke' }, - { accountNumber: '0210', debit: 120000, description: 'Gebäude' }, - { accountNumber: '0500', debit: 35000, description: 'Betriebs- und Geschäftsausstattung' }, - { accountNumber: '0400', debit: 8000, description: 'Fuhrpark' }, - { accountNumber: '1200', debit: 25000, description: 'Bank' }, - { accountNumber: '1000', debit: 2500, description: 'Kasse' }, - { accountNumber: '1400', debit: 18000, description: 'Forderungen' }, - { accountNumber: '3100', debit: 12000, description: 'Warenvorräte' }, - + { accountNumber: '0200', debit: 45000, description: 'Grundstücke', postingKey: 40 }, + { accountNumber: '0210', debit: 120000, description: 'Gebäude', postingKey: 40 }, + { accountNumber: '0500', debit: 35000, description: 'Betriebs- und Geschäftsausstattung', postingKey: 40 }, + { accountNumber: '0400', debit: 8000, description: 'Fuhrpark', postingKey: 40 }, + { accountNumber: '1200', debit: 25000, description: 'Bank', postingKey: 40 }, + { accountNumber: '1000', debit: 2500, description: 'Kasse', postingKey: 40 }, + { accountNumber: '10001', debit: 18000, description: 'Forderungen Kunde', postingKey: 40 }, + { accountNumber: '3100', debit: 12000, description: 'Warenvorräte', postingKey: 40 }, + // Credit all liability and equity accounts - { accountNumber: '2000', credit: 150000, description: 'Eigenkapital' }, - { accountNumber: '2900', credit: 35000, description: 'Gewinnrücklagen' }, - { accountNumber: '1600', credit: 52500, description: 'Verbindlichkeiten L+L' }, - { accountNumber: '3300', credit: 28000, description: 'Verbindlichkeiten Kreditinstitute' }, + { accountNumber: '2000', credit: 150000, description: 'Eigenkapital', postingKey: 40 }, + { accountNumber: '2900', credit: 35000, description: 'Gewinnrücklagen', postingKey: 40 }, + { accountNumber: '70001', credit: 52500, description: 'Verbindlichkeiten Lieferant', postingKey: 40 }, + { accountNumber: '3300', credit: 28000, description: 'Verbindlichkeiten Kreditinstitute', postingKey: 40 }, ], skrType: 'SKR03', }); @@ -57,28 +78,28 @@ tap.test('should set up opening balances (Eröffnungsbilanz)', async () => { tap.test('should record Q1 business transactions', async () => { // 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({ date: new Date('2024-01-15'), description: 'Verkauf Waren auf Rechnung', reference: 'RE-2024-001', lines: [ - { accountNumber: '1400', debit: 11900, description: 'Forderungen inkl. USt' }, - { accountNumber: '8400', credit: 10000, description: 'Erlöse 19% USt' }, - { accountNumber: '1771', credit: 1900, description: 'Umsatzsteuer 19%' }, + { accountNumber: '10001', debit: 11900, description: 'Forderungen inkl. USt', postingKey: 9 }, + { accountNumber: '8400', credit: 10000, description: 'Erlöse 19% USt', postingKey: 40 }, + { accountNumber: '1771', credit: 1900, description: 'Umsatzsteuer 19%', postingKey: 40 }, ], 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({ date: new Date('2024-01-20'), description: 'Einkauf Material auf Rechnung', reference: 'ER-2024-001', lines: [ - { accountNumber: '5400', debit: 5000, description: 'Wareneingang 19% Vorsteuer' }, - { accountNumber: '1571', debit: 950, description: 'Vorsteuer 19%' }, - { accountNumber: '1600', credit: 5950, description: 'Verbindlichkeiten' }, + { accountNumber: '5400', debit: 5000, description: 'Wareneingang 19% Vorsteuer', postingKey: 40 }, + { accountNumber: '1571', debit: 950, description: 'Vorsteuer 19%', postingKey: 9 }, + { accountNumber: '70001', credit: 5950, description: 'Verbindlichkeiten', postingKey: 9 }, ], skrType: 'SKR03', }); @@ -89,21 +110,21 @@ tap.test('should record Q1 business transactions', async () => { description: 'Gehaltszahlung Januar', reference: 'GH-2024-01', lines: [ - { accountNumber: '6000', debit: 8000, description: 'Löhne und Gehälter' }, - { accountNumber: '6100', debit: 1600, description: 'Sozialversicherung AG-Anteil' }, - { accountNumber: '1200', credit: 9600, description: 'Banküberweisung' }, + { accountNumber: '6000', debit: 8000, description: 'Löhne und Gehälter', postingKey: 40 }, + { accountNumber: '6100', debit: 1600, description: 'Sozialversicherung AG-Anteil', postingKey: 40 }, + { accountNumber: '1200', credit: 9600, description: 'Banküberweisung', postingKey: 40 }, ], skrType: 'SKR03', }); - // Customer payment received + // Customer payment received - using debtor account 10001 instead of automatic 1400 await api.postJournalEntry({ date: new Date('2024-02-10'), description: 'Zahlungseingang Kunde', reference: 'ZE-2024-001', lines: [ - { accountNumber: '1200', debit: 11900, description: 'Bankgutschrift' }, - { accountNumber: '1400', credit: 11900, description: 'Forderungsausgleich' }, + { accountNumber: '1200', debit: 11900, description: 'Bankgutschrift', postingKey: 40 }, + { accountNumber: '10001', credit: 11900, description: 'Forderungsausgleich', postingKey: 3 }, ], skrType: 'SKR03', }); @@ -114,8 +135,8 @@ tap.test('should record Q1 business transactions', async () => { description: 'Miete Februar', reference: 'MI-2024-02', lines: [ - { accountNumber: '7100', debit: 2000, description: 'Miete' }, - { accountNumber: '1200', credit: 2000, description: 'Banküberweisung' }, + { accountNumber: '7100', debit: 2000, description: 'Miete', postingKey: 40 }, + { accountNumber: '1200', credit: 2000, description: 'Banküberweisung', postingKey: 40 }, ], skrType: 'SKR03', }); @@ -126,9 +147,9 @@ tap.test('should record Q1 business transactions', async () => { description: 'Büromaterial', reference: 'BM-2024-001', lines: [ - { accountNumber: '6800', debit: 200, description: 'Bürobedarf' }, - { accountNumber: '1571', debit: 38, description: 'Vorsteuer 19%' }, - { accountNumber: '1200', credit: 238, description: 'Bankzahlung' }, + { accountNumber: '6800', debit: 200, description: 'Bürobedarf', postingKey: 40 }, + { accountNumber: '1571', debit: 38, description: 'Vorsteuer 19%', postingKey: 9 }, + { accountNumber: '1200', credit: 238, description: 'Bankzahlung', postingKey: 40 }, ], skrType: 'SKR03', }); @@ -139,22 +160,22 @@ tap.test('should record Q1 business transactions', async () => { description: 'Tankrechnung Firmenfahrzeug', reference: 'KFZ-2024-001', lines: [ - { accountNumber: '7400', debit: 150, description: 'Kfz-Kosten' }, - { accountNumber: '1571', debit: 28.50, description: 'Vorsteuer 19%' }, - { accountNumber: '1200', credit: 178.50, description: 'Bankzahlung' }, + { accountNumber: '7400', debit: 150, description: 'Kfz-Kosten', postingKey: 40 }, + { accountNumber: '1571', debit: 28.50, description: 'Vorsteuer 19%', postingKey: 9 }, + { accountNumber: '1200', credit: 178.50, description: 'Bankzahlung', postingKey: 40 }, ], skrType: 'SKR03', }); - // Another sale + // Another sale - using debtor account 10001 instead of automatic 1400 await api.postJournalEntry({ date: new Date('2024-03-20'), description: 'Verkauf Dienstleistung', reference: 'RE-2024-002', lines: [ - { accountNumber: '1400', debit: 7140, description: 'Forderungen inkl. USt' }, - { accountNumber: '8400', credit: 6000, description: 'Erlöse 19% USt' }, - { accountNumber: '1771', credit: 1140, description: 'Umsatzsteuer 19%' }, + { accountNumber: '10001', debit: 7140, description: 'Forderungen inkl. USt', postingKey: 9 }, + { accountNumber: '8400', credit: 6000, description: 'Erlöse 19% USt', postingKey: 40 }, + { accountNumber: '1771', credit: 1140, description: 'Umsatzsteuer 19%', postingKey: 40 }, ], skrType: 'SKR03', }); @@ -169,35 +190,35 @@ tap.test('should record Q2-Q4 business transactions', async () => { description: 'Kauf neue Produktionsmaschine', reference: 'INV-2024-001', lines: [ - { accountNumber: '0500', debit: 25000, description: 'Neue Maschine' }, - { accountNumber: '1571', debit: 4750, description: 'Vorsteuer 19%' }, - { accountNumber: '1200', credit: 29750, description: 'Banküberweisung' }, + { accountNumber: '0500', debit: 25000, description: 'Neue Maschine', postingKey: 40 }, + { accountNumber: '1571', debit: 4750, description: 'Vorsteuer 19%', postingKey: 9 }, + { accountNumber: '1200', credit: 29750, description: 'Banküberweisung', postingKey: 40 }, ], skrType: 'SKR03', }); - // Q2: Large sale + // Q2: Large sale - using debtor account 10001 await api.postJournalEntry({ date: new Date('2024-05-10'), description: 'Großauftrag Kunde ABC', reference: 'RE-2024-003', lines: [ - { accountNumber: '1400', debit: 35700, description: 'Forderungen inkl. USt' }, - { accountNumber: '8400', credit: 30000, description: 'Erlöse 19% USt' }, - { accountNumber: '1771', credit: 5700, description: 'Umsatzsteuer 19%' }, + { accountNumber: '10001', debit: 35700, description: 'Forderungen inkl. USt', postingKey: 9 }, + { accountNumber: '8400', credit: 30000, description: 'Erlöse 19% USt', postingKey: 40 }, + { accountNumber: '1771', credit: 5700, description: 'Umsatzsteuer 19%', postingKey: 40 }, ], skrType: 'SKR03', }); - // Q3: Marketing expenses + // Q3: Marketing expenses - using creditor account 70001 await api.postJournalEntry({ date: new Date('2024-07-10'), description: 'Werbekampagne', reference: 'WK-2024-001', lines: [ - { accountNumber: '6600', debit: 5000, description: 'Werbekosten' }, - { accountNumber: '1571', debit: 950, description: 'Vorsteuer 19%' }, - { accountNumber: '1600', credit: 5950, description: 'Verbindlichkeiten' }, + { accountNumber: '6600', debit: 5000, description: 'Werbekosten', postingKey: 40 }, + { accountNumber: '1571', debit: 950, description: 'Vorsteuer 19%', postingKey: 9 }, + { accountNumber: '70001', credit: 5950, description: 'Verbindlichkeiten', postingKey: 9 }, ], skrType: 'SKR03', }); @@ -208,9 +229,9 @@ tap.test('should record Q2-Q4 business transactions', async () => { description: 'Steuerberatung', reference: 'STB-2024-001', lines: [ - { accountNumber: '6700', debit: 2500, description: 'Steuerberatungskosten' }, - { accountNumber: '1571', debit: 475, description: 'Vorsteuer 19%' }, - { accountNumber: '1200', credit: 2975, description: 'Banküberweisung' }, + { accountNumber: '6700', debit: 2500, description: 'Steuerberatungskosten', postingKey: 40 }, + { accountNumber: '1571', debit: 475, description: 'Vorsteuer 19%', postingKey: 9 }, + { accountNumber: '1200', credit: 2975, description: 'Banküberweisung', postingKey: 40 }, ], skrType: 'SKR03', }); @@ -221,35 +242,35 @@ tap.test('should record Q2-Q4 business transactions', async () => { description: 'Jahresbonus Mitarbeiter', reference: 'BON-2024', lines: [ - { accountNumber: '6000', debit: 10000, description: 'Tantieme' }, - { accountNumber: '6100', debit: 2000, description: 'Sozialversicherung AG-Anteil' }, - { accountNumber: '1200', credit: 12000, description: 'Banküberweisung' }, + { accountNumber: '6000', debit: 10000, description: 'Tantieme', postingKey: 40 }, + { accountNumber: '6100', debit: 2000, description: 'Sozialversicherung AG-Anteil', postingKey: 40 }, + { accountNumber: '1200', credit: 12000, description: 'Banküberweisung', postingKey: 40 }, ], skrType: 'SKR03', }); - // Q4: Collection of outstanding receivables + // Q4: Collection of outstanding receivables - using debtor account 10001 await api.postJournalEntry({ date: new Date('2024-12-15'), description: 'Zahlungseingang Großauftrag', reference: 'ZE-2024-003', lines: [ - { accountNumber: '1200', debit: 35700, description: 'Bankgutschrift' }, - { accountNumber: '1400', credit: 35700, description: 'Forderungsausgleich' }, + { accountNumber: '1200', debit: 35700, description: 'Bankgutschrift', postingKey: 40 }, + { accountNumber: '10001', credit: 35700, description: 'Forderungsausgleich', postingKey: 3 }, ], skrType: 'SKR03', }); }); tap.test('should perform year-end adjustments (Jahresabschlussbuchungen)', async () => { - // 1. Depreciation (Abschreibungen) + // 1. Depreciation (Abschreibungen) - internal adjustments use posting key 40 await api.postJournalEntry({ date: new Date('2024-12-31'), description: 'Abschreibung Gebäude (linear 2%)', reference: 'AFA-2024-001', lines: [ - { accountNumber: '7000', debit: 2400, description: 'AfA auf Gebäude' }, - { accountNumber: '0210', credit: 2400, description: 'Wertberichtigung Gebäude' }, + { accountNumber: '7000', debit: 2400, description: 'AfA auf Gebäude', postingKey: 40 }, + { accountNumber: '0210', credit: 2400, description: 'Wertberichtigung Gebäude', postingKey: 40 }, ], skrType: 'SKR03', }); @@ -259,8 +280,8 @@ tap.test('should perform year-end adjustments (Jahresabschlussbuchungen)', async description: 'Abschreibung BGA (linear 10%)', reference: 'AFA-2024-002', lines: [ - { accountNumber: '7000', debit: 6000, description: 'AfA auf BGA' }, // (35000 + 25000) * 10% - { accountNumber: '0500', credit: 6000, description: 'Wertberichtigung BGA' }, + { accountNumber: '7000', debit: 6000, description: 'AfA auf BGA', postingKey: 40 }, // (35000 + 25000) * 10% + { accountNumber: '0500', credit: 6000, description: 'Wertberichtigung BGA', postingKey: 40 }, ], skrType: 'SKR03', }); @@ -270,57 +291,57 @@ tap.test('should perform year-end adjustments (Jahresabschlussbuchungen)', async description: 'Abschreibung Fuhrpark (linear 20%)', reference: 'AFA-2024-003', lines: [ - { accountNumber: '7000', debit: 1600, description: 'AfA auf Fuhrpark' }, - { accountNumber: '0400', credit: 1600, description: 'Wertberichtigung Fuhrpark' }, + { accountNumber: '7000', debit: 1600, description: 'AfA auf Fuhrpark', postingKey: 40 }, + { accountNumber: '0400', credit: 1600, description: 'Wertberichtigung Fuhrpark', postingKey: 40 }, ], skrType: 'SKR03', }); - // 2. Accruals (Rechnungsabgrenzung) + // 2. Accruals (Rechnungsabgrenzung) - internal adjustments use posting key 40 await api.postJournalEntry({ date: new Date('2024-12-31'), description: 'Aktive Rechnungsabgrenzung - Vorausbezahlte Versicherung', reference: 'ARA-2024-001', lines: [ - { accountNumber: '1900', debit: 1000, description: 'Aktive Rechnungsabgrenzung' }, - { accountNumber: '7300', credit: 1000, description: 'Versicherungen' }, + { accountNumber: '1900', debit: 1000, description: 'Aktive Rechnungsabgrenzung', postingKey: 40 }, + { accountNumber: '7300', credit: 1000, description: 'Versicherungen', postingKey: 40 }, ], skrType: 'SKR03', }); - // 3. Provisions (Rückstellungen) + // 3. Provisions (Rückstellungen) - internal adjustments use posting key 40 await api.postJournalEntry({ date: new Date('2024-12-31'), description: 'Rückstellung für Jahresabschlusskosten', reference: 'RS-2024-001', lines: [ - { accountNumber: '6700', debit: 3000, description: 'Rechts- und Beratungskosten' }, - { accountNumber: '3000', credit: 3000, description: 'Rückstellungen' }, + { accountNumber: '6700', debit: 3000, description: 'Rechts- und Beratungskosten', postingKey: 40 }, + { accountNumber: '3000', credit: 3000, description: 'Rückstellungen', postingKey: 40 }, ], skrType: 'SKR03', }); - // 4. Inventory adjustment + // 4. Inventory adjustment - internal adjustments use posting key 40 await api.postJournalEntry({ date: new Date('2024-12-31'), description: 'Bestandsveränderung Waren', reference: 'BV-2024-001', lines: [ - { accountNumber: '3100', debit: 3000, description: 'Warenbestand Zugang' }, - { accountNumber: '5900', credit: 3000, description: 'Bestandsveränderungen' }, + { accountNumber: '3100', debit: 3000, description: 'Warenbestand Zugang', postingKey: 40 }, + { accountNumber: '5900', credit: 3000, description: 'Bestandsveränderungen', postingKey: 40 }, ], skrType: 'SKR03', }); - // 5. VAT clearing (Umsatzsteuer-Vorauszahlung) + // 5. VAT clearing (Umsatzsteuer-Vorauszahlung) - internal adjustments use posting key 40 await api.postJournalEntry({ date: new Date('2024-12-31'), description: 'USt-Abschluss Q4', reference: 'UST-2024-Q4', lines: [ - { accountNumber: '1771', debit: 8740, description: 'USt-Saldo' }, // Total collected VAT - { accountNumber: '1571', credit: 7191.50, description: 'Vorsteuer-Saldo' }, // Total input VAT - { accountNumber: '1800', credit: 1548.50, description: 'USt-Zahllast' }, + { accountNumber: '1771', debit: 8740, description: 'USt-Saldo', postingKey: 40 }, // Total collected VAT + { accountNumber: '1571', credit: 7191.50, description: 'Vorsteuer-Saldo', postingKey: 40 }, // Total input VAT + { accountNumber: '1800', credit: 1548.50, description: 'USt-Zahllast', postingKey: 40 }, ], skrType: 'SKR03', }); @@ -366,41 +387,41 @@ tap.test('should calculate income statement (GuV) before closing', async () => { tap.test('should perform closing entries (Abschlussbuchungen)', async () => { // 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({ date: new Date('2024-12-31'), description: 'Abschluss Ertragskonten', reference: 'AB-2024-001', lines: [ - { accountNumber: '8400', debit: 46000, description: 'Erlöse abschließen' }, - { accountNumber: '9400', credit: 46000, description: 'GuV-Konto' }, + { accountNumber: '8400', debit: 46000, description: 'Erlöse abschließen', postingKey: 40 }, + { accountNumber: '9400', credit: 46000, description: 'GuV-Konto', postingKey: 40 }, ], skrType: 'SKR03', }); - // Close expense accounts + // Close expense accounts - year-end closing uses posting key 40 await api.postJournalEntry({ date: new Date('2024-12-31'), description: 'Abschluss Aufwandskonten', reference: 'AB-2024-002', lines: [ - { accountNumber: '9400', debit: 45450, description: 'GuV-Konto' }, - { accountNumber: '7300', debit: 1000, description: 'Versicherung abschließen (credit balance)' }, - { accountNumber: '5900', debit: 3000, description: 'Bestandsveränderungen abschließen (credit balance)' }, - { accountNumber: '5400', credit: 5000, description: 'Wareneingang abschließen' }, - { accountNumber: '6000', credit: 18000, description: 'Löhne und Gehälter abschließen' }, - { accountNumber: '6100', credit: 3600, description: 'SV AG-Anteil abschließen' }, - { accountNumber: '7000', credit: 10000, description: 'AfA abschließen' }, - { accountNumber: '7100', credit: 2000, description: 'Miete abschließen' }, - { accountNumber: '7400', credit: 150, description: 'Kfz abschließen' }, - { accountNumber: '6600', credit: 5000, description: 'Werbung abschließen' }, - { accountNumber: '6700', credit: 5500, description: 'Beratung abschließen' }, - { accountNumber: '6800', credit: 200, description: 'Bürobedarf abschließen' }, + { accountNumber: '9400', debit: 45450, description: 'GuV-Konto', postingKey: 40 }, + { accountNumber: '7300', debit: 1000, description: 'Versicherung abschließen (credit balance)', postingKey: 40 }, + { accountNumber: '5900', debit: 3000, description: 'Bestandsveränderungen abschließen (credit balance)', postingKey: 40 }, + { accountNumber: '5400', credit: 5000, description: 'Wareneingang abschließen', postingKey: 40 }, + { accountNumber: '6000', credit: 18000, description: 'Löhne und Gehälter abschließen', postingKey: 40 }, + { accountNumber: '6100', credit: 3600, description: 'SV AG-Anteil abschließen', postingKey: 40 }, + { accountNumber: '7000', credit: 10000, description: 'AfA abschließen', postingKey: 40 }, + { accountNumber: '7100', credit: 2000, description: 'Miete abschließen', postingKey: 40 }, + { accountNumber: '7400', credit: 150, description: 'Kfz abschließen', postingKey: 40 }, + { accountNumber: '6600', credit: 5000, description: 'Werbung abschließen', postingKey: 40 }, + { accountNumber: '6700', credit: 5500, description: 'Beratung abschließen', postingKey: 40 }, + { accountNumber: '6800', credit: 200, description: 'Bürobedarf abschließen', postingKey: 40 }, ], 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 if (guv_result > 0) { await api.postJournalEntry({ @@ -408,8 +429,8 @@ tap.test('should perform closing entries (Abschlussbuchungen)', async () => { description: 'Jahresgewinn auf Eigenkapital', reference: 'AB-2024-003', lines: [ - { accountNumber: '9400', debit: guv_result, description: 'GuV-Konto ausgleichen' }, - { accountNumber: '2900', credit: guv_result, description: 'Gewinnrücklagen' }, + { accountNumber: '9400', debit: guv_result, description: 'GuV-Konto ausgleichen', postingKey: 40 }, + { accountNumber: '2900', credit: guv_result, description: 'Gewinnrücklagen', postingKey: 40 }, ], skrType: 'SKR03', }); @@ -419,8 +440,8 @@ tap.test('should perform closing entries (Abschlussbuchungen)', async () => { description: 'Jahresverlust auf Eigenkapital', reference: 'AB-2024-003', lines: [ - { accountNumber: '2500', debit: Math.abs(guv_result), description: 'Verlustvortrag' }, - { accountNumber: '9400', credit: Math.abs(guv_result), description: 'GuV-Konto ausgleichen' }, + { accountNumber: '2500', debit: Math.abs(guv_result), description: 'Verlustvortrag', postingKey: 40 }, + { accountNumber: '9400', credit: Math.abs(guv_result), description: 'GuV-Konto ausgleichen', postingKey: 40 }, ], skrType: 'SKR03', }); diff --git a/test/test.jahresabschluss.skr04.ts b/test/test.jahresabschluss.skr04.ts index f4e23ca..d526d2f 100644 --- a/test/test.jahresabschluss.skr04.ts +++ b/test/test.jahresabschluss.skr04.ts @@ -7,7 +7,7 @@ let testConfig: Awaited>; tap.test('should demonstrate complete Jahresabschluss (Annual Financial Statement) for SKR04', async () => { testConfig = await getTestConfig(); - + // Use timestamp to ensure unique database for each test run const timestamp = Date.now(); api = new skr.SkrApi({ @@ -17,12 +17,31 @@ tap.test('should demonstrate complete Jahresabschluss (Annual Financial Statemen await api.initialize('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 () => { // Opening balances from previous year's closing // 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) const openingEntry = await api.postJournalEntry({ date: new Date('2024-01-01'), @@ -30,19 +49,19 @@ tap.test('should set up opening balances (Eröffnungsbilanz) for SKR04', async ( reference: 'EB-2024', lines: [ // Debit all asset accounts - { accountNumber: '0200', debit: 45000, description: 'Grundstücke' }, - { accountNumber: '0210', debit: 120000, description: 'Gebäude' }, - { accountNumber: '0500', debit: 35000, description: 'BGA' }, - { accountNumber: '0400', debit: 8000, description: 'Fuhrpark' }, - { accountNumber: '1200', debit: 25000, description: 'Bank' }, - { accountNumber: '1000', debit: 2500, description: 'Kasse' }, - { accountNumber: '1400', debit: 18000, description: 'Forderungen' }, - + { accountNumber: '0200', debit: 45000, description: 'Grundstücke', postingKey: 40 }, + { accountNumber: '0210', debit: 120000, description: 'Gebäude', postingKey: 40 }, + { accountNumber: '0500', debit: 35000, description: 'BGA', postingKey: 40 }, + { accountNumber: '0400', debit: 8000, description: 'Fuhrpark', postingKey: 40 }, + { accountNumber: '1200', debit: 25000, description: 'Bank', postingKey: 40 }, + { accountNumber: '1000', debit: 2500, description: 'Kasse', postingKey: 40 }, + { accountNumber: '10001', debit: 18000, description: 'Forderungen Kunde', postingKey: 40 }, + // Credit all liability and equity accounts - { accountNumber: '9000', credit: 150000, description: 'Eigenkapital' }, - { accountNumber: '9300', credit: 35000, description: 'Gewinnrücklagen' }, - { accountNumber: '1600', credit: 40500, description: 'Verbindlichkeiten L+L' }, - { accountNumber: '1700', credit: 28000, description: 'Sonstige Verbindlichkeiten' }, + { accountNumber: '9000', credit: 150000, description: 'Eigenkapital', postingKey: 40 }, + { accountNumber: '9300', credit: 35000, description: 'Gewinnrücklagen', postingKey: 40 }, + { accountNumber: '70001', credit: 40500, description: 'Verbindlichkeiten Lieferant', postingKey: 40 }, + { accountNumber: '1700', credit: 28000, description: 'Sonstige Verbindlichkeiten', postingKey: 40 }, ], skrType: 'SKR04', }); @@ -61,9 +80,9 @@ tap.test('should record Q1 business transactions for SKR04', async () => { description: 'Verkauf Waren auf Rechnung', reference: 'RE-2024-001', lines: [ - { accountNumber: '1400', debit: 11900, description: 'Forderungen inkl. USt' }, - { accountNumber: '4300', credit: 10000, description: 'Erlöse 19% USt' }, - { accountNumber: '1771', credit: 1900, description: 'Umsatzsteuer 19%' }, + { accountNumber: '10001', debit: 11900, description: 'Forderungen inkl. USt', postingKey: 9 }, + { accountNumber: '4300', credit: 10000, description: 'Erlöse 19% USt', postingKey: 40 }, + { accountNumber: '1771', credit: 1900, description: 'Umsatzsteuer 19%', postingKey: 40 }, ], skrType: 'SKR04', }); @@ -74,9 +93,9 @@ tap.test('should record Q1 business transactions for SKR04', async () => { description: 'Einkauf Material auf Rechnung', reference: 'ER-2024-001', lines: [ - { accountNumber: '2100', debit: 5000, description: 'Bezogene Waren' }, - { accountNumber: '1571', debit: 950, description: 'Vorsteuer 19%' }, - { accountNumber: '1600', credit: 5950, description: 'Verbindlichkeiten' }, + { accountNumber: '2100', debit: 5000, description: 'Bezogene Waren', postingKey: 40 }, + { accountNumber: '1571', debit: 950, description: 'Vorsteuer 19%', postingKey: 9 }, + { accountNumber: '70001', credit: 5950, description: 'Verbindlichkeiten', postingKey: 9 }, ], skrType: 'SKR04', }); @@ -87,9 +106,9 @@ tap.test('should record Q1 business transactions for SKR04', async () => { description: 'Gehaltszahlung Januar', reference: 'GH-2024-01', lines: [ - { accountNumber: '2300', debit: 8000, description: 'Löhne' }, - { accountNumber: '2400', debit: 1600, description: 'Gehälter' }, - { accountNumber: '1200', credit: 9600, description: 'Banküberweisung' }, + { accountNumber: '2300', debit: 8000, description: 'Löhne', postingKey: 40 }, + { accountNumber: '2400', debit: 1600, description: 'Gehälter', postingKey: 40 }, + { accountNumber: '1200', credit: 9600, description: 'Banküberweisung', postingKey: 40 }, ], skrType: 'SKR04', }); @@ -100,8 +119,8 @@ tap.test('should record Q1 business transactions for SKR04', async () => { description: 'Zahlungseingang Kunde', reference: 'ZE-2024-001', lines: [ - { accountNumber: '1200', debit: 11900, description: 'Bankgutschrift' }, - { accountNumber: '1400', credit: 11900, description: 'Forderungsausgleich' }, + { accountNumber: '1200', debit: 11900, description: 'Bankgutschrift', postingKey: 40 }, + { accountNumber: '10001', credit: 11900, description: 'Forderungsausgleich', postingKey: 3 }, ], skrType: 'SKR04', }); @@ -112,8 +131,8 @@ tap.test('should record Q1 business transactions for SKR04', async () => { description: 'Miete Februar', reference: 'MI-2024-02', lines: [ - { accountNumber: '3000', debit: 2000, description: 'Miete' }, - { accountNumber: '1200', credit: 2000, description: 'Banküberweisung' }, + { accountNumber: '3000', debit: 2000, description: 'Miete', postingKey: 40 }, + { accountNumber: '1200', credit: 2000, description: 'Banküberweisung', postingKey: 40 }, ], skrType: 'SKR04', }); @@ -124,9 +143,9 @@ tap.test('should record Q1 business transactions for SKR04', async () => { description: 'Büromaterial', reference: 'BM-2024-001', lines: [ - { accountNumber: '3100', debit: 200, description: 'Bürobedarf' }, - { accountNumber: '1571', debit: 38, description: 'Vorsteuer 19%' }, - { accountNumber: '1200', credit: 238, description: 'Bankzahlung' }, + { accountNumber: '3100', debit: 200, description: 'Bürobedarf', postingKey: 40 }, + { accountNumber: '1571', debit: 38, description: 'Vorsteuer 19%', postingKey: 9 }, + { accountNumber: '1200', credit: 238, description: 'Bankzahlung', postingKey: 40 }, ], skrType: 'SKR04', }); @@ -137,9 +156,9 @@ tap.test('should record Q1 business transactions for SKR04', async () => { description: 'Tankrechnung Firmenfahrzeug', reference: 'KFZ-2024-001', lines: [ - { accountNumber: '3300', debit: 150, description: 'Kfz-Kosten' }, - { accountNumber: '1571', debit: 28.50, description: 'Vorsteuer 19%' }, - { accountNumber: '1200', credit: 178.50, description: 'Bankzahlung' }, + { accountNumber: '3300', debit: 150, description: 'Kfz-Kosten', postingKey: 40 }, + { accountNumber: '1571', debit: 28.50, description: 'Vorsteuer 19%', postingKey: 9 }, + { accountNumber: '1200', credit: 178.50, description: 'Bankzahlung', postingKey: 40 }, ], skrType: 'SKR04', }); @@ -150,9 +169,9 @@ tap.test('should record Q1 business transactions for SKR04', async () => { description: 'Verkauf Dienstleistung', reference: 'RE-2024-002', lines: [ - { accountNumber: '1400', debit: 7140, description: 'Forderungen inkl. USt' }, - { accountNumber: '4300', credit: 6000, description: 'Erlöse 19% USt' }, - { accountNumber: '1771', credit: 1140, description: 'Umsatzsteuer 19%' }, + { accountNumber: '10001', debit: 7140, description: 'Forderungen inkl. USt', postingKey: 9 }, + { accountNumber: '4300', credit: 6000, description: 'Erlöse 19% USt', postingKey: 40 }, + { accountNumber: '1771', credit: 1140, description: 'Umsatzsteuer 19%', postingKey: 40 }, ], skrType: 'SKR04', }); @@ -167,9 +186,9 @@ tap.test('should record Q2-Q4 business transactions for SKR04', async () => { description: 'Kauf neue Produktionsmaschine', reference: 'INV-2024-001', lines: [ - { accountNumber: '0500', debit: 25000, description: 'Neue Maschine' }, - { accountNumber: '1571', debit: 4750, description: 'Vorsteuer 19%' }, - { accountNumber: '1200', credit: 29750, description: 'Banküberweisung' }, + { accountNumber: '0500', debit: 25000, description: 'Neue Maschine', postingKey: 40 }, + { accountNumber: '1571', debit: 4750, description: 'Vorsteuer 19%', postingKey: 9 }, + { accountNumber: '1200', credit: 29750, description: 'Banküberweisung', postingKey: 40 }, ], skrType: 'SKR04', }); @@ -180,9 +199,9 @@ tap.test('should record Q2-Q4 business transactions for SKR04', async () => { description: 'Großauftrag Kunde ABC', reference: 'RE-2024-003', lines: [ - { accountNumber: '1400', debit: 35700, description: 'Forderungen inkl. USt' }, - { accountNumber: '4300', credit: 30000, description: 'Erlöse 19% USt' }, - { accountNumber: '1771', credit: 5700, description: 'Umsatzsteuer 19%' }, + { accountNumber: '10001', debit: 35700, description: 'Forderungen inkl. USt', postingKey: 9 }, + { accountNumber: '4300', credit: 30000, description: 'Erlöse 19% USt', postingKey: 40 }, + { accountNumber: '1771', credit: 5700, description: 'Umsatzsteuer 19%', postingKey: 40 }, ], skrType: 'SKR04', }); @@ -193,9 +212,9 @@ tap.test('should record Q2-Q4 business transactions for SKR04', async () => { description: 'Werbekampagne', reference: 'WK-2024-001', lines: [ - { accountNumber: '3400', debit: 5000, description: 'Werbekosten' }, - { accountNumber: '1571', debit: 950, description: 'Vorsteuer 19%' }, - { accountNumber: '1600', credit: 5950, description: 'Verbindlichkeiten' }, + { accountNumber: '3400', debit: 5000, description: 'Werbekosten', postingKey: 40 }, + { accountNumber: '1571', debit: 950, description: 'Vorsteuer 19%', postingKey: 9 }, + { accountNumber: '70001', credit: 5950, description: 'Verbindlichkeiten', postingKey: 9 }, ], skrType: 'SKR04', }); @@ -206,9 +225,9 @@ tap.test('should record Q2-Q4 business transactions for SKR04', async () => { description: 'Steuerberatung', reference: 'STB-2024-001', lines: [ - { accountNumber: '3500', debit: 2500, description: 'Steuerberatungskosten' }, - { accountNumber: '1571', debit: 475, description: 'Vorsteuer 19%' }, - { accountNumber: '1200', credit: 2975, description: 'Banküberweisung' }, + { accountNumber: '3500', debit: 2500, description: 'Steuerberatungskosten', postingKey: 40 }, + { accountNumber: '1571', debit: 475, description: 'Vorsteuer 19%', postingKey: 9 }, + { accountNumber: '1200', credit: 2975, description: 'Banküberweisung', postingKey: 40 }, ], skrType: 'SKR04', }); @@ -219,9 +238,9 @@ tap.test('should record Q2-Q4 business transactions for SKR04', async () => { description: 'Jahresbonus Mitarbeiter', reference: 'BON-2024', lines: [ - { accountNumber: '2300', debit: 10000, description: 'Tantieme' }, - { accountNumber: '2400', debit: 2000, description: 'Gehälter Bonus' }, - { accountNumber: '1200', credit: 12000, description: 'Banküberweisung' }, + { accountNumber: '2300', debit: 10000, description: 'Tantieme', postingKey: 40 }, + { accountNumber: '2400', debit: 2000, description: 'Gehälter Bonus', postingKey: 40 }, + { accountNumber: '1200', credit: 12000, description: 'Banküberweisung', postingKey: 40 }, ], skrType: 'SKR04', }); @@ -232,8 +251,8 @@ tap.test('should record Q2-Q4 business transactions for SKR04', async () => { description: 'Zahlungseingang Großauftrag', reference: 'ZE-2024-003', lines: [ - { accountNumber: '1200', debit: 35700, description: 'Bankgutschrift' }, - { accountNumber: '1400', credit: 35700, description: 'Forderungsausgleich' }, + { accountNumber: '1200', debit: 35700, description: 'Bankgutschrift', postingKey: 40 }, + { accountNumber: '10001', credit: 35700, description: 'Forderungsausgleich', postingKey: 3 }, ], skrType: 'SKR04', }); @@ -246,8 +265,8 @@ tap.test('should perform year-end adjustments (Jahresabschlussbuchungen) for SKR description: 'Abschreibung Gebäude (linear 2%)', reference: 'AFA-2024-001', lines: [ - { accountNumber: '3700', debit: 2400, description: 'AfA auf Gebäude' }, - { accountNumber: '0210', credit: 2400, description: 'Wertberichtigung Gebäude' }, + { accountNumber: '3700', debit: 2400, description: 'AfA auf Gebäude', postingKey: 40 }, + { accountNumber: '0210', credit: 2400, description: 'Wertberichtigung Gebäude', postingKey: 40 }, ], skrType: 'SKR04', }); @@ -257,8 +276,8 @@ tap.test('should perform year-end adjustments (Jahresabschlussbuchungen) for SKR description: 'Abschreibung BGA (linear 10%)', reference: 'AFA-2024-002', lines: [ - { accountNumber: '3700', debit: 6000, description: 'AfA auf BGA' }, // (35000 + 25000) * 10% - { accountNumber: '0500', credit: 6000, description: 'Wertberichtigung BGA' }, + { accountNumber: '3700', debit: 6000, description: 'AfA auf BGA', postingKey: 40 }, // (35000 + 25000) * 10% + { accountNumber: '0500', credit: 6000, description: 'Wertberichtigung BGA', postingKey: 40 }, ], skrType: 'SKR04', }); @@ -268,8 +287,8 @@ tap.test('should perform year-end adjustments (Jahresabschlussbuchungen) for SKR description: 'Abschreibung Fuhrpark (linear 20%)', reference: 'AFA-2024-003', lines: [ - { accountNumber: '3700', debit: 1600, description: 'AfA auf Fuhrpark' }, - { accountNumber: '0400', credit: 1600, description: 'Wertberichtigung Fuhrpark' }, + { accountNumber: '3700', debit: 1600, description: 'AfA auf Fuhrpark', postingKey: 40 }, + { accountNumber: '0400', credit: 1600, description: 'Wertberichtigung Fuhrpark', postingKey: 40 }, ], skrType: 'SKR04', }); @@ -280,8 +299,8 @@ tap.test('should perform year-end adjustments (Jahresabschlussbuchungen) for SKR description: 'Aktive Rechnungsabgrenzung - Vorausbezahlte Versicherung', reference: 'ARA-2024-001', lines: [ - { accountNumber: '1900', debit: 1000, description: 'Aktive Rechnungsabgrenzung' }, - { accountNumber: '3200', credit: 1000, description: 'Versicherungen' }, + { accountNumber: '1900', debit: 1000, description: 'Aktive Rechnungsabgrenzung', postingKey: 40 }, + { accountNumber: '3200', credit: 1000, description: 'Versicherungen', postingKey: 40 }, ], skrType: 'SKR04', }); @@ -292,8 +311,8 @@ tap.test('should perform year-end adjustments (Jahresabschlussbuchungen) for SKR description: 'Rückstellung für Jahresabschlusskosten', reference: 'RS-2024-001', lines: [ - { accountNumber: '3500', debit: 3000, description: 'Rechts- und Beratungskosten' }, - { accountNumber: '0800', credit: 3000, description: 'Rückstellungen' }, + { accountNumber: '3500', debit: 3000, description: 'Rechts- und Beratungskosten', postingKey: 40 }, + { accountNumber: '0800', credit: 3000, description: 'Rückstellungen', postingKey: 40 }, ], skrType: 'SKR04', }); @@ -304,9 +323,9 @@ tap.test('should perform year-end adjustments (Jahresabschlussbuchungen) for SKR description: 'USt-Abschluss Q4', reference: 'UST-2024-Q4', lines: [ - { accountNumber: '1771', debit: 8740, description: 'USt-Saldo' }, // Total collected VAT - { accountNumber: '1571', credit: 7191.50, description: 'Vorsteuer-Saldo' }, // Total input VAT - { accountNumber: '1700', credit: 1548.50, description: 'USt-Zahllast' }, + { accountNumber: '1771', debit: 8740, description: 'USt-Saldo', postingKey: 40 }, // Total collected VAT + { accountNumber: '1571', credit: 7191.50, description: 'Vorsteuer-Saldo', postingKey: 40 }, // Total input VAT + { accountNumber: '1700', credit: 1548.50, description: 'USt-Zahllast', postingKey: 40 }, ], skrType: 'SKR04', }); @@ -360,8 +379,8 @@ tap.test('should perform closing entries (Abschlussbuchungen) for SKR04', async description: 'Abschluss Ertragskonten', reference: 'AB-2024-001', lines: [ - { accountNumber: '4300', debit: 46000, description: 'Erlöse abschließen' }, - { accountNumber: '9500', credit: 46000, description: 'GuV-Konto' }, + { accountNumber: '4300', debit: 46000, description: 'Erlöse abschließen', postingKey: 40 }, + { accountNumber: '9500', credit: 46000, description: 'GuV-Konto', postingKey: 40 }, ], skrType: 'SKR04', }); @@ -372,17 +391,17 @@ tap.test('should perform closing entries (Abschlussbuchungen) for SKR04', async description: 'Abschluss Aufwandskonten', reference: 'AB-2024-002', lines: [ - { accountNumber: '9500', debit: 48450, description: 'GuV-Konto' }, - { accountNumber: '3200', debit: 1000, description: 'Versicherung abschließen (credit balance)' }, - { accountNumber: '2100', credit: 5000, description: 'Bezogene Waren abschließen' }, - { accountNumber: '2300', credit: 18000, description: 'Löhne abschließen' }, - { accountNumber: '2400', credit: 3600, description: 'Gehälter abschließen' }, - { accountNumber: '3700', credit: 10000, description: 'AfA abschließen' }, - { accountNumber: '3000', credit: 2000, description: 'Miete abschließen' }, - { accountNumber: '3300', credit: 150, description: 'Kfz abschließen' }, - { accountNumber: '3400', credit: 5000, description: 'Werbung abschließen' }, - { accountNumber: '3500', credit: 5500, description: 'Beratung abschließen' }, - { accountNumber: '3100', credit: 200, description: 'Bürobedarf abschließen' }, + { accountNumber: '9500', debit: 48450, description: 'GuV-Konto', postingKey: 40 }, + { accountNumber: '3200', debit: 1000, description: 'Versicherung abschließen (credit balance)', postingKey: 40 }, + { accountNumber: '2100', credit: 5000, description: 'Bezogene Waren abschließen', postingKey: 40 }, + { accountNumber: '2300', credit: 18000, description: 'Löhne abschließen', postingKey: 40 }, + { accountNumber: '2400', credit: 3600, description: 'Gehälter abschließen', postingKey: 40 }, + { accountNumber: '3700', credit: 10000, description: 'AfA abschließen', postingKey: 40 }, + { accountNumber: '3000', credit: 2000, description: 'Miete abschließen', postingKey: 40 }, + { accountNumber: '3300', credit: 150, description: 'Kfz abschließen', postingKey: 40 }, + { accountNumber: '3400', credit: 5000, description: 'Werbung abschließen', postingKey: 40 }, + { accountNumber: '3500', credit: 5500, description: 'Beratung abschließen', postingKey: 40 }, + { accountNumber: '3100', credit: 200, description: 'Bürobedarf abschließen', postingKey: 40 }, ], skrType: 'SKR04', }); @@ -395,8 +414,8 @@ tap.test('should perform closing entries (Abschlussbuchungen) for SKR04', async description: 'Jahresgewinn auf Eigenkapital', reference: 'AB-2024-003', lines: [ - { accountNumber: '9500', debit: guv_result, description: 'GuV-Konto ausgleichen' }, - { accountNumber: '9300', credit: guv_result, description: 'Gewinnrücklagen' }, + { accountNumber: '9500', debit: guv_result, description: 'GuV-Konto ausgleichen', postingKey: 40 }, + { accountNumber: '9300', credit: guv_result, description: 'Gewinnrücklagen', postingKey: 40 }, ], skrType: 'SKR04', }); @@ -406,8 +425,8 @@ tap.test('should perform closing entries (Abschlussbuchungen) for SKR04', async description: 'Jahresverlust auf Eigenkapital', reference: 'AB-2024-003', lines: [ - { accountNumber: '9400', debit: Math.abs(guv_result), description: 'Verlustvortrag' }, - { accountNumber: '9500', credit: Math.abs(guv_result), description: 'GuV-Konto ausgleichen' }, + { accountNumber: '9400', debit: Math.abs(guv_result), description: 'Verlustvortrag', postingKey: 40 }, + { accountNumber: '9500', credit: Math.abs(guv_result), description: 'GuV-Konto ausgleichen', postingKey: 40 }, ], skrType: 'SKR04', }); diff --git a/test/test.skr03.ts b/test/test.skr03.ts index 2e4eee6..0282c35 100644 --- a/test/test.skr03.ts +++ b/test/test.skr03.ts @@ -91,9 +91,9 @@ tap.test('should post journal entry in SKR03', async () => { description: 'Test journal entry', reference: 'JE-001', lines: [ - { accountNumber: '1000', debit: 500 }, // Cash - { accountNumber: '1200', debit: 500 }, // Bank - { accountNumber: '4000', credit: 1000 }, // Revenue + { accountNumber: '1000', debit: 500, postingKey: 40 }, // Cash + { accountNumber: '1200', debit: 500, postingKey: 40 }, // Bank + { accountNumber: '4000', credit: 1000, postingKey: 40 }, // Revenue ], skrType: 'SKR03', }); diff --git a/test/test.skr04.ts b/test/test.skr04.ts index 682a893..716aa06 100644 --- a/test/test.skr04.ts +++ b/test/test.skr04.ts @@ -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 () => { + // 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({ date: new Date(), debitAccount: '5400', // Goods with 19% VAT - creditAccount: '1600', // Trade payables + creditAccount: '70001', // Creditor account (supplier) amount: 119, description: 'Purchase with VAT', reference: 'BILL-001', diff --git a/test/test.transactions.ts b/test/test.transactions.ts index 9d88b97..e87ff7c 100644 --- a/test/test.transactions.ts +++ b/test/test.transactions.ts @@ -29,8 +29,8 @@ tap.test('should enforce double-entry bookkeeping rules', async () => { description: 'Unbalanced entry', reference: 'TEST-001', lines: [ - { accountNumber: '1000', debit: 100 }, - { accountNumber: '4000', credit: 50 }, // Unbalanced! + { accountNumber: '1000', debit: 100, postingKey: 40 }, + { accountNumber: '4000', credit: 50, postingKey: 40 }, // Unbalanced! ], skrType: 'SKR03', }); @@ -99,10 +99,10 @@ tap.test( description: 'Complex distribution', reference: 'COMPLEX-001', lines: [ - { accountNumber: '5000', debit: 500, description: 'Materials' }, - { accountNumber: '6000', debit: 300, description: 'Wages' }, - { accountNumber: '7100', debit: 200, description: 'Rent' }, - { accountNumber: '1200', credit: 1000, description: 'Bank payment' }, + { accountNumber: '5000', debit: 500, description: 'Materials', postingKey: 40 }, + { accountNumber: '6000', debit: 300, description: 'Wages', postingKey: 40 }, + { accountNumber: '7100', debit: 200, description: 'Rent', postingKey: 40 }, + { accountNumber: '1200', credit: 1000, description: 'Bank payment', postingKey: 40 }, ], skrType: 'SKR03', }); @@ -220,10 +220,19 @@ tap.test('should handle batch transaction posting', 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({ date: new Date(), debitAccount: '5400', // Goods with 19% VAT - creditAccount: '1600', // Trade payables + creditAccount: '70001', // Creditor account (supplier) amount: 119, description: 'Purchase including VAT', skrType: 'SKR03', diff --git a/ts/skr.classes.account.ts b/ts/skr.classes.account.ts index cd18b97..7bb6242 100644 --- a/ts/skr.classes.account.ts +++ b/ts/skr.classes.account.ts @@ -56,6 +56,9 @@ export class Account extends SmartDataDbDoc { @svDb() public isSystemAccount: boolean; + @svDb() + public isAutomaticAccount: boolean; + @svDb() public createdAt: Date; @@ -90,6 +93,7 @@ export class Account extends SmartDataDbDoc { 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 { ); } + /** + * 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 { + // 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 { public async beforeSave(): Promise { // 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 { 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; + } } } diff --git a/ts/skr.classes.journalentry.ts b/ts/skr.classes.journalentry.ts index 9ccbbb6..00e16f1 100644 --- a/ts/skr.classes.journalentry.ts +++ b/ts/skr.classes.journalentry.ts @@ -2,6 +2,11 @@ import * as plugins from './plugins.js'; import { getDbSync } from './skr.database.js'; import { Account } from './skr.classes.account.js'; import { Transaction } from './skr.classes.transaction.js'; +import { + validatePostingKey, + validatePostingKeyConsistency, + getPostingKeyDescription, +} from './skr.postingkeys.js'; import type { TSKRType, IJournalEntry, @@ -212,22 +217,84 @@ export class JournalEntry extends SmartDataDbDoc { throw new Error('Journal entry must have at least 2 lines'); } - // Validate all accounts exist and are active + // Validate all accounts exist, are active, and can be posted to + const validationErrors: string[] = []; + const validationWarnings: string[] = []; + for (const line of this.lines) { + // Validate posting key is present (REQUIRED) + if (!line.postingKey) { + validationErrors.push( + `Line for account ${line.accountNumber} is missing required posting key (Buchungsschlüssel). ` + + `Posting keys are mandatory for DATEV compliance.` + ); + continue; // Skip further validation for this line + } + + // Validate account is not an automatic account (Automatikkonto) + try { + await Account.validateAccountForPosting(line.accountNumber, this.skrType); + } catch (error) { + validationErrors.push(error.message); + continue; // Skip further validation for this line + } + + // Get account for posting key validation const account = await Account.getAccountByNumber( line.accountNumber, this.skrType, ); if (!account) { - throw new Error( + validationErrors.push( `Account ${line.accountNumber} not found for ${this.skrType}`, ); + continue; } if (!account.isActive) { - throw new Error(`Account ${line.accountNumber} is not active`); + validationErrors.push(`Account ${line.accountNumber} is not active`); + continue; } + + // Validate posting key for this line + const amount = line.debit || line.credit || 0; + const postingKeyValidation = validatePostingKey( + line.postingKey, + line.accountNumber, + amount + ); + + if (!postingKeyValidation.isValid) { + validationErrors.push(...postingKeyValidation.errors); + } + + if (postingKeyValidation.warnings.length > 0) { + validationWarnings.push(...postingKeyValidation.warnings); + } + } + + // Validate posting key consistency across all lines + const consistencyValidation = validatePostingKeyConsistency(this.lines); + if (!consistencyValidation.isValid) { + validationErrors.push(...consistencyValidation.errors); + } + if (consistencyValidation.warnings.length > 0) { + validationWarnings.push(...consistencyValidation.warnings); + } + + // Log warnings but don't fail validation + if (validationWarnings.length > 0) { + console.warn('Journal entry validation warnings:'); + validationWarnings.forEach(warning => console.warn(` - ${warning}`)); + } + + // Throw if any errors + if (validationErrors.length > 0) { + throw new Error( + 'Journal entry validation failed:\n' + + validationErrors.map(e => ` - ${e}`).join('\n') + ); } } @@ -325,6 +392,7 @@ export class JournalEntry extends SmartDataDbDoc { credit: line.debit, // Swap description: `Reversal: ${line.description || ''}`, costCenter: line.costCenter, + postingKey: line.postingKey, // Keep same posting key for reversal })); const reversalEntry = new JournalEntry({ diff --git a/ts/skr.classes.ledger.ts b/ts/skr.classes.ledger.ts index e7df185..7c4087a 100644 --- a/ts/skr.classes.ledger.ts +++ b/ts/skr.classes.ledger.ts @@ -418,6 +418,7 @@ export class Ledger { accountNumber: account.accountNumber, debit: Math.abs(balance), description: `Closing ${account.accountName}`, + postingKey: 40, // Tax-free - internal closing entry }); totalRevenue += Math.abs(balance); } @@ -429,6 +430,7 @@ export class Ledger { accountNumber: closingAccountNumber, credit: totalRevenue, description: 'Revenue closing to P&L', + postingKey: 40, // Tax-free - internal closing entry }); const revenueClosingEntry = await this.postJournalEntry({ @@ -458,6 +460,7 @@ export class Ledger { accountNumber: account.accountNumber, credit: Math.abs(balance), description: `Closing ${account.accountName}`, + postingKey: 40, // Tax-free - internal closing entry }); totalExpense += Math.abs(balance); } @@ -469,6 +472,7 @@ export class Ledger { accountNumber: closingAccountNumber, debit: totalExpense, description: 'Expense closing to P&L', + postingKey: 40, // Tax-free - internal closing entry }); const expenseClosingEntry = await this.postJournalEntry({ diff --git a/ts/skr.invoice.booking.ts b/ts/skr.invoice.booking.ts index 96ae676..8183ae0 100644 --- a/ts/skr.invoice.booking.ts +++ b/ts/skr.invoice.booking.ts @@ -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 } ); } diff --git a/ts/skr.postingkeys.ts b/ts/skr.postingkeys.ts new file mode 100644 index 0000000..552d61a --- /dev/null +++ b/ts/skr.postingkeys.ts @@ -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 = { + 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); +} diff --git a/ts/skr.types.ts b/ts/skr.types.ts index ef02eab..0048725 100644 --- a/ts/skr.types.ts +++ b/ts/skr.types.ts @@ -9,6 +9,18 @@ export type TSKRType = 'SKR03' | 'SKR04'; 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 = | 'trial_balance' | 'income_statement' @@ -16,6 +28,18 @@ export type TReportType = | 'general_ledger' | '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 { accountNumber: string; accountName: string; @@ -25,6 +49,7 @@ export interface IAccountData { description?: string; vatRate?: number; isActive?: boolean; + isAutomaticAccount?: boolean; // Automatikkonto (e.g., 1400, 1600) - cannot be posted to directly } export interface ITransactionData { @@ -53,6 +78,7 @@ export interface IJournalEntryLine { credit?: number; description?: string; costCenter?: string; + postingKey: TPostingKey; // REQUIRED: DATEV posting key for VAT automation control } export interface ITrialBalanceEntry { diff --git a/ts/skr03.data.ts b/ts/skr03.data.ts index e985ab1..d47acd1 100644 --- a/ts/skr03.data.ts +++ b/ts/skr03.data.ts @@ -159,6 +159,7 @@ export const SKR03_ACCOUNTS: IAccountData[] = [ accountType: 'asset', skrType: 'SKR03', description: 'Trade receivables', + isAutomaticAccount: true, // Automatikkonto - cannot be posted to directly, use debtor accounts (10000-69999) }, { accountNumber: '1500', @@ -199,6 +200,7 @@ export const SKR03_ACCOUNTS: IAccountData[] = [ accountType: 'liability', skrType: 'SKR03', description: 'Trade payables', + isAutomaticAccount: true, // Automatikkonto - cannot be posted to directly, use creditor accounts (70000-99999) }, { accountNumber: '1700', diff --git a/ts/skr04.data.ts b/ts/skr04.data.ts index 970825c..2247f54 100644 --- a/ts/skr04.data.ts +++ b/ts/skr04.data.ts @@ -159,6 +159,7 @@ export const SKR04_ACCOUNTS: IAccountData[] = [ accountType: 'asset', skrType: 'SKR04', description: 'Trade receivables', + isAutomaticAccount: true, // Automatikkonto - cannot be posted to directly, use debtor accounts (10000-69999) }, { accountNumber: '1500', @@ -199,6 +200,7 @@ export const SKR04_ACCOUNTS: IAccountData[] = [ accountType: 'liability', skrType: 'SKR04', description: 'Trade payables', + isAutomaticAccount: true, // Automatikkonto - cannot be posted to directly, use creditor accounts (70000-99999) }, { accountNumber: '1700',