diff --git a/migrations/001_initial_schema.sql b/migrations/001_initial_schema.sql index caf63ce..01be8d2 100644 --- a/migrations/001_initial_schema.sql +++ b/migrations/001_initial_schema.sql @@ -23,7 +23,7 @@ CREATE TABLE IF NOT EXISTS accounts ( id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY, kontonummer VARCHAR(10) NOT NULL UNIQUE, bezeichnung VARCHAR(255) NOT NULL, - kontenart ENUM('Aktiv','Passiv','Aufwand','Ertrag','Neutral') NOT NULL, + kontenart ENUM('Betriebseinnahme','Betriebsausgabe','Zahlungskonto','Verbindlichkeit','Privat') NOT NULL, beschreibung TEXT, steuercode VARCHAR(20) NULL COMMENT 'z.B. USt19, VSt19, USt7, VSt7', ustva_kz VARCHAR(10) NULL COMMENT 'ELSTER Kennzahl z.B. 81, 35', @@ -122,34 +122,36 @@ CREATE TABLE IF NOT EXISTS belege ( ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; -- ============================================================ --- Buchungen (Journaleinträge, doppelte Buchführung) +-- Buchungen (EÜR-Journaleinträge: Einnahme oder Ausgabe) -- GoBD: KEINE UPDATE/DELETE auf gesperrte Buchungen -- ============================================================ CREATE TABLE IF NOT EXISTS bookings ( - id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY, - buchungs_nr VARCHAR(30) NOT NULL UNIQUE, - beleg_id INT UNSIGNED NOT NULL, - buchungs_datum DATE NOT NULL, - soll_konto_id INT UNSIGNED NOT NULL, - haben_konto_id INT UNSIGNED NOT NULL, - betrag_netto DECIMAL(12,2) NOT NULL, - ust_satz DECIMAL(5,2) NOT NULL DEFAULT 0.00 COMMENT 'Steuersatz in % (0, 7, 19)', - betrag_ust DECIMAL(12,2) NOT NULL DEFAULT 0.00, - betrag_brutto DECIMAL(12,2) NOT NULL, - buchungstext VARCHAR(500) NOT NULL, - is_reversal TINYINT(1) NOT NULL DEFAULT 0 COMMENT '1 = Stornobuchung', - reversed_by INT UNSIGNED NULL COMMENT 'ID der Stornobuchung', - locked TINYINT(1) NOT NULL DEFAULT 0 COMMENT 'GoBD: unveränderlich', - fiscal_year_id INT UNSIGNED NOT NULL, - created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY, + buchungs_nr VARCHAR(30) NOT NULL UNIQUE, + beleg_id INT UNSIGNED NOT NULL, + buchungs_datum DATE NOT NULL, + buchungstyp ENUM('Einnahme','Ausgabe') NOT NULL, + kategorie_konto_id INT UNSIGNED NOT NULL COMMENT 'Was? z.B. Büromaterial, Honorar', + zahlungskonto_id INT UNSIGNED NOT NULL COMMENT 'Wie bezahlt? z.B. Bank, Kasse', + betrag_netto DECIMAL(12,2) NOT NULL, + ust_satz DECIMAL(5,2) NOT NULL DEFAULT 0.00 COMMENT 'Steuersatz in % (0, 7, 19)', + betrag_ust DECIMAL(12,2) NOT NULL DEFAULT 0.00, + betrag_brutto DECIMAL(12,2) NOT NULL, + buchungstext VARCHAR(500) NOT NULL, + is_reversal TINYINT(1) NOT NULL DEFAULT 0 COMMENT '1 = Stornobuchung', + reversed_by INT UNSIGNED NULL COMMENT 'ID der Stornobuchung', + locked TINYINT(1) NOT NULL DEFAULT 0 COMMENT 'GoBD: unveränderlich', + fiscal_year_id INT UNSIGNED NOT NULL, + created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, CONSTRAINT fk_booking_beleg FOREIGN KEY (beleg_id) REFERENCES belege(id), - CONSTRAINT fk_booking_soll FOREIGN KEY (soll_konto_id) REFERENCES accounts(id), - CONSTRAINT fk_booking_haben FOREIGN KEY (haben_konto_id) REFERENCES accounts(id), + CONSTRAINT fk_booking_kategorie FOREIGN KEY (kategorie_konto_id) REFERENCES accounts(id), + CONSTRAINT fk_booking_zahlung FOREIGN KEY (zahlungskonto_id) REFERENCES accounts(id), CONSTRAINT fk_booking_year FOREIGN KEY (fiscal_year_id) REFERENCES fiscal_years(id), INDEX idx_buchungs_datum (buchungs_datum), INDEX idx_fiscal_year (fiscal_year_id), - INDEX idx_soll_konto (soll_konto_id), - INDEX idx_haben_konto (haben_konto_id) + INDEX idx_buchungstyp (buchungstyp), + INDEX idx_kategorie_konto (kategorie_konto_id), + INDEX idx_zahlungskonto (zahlungskonto_id) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; -- GoBD-Trigger: Gesperrte Buchungen dürfen nicht geändert werden diff --git a/migrations/002_seed_skr03.sql b/migrations/002_seed_skr03.sql index e3b7780..2c35cb6 100644 --- a/migrations/002_seed_skr03.sql +++ b/migrations/002_seed_skr03.sql @@ -1,5 +1,5 @@ -- SKR03 Kontenrahmen - Wichtigste Konten für EÜR / Freiberufler --- Kontenarten: Aktiv, Passiv, Aufwand, Ertrag, Neutral +-- Kontenarten: Betriebseinnahme, Betriebsausgabe, Zahlungskonto, Verbindlichkeit, Privat -- steuercode: USt19, USt7, VSt19, VSt7, NULL (kein Steuercode) -- ustva_kz: ELSTER-Kennzahl für UStVA @@ -8,116 +8,116 @@ INSERT INTO accounts (kontonummer, bezeichnung, kontenart, steuercode, ustva_kz, -- ============================================================ -- KLASSE 0: Anlagevermögen -- ============================================================ -('0410', 'EDV-Software', 'Aktiv', NULL, NULL, 1), -('0420', 'Büroeinrichtung', 'Aktiv', NULL, NULL, 1), -('0430', 'Technische Anlagen und Maschinen', 'Aktiv', NULL, NULL, 1), -('0480', 'Betriebs- und Geschäftsausstattung', 'Aktiv', NULL, NULL, 1), -('0490', 'Geringwertige Wirtschaftsgüter', 'Aktiv', NULL, NULL, 1), +('0410', 'EDV-Software', 'Zahlungskonto', NULL, NULL, 1), +('0420', 'Büroeinrichtung', 'Zahlungskonto', NULL, NULL, 1), +('0430', 'Technische Anlagen und Maschinen', 'Zahlungskonto', NULL, NULL, 1), +('0480', 'Betriebs- und Geschäftsausstattung', 'Zahlungskonto', NULL, NULL, 1), +('0490', 'Geringwertige Wirtschaftsgüter', 'Zahlungskonto', NULL, NULL, 1), -- ============================================================ -- KLASSE 1: Umlaufvermögen / Geldkonten -- ============================================================ -('1000', 'Kasse', 'Aktiv', NULL, NULL, 1), -('1200', 'Bank', 'Aktiv', NULL, NULL, 1), -('1361', 'Abziehbare Vorsteuer 19 %', 'Aktiv', 'VSt19', '66', 1), -('1371', 'Abziehbare Vorsteuer 7 %', 'Aktiv', 'VSt7', '66', 1), -('1400', 'Forderungen aus Lieferungen und Leistungen', 'Aktiv', NULL, NULL, 1), -('1460', 'Forderungen aus Lieferungen und Leistungen (steuerfrei)', 'Aktiv', NULL, NULL, 1), -('1576', 'Abziehbare Vorsteuer 19 % (Eingangsrechnungen)', 'Aktiv', 'VSt19', '66', 1), -('1571', 'Abziehbare Vorsteuer 7 % (Eingangsrechnungen)', 'Aktiv', 'VSt7', '66', 1), +('1000', 'Kasse', 'Zahlungskonto', NULL, NULL, 1), +('1200', 'Bank', 'Zahlungskonto', NULL, NULL, 1), +('1361', 'Abziehbare Vorsteuer 19 %', 'Zahlungskonto', 'VSt19', '66', 1), +('1371', 'Abziehbare Vorsteuer 7 %', 'Zahlungskonto', 'VSt7', '66', 1), +('1400', 'Forderungen aus Lieferungen und Leistungen', 'Zahlungskonto', NULL, NULL, 1), +('1460', 'Forderungen aus Lieferungen und Leistungen (steuerfrei)', 'Zahlungskonto', NULL, NULL, 1), +('1576', 'Abziehbare Vorsteuer 19 % (Eingangsrechnungen)', 'Zahlungskonto', 'VSt19', '66', 1), +('1571', 'Abziehbare Vorsteuer 7 % (Eingangsrechnungen)', 'Zahlungskonto', 'VSt7', '66', 1), -- ============================================================ -- KLASSE 1: Verbindlichkeiten (Passiv) -- ============================================================ -('1600', 'Verbindlichkeiten aus Lieferungen und Leistungen', 'Passiv', NULL, NULL, 1), -('1700', 'Sonstige Verbindlichkeiten', 'Passiv', NULL, NULL, 1), -('1741', 'Umsatzsteuer 19 %', 'Passiv', 'USt19', '83', 1), -('1742', 'Umsatzsteuer 7 %', 'Passiv', 'USt7', '84', 1), -('1771', 'Umsatzsteuer-Vorauszahlungen', 'Passiv', NULL, NULL, 1), -('1780', 'Umsatzsteuer-Vorauszahlungen lfd. Jahr', 'Passiv', NULL, NULL, 1), +('1600', 'Verbindlichkeiten aus Lieferungen und Leistungen', 'Verbindlichkeit', NULL, NULL, 1), +('1700', 'Sonstige Verbindlichkeiten', 'Verbindlichkeit', NULL, NULL, 1), +('1741', 'Umsatzsteuer 19 %', 'Verbindlichkeit', 'USt19', '83', 1), +('1742', 'Umsatzsteuer 7 %', 'Verbindlichkeit', 'USt7', '84', 1), +('1771', 'Umsatzsteuer-Vorauszahlungen', 'Verbindlichkeit', NULL, NULL, 1), +('1780', 'Umsatzsteuer-Vorauszahlungen lfd. Jahr', 'Verbindlichkeit', NULL, NULL, 1), -- ============================================================ -- KLASSE 3: Wareneinkauf (Aufwand) -- ============================================================ -('3100', 'Wareneinkauf 19 % Vorsteuer', 'Aufwand', 'VSt19', '66', 1), -('3110', 'Wareneinkauf 7 % Vorsteuer', 'Aufwand', 'VSt7', '66', 1), -('3120', 'Wareneinkauf ohne Vorsteuer', 'Aufwand', NULL, NULL, 1), +('3100', 'Wareneinkauf 19 % Vorsteuer', 'Betriebsausgabe', 'VSt19', '66', 1), +('3110', 'Wareneinkauf 7 % Vorsteuer', 'Betriebsausgabe', 'VSt7', '66', 1), +('3120', 'Wareneinkauf ohne Vorsteuer', 'Betriebsausgabe', NULL, NULL, 1), -- ============================================================ -- KLASSE 4: Betriebliche Aufwendungen -- ============================================================ -('4100', 'Gehälter', 'Aufwand', NULL, NULL, 1), -('4110', 'Löhne', 'Aufwand', NULL, NULL, 1), -('4120', 'Gesetzliche soziale Aufwendungen', 'Aufwand', NULL, NULL, 1), -('4130', 'Freiwillige soziale Aufwendungen', 'Aufwand', NULL, NULL, 1), -('4140', 'Aufwendungen für Altersversorgung', 'Aufwand', NULL, NULL, 1), -('4200', 'Raumkosten (Miete)', 'Aufwand', 'VSt19', '66', 1), -('4210', 'Heizung', 'Aufwand', 'VSt19', '66', 1), -('4240', 'Gas, Strom, Wasser', 'Aufwand', 'VSt19', '66', 1), -('4250', 'Reinigung', 'Aufwand', 'VSt19', '66', 1), -('4260', 'Gebäudeversicherungen', 'Aufwand', NULL, NULL, 1), -('4300', 'Betriebliche Versicherungen', 'Aufwand', NULL, NULL, 1), -('4320', 'Beiträge', 'Aufwand', NULL, NULL, 1), -('4330', 'Berufsgenossenschaft', 'Aufwand', NULL, NULL, 1), -('4360', 'Beiträge zu Berufsverbänden', 'Aufwand', NULL, NULL, 1), -('4380', 'Verwaltungs-Gemeinkosten', 'Aufwand', 'VSt19', '66', 1), -('4390', 'Aufwand für Arbeitnehmer-Überlassung', 'Aufwand', 'VSt19', '66', 1), -('4400', 'PKW-Kosten', 'Aufwand', 'VSt19', '66', 1), -('4410', 'LKW-Kosten', 'Aufwand', 'VSt19', '66', 1), -('4430', 'Kraftfahrzeugsteuer', 'Aufwand', NULL, NULL, 1), -('4460', 'Reisekosten Arbeitnehmer', 'Aufwand', 'VSt19', '66', 1), -('4500', 'Werbekosten', 'Aufwand', 'VSt19', '66', 1), -('4510', 'Kosten der Warenabgabe (Muster, Proben)', 'Aufwand', 'VSt19', '66', 1), -('4520', 'Repräsentationskosten', 'Aufwand', 'VSt19', '66', 1), -('4530', 'Messekosten', 'Aufwand', 'VSt19', '66', 1), -('4540', 'Bewirtungskosten', 'Aufwand', 'VSt19', '66', 1), -('4560', 'Buchführungskosten', 'Aufwand', 'VSt19', '66', 1), -('4570', 'Jahresabschluss- und Prüfungskosten', 'Aufwand', 'VSt19', '66', 1), -('4580', 'Beratungskosten', 'Aufwand', 'VSt19', '66', 1), -('4600', 'Bürobedarf', 'Aufwand', 'VSt19', '66', 1), -('4610', 'Zeitschriften, Bücher', 'Aufwand', 'VSt7', '66', 1), -('4620', 'Portokosten', 'Aufwand', NULL, NULL, 1), -('4630', 'Telefon', 'Aufwand', 'VSt19', '66', 1), -('4640', 'Internetkosten', 'Aufwand', 'VSt19', '66', 1), -('4650', 'Büromaschinen (Wartung/Reparatur)', 'Aufwand', 'VSt19', '66', 1), -('4660', 'EDV-Kosten (Software, Lizenzen)', 'Aufwand', 'VSt19', '66', 1), -('4670', 'Betriebliche Fortbildungskosten', 'Aufwand', 'VSt19', '66', 1), -('4680', 'Fachliteratur', 'Aufwand', 'VSt7', '66', 1), -('4700', 'Abschreibungen auf Anlagevermögen', 'Aufwand', NULL, NULL, 1), -('4710', 'Abschreibungen auf GWG', 'Aufwand', NULL, NULL, 1), -('4800', 'Bankgebühren', 'Aufwand', NULL, NULL, 1), -('4820', 'Zinsen und ähnliche Aufwendungen', 'Aufwand', NULL, NULL, 1), -('4830', 'Mahngebühren, Inkassokosten', 'Aufwand', NULL, NULL, 1), -('4900', 'Sonstige betriebliche Aufwendungen', 'Aufwand', 'VSt19', '66', 1), -('4920', 'Gewerbesteuer', 'Aufwand', NULL, NULL, 1), -('4930', 'Kraftfahrzeugsteuer (betrieblich)', 'Aufwand', NULL, NULL, 1), -('4950', 'Sonstige Steuern', 'Aufwand', NULL, NULL, 1), -('4960', 'Sonstige betriebliche Aufwendungen (ohne VSt)', 'Aufwand', NULL, NULL, 1), -('4970', 'Aufwendungen aus Verlust Forderungen', 'Aufwand', NULL, NULL, 1), +('4100', 'Gehälter', 'Betriebsausgabe', NULL, NULL, 1), +('4110', 'Löhne', 'Betriebsausgabe', NULL, NULL, 1), +('4120', 'Gesetzliche soziale Aufwendungen', 'Betriebsausgabe', NULL, NULL, 1), +('4130', 'Freiwillige soziale Aufwendungen', 'Betriebsausgabe', NULL, NULL, 1), +('4140', 'Aufwendungen für Altersversorgung', 'Betriebsausgabe', NULL, NULL, 1), +('4200', 'Raumkosten (Miete)', 'Betriebsausgabe', 'VSt19', '66', 1), +('4210', 'Heizung', 'Betriebsausgabe', 'VSt19', '66', 1), +('4240', 'Gas, Strom, Wasser', 'Betriebsausgabe', 'VSt19', '66', 1), +('4250', 'Reinigung', 'Betriebsausgabe', 'VSt19', '66', 1), +('4260', 'Gebäudeversicherungen', 'Betriebsausgabe', NULL, NULL, 1), +('4300', 'Betriebliche Versicherungen', 'Betriebsausgabe', NULL, NULL, 1), +('4320', 'Beiträge', 'Betriebsausgabe', NULL, NULL, 1), +('4330', 'Berufsgenossenschaft', 'Betriebsausgabe', NULL, NULL, 1), +('4360', 'Beiträge zu Berufsverbänden', 'Betriebsausgabe', NULL, NULL, 1), +('4380', 'Verwaltungs-Gemeinkosten', 'Betriebsausgabe', 'VSt19', '66', 1), +('4390', 'Aufwand für Arbeitnehmer-Überlassung', 'Betriebsausgabe', 'VSt19', '66', 1), +('4400', 'PKW-Kosten', 'Betriebsausgabe', 'VSt19', '66', 1), +('4410', 'LKW-Kosten', 'Betriebsausgabe', 'VSt19', '66', 1), +('4430', 'Kraftfahrzeugsteuer', 'Betriebsausgabe', NULL, NULL, 1), +('4460', 'Reisekosten Arbeitnehmer', 'Betriebsausgabe', 'VSt19', '66', 1), +('4500', 'Werbekosten', 'Betriebsausgabe', 'VSt19', '66', 1), +('4510', 'Kosten der Warenabgabe (Muster, Proben)', 'Betriebsausgabe', 'VSt19', '66', 1), +('4520', 'Repräsentationskosten', 'Betriebsausgabe', 'VSt19', '66', 1), +('4530', 'Messekosten', 'Betriebsausgabe', 'VSt19', '66', 1), +('4540', 'Bewirtungskosten', 'Betriebsausgabe', 'VSt19', '66', 1), +('4560', 'Buchführungskosten', 'Betriebsausgabe', 'VSt19', '66', 1), +('4570', 'Jahresabschluss- und Prüfungskosten', 'Betriebsausgabe', 'VSt19', '66', 1), +('4580', 'Beratungskosten', 'Betriebsausgabe', 'VSt19', '66', 1), +('4600', 'Bürobedarf', 'Betriebsausgabe', 'VSt19', '66', 1), +('4610', 'Zeitschriften, Bücher', 'Betriebsausgabe', 'VSt7', '66', 1), +('4620', 'Portokosten', 'Betriebsausgabe', NULL, NULL, 1), +('4630', 'Telefon', 'Betriebsausgabe', 'VSt19', '66', 1), +('4640', 'Internetkosten', 'Betriebsausgabe', 'VSt19', '66', 1), +('4650', 'Büromaschinen (Wartung/Reparatur)', 'Betriebsausgabe', 'VSt19', '66', 1), +('4660', 'EDV-Kosten (Software, Lizenzen)', 'Betriebsausgabe', 'VSt19', '66', 1), +('4670', 'Betriebliche Fortbildungskosten', 'Betriebsausgabe', 'VSt19', '66', 1), +('4680', 'Fachliteratur', 'Betriebsausgabe', 'VSt7', '66', 1), +('4700', 'Abschreibungen auf Anlagevermögen', 'Betriebsausgabe', NULL, NULL, 1), +('4710', 'Abschreibungen auf GWG', 'Betriebsausgabe', NULL, NULL, 1), +('4800', 'Bankgebühren', 'Betriebsausgabe', NULL, NULL, 1), +('4820', 'Zinsen und ähnliche Aufwendungen', 'Betriebsausgabe', NULL, NULL, 1), +('4830', 'Mahngebühren, Inkassokosten', 'Betriebsausgabe', NULL, NULL, 1), +('4900', 'Sonstige betriebliche Aufwendungen', 'Betriebsausgabe', 'VSt19', '66', 1), +('4920', 'Gewerbesteuer', 'Betriebsausgabe', NULL, NULL, 1), +('4930', 'Kraftfahrzeugsteuer (betrieblich)', 'Betriebsausgabe', NULL, NULL, 1), +('4950', 'Sonstige Steuern', 'Betriebsausgabe', NULL, NULL, 1), +('4960', 'Sonstige betriebliche Aufwendungen (ohne VSt)', 'Betriebsausgabe', NULL, NULL, 1), +('4970', 'Aufwendungen aus Verlust Forderungen', 'Betriebsausgabe', NULL, NULL, 1), -- ============================================================ -- KLASSE 8: Erträge -- ============================================================ -('8000', 'Erlöse aus Lieferungen und Leistungen 19 % USt', 'Ertrag', 'USt19', '81', 1), -('8100', 'Erlöse aus Lieferungen und Leistungen 7 % USt', 'Ertrag', 'USt7', '86', 1), -('8120', 'Erlöse aus Lieferungen und Leistungen (steuerfrei)', 'Ertrag', NULL, '35', 1), -('8125', 'Steuerfreie EU-Lieferungen', 'Ertrag', NULL, '41', 1), -('8130', 'Sonstige steuerfreie Umsätze', 'Ertrag', NULL, NULL, 1), -('8200', 'Erlöse aus sonstigen Leistungen 19 % USt', 'Ertrag', 'USt19', '81', 1), -('8300', 'Erlöse aus sonstigen Leistungen 7 % USt', 'Ertrag', 'USt7', '86', 1), -('8400', 'Erlöse aus Anlagenabgängen (steuerpflichtig)', 'Ertrag', 'USt19', '81', 1), -('8500', 'Zinsen und ähnliche Erträge', 'Ertrag', NULL, NULL, 1), -('8600', 'Erträge aus Beteiligungen', 'Ertrag', NULL, NULL, 1), -('8900', 'Sonstige betriebliche Erträge', 'Ertrag', NULL, NULL, 1), -('8910', 'Sonstige betriebliche Erträge (steuerfrei)', 'Ertrag', NULL, NULL, 1), -('8920', 'Erträge aus Auflösung Rückstellungen', 'Ertrag', NULL, NULL, 1), -('8940', 'Erträge aus Kursgewinnen', 'Ertrag', NULL, NULL, 1), +('8000', 'Erlöse aus Lieferungen und Leistungen 19 % USt', 'Betriebseinnahme', 'USt19', '81', 1), +('8100', 'Erlöse aus Lieferungen und Leistungen 7 % USt', 'Betriebseinnahme', 'USt7', '86', 1), +('8120', 'Erlöse aus Lieferungen und Leistungen (steuerfrei)', 'Betriebseinnahme', NULL, '35', 1), +('8125', 'Steuerfreie EU-Lieferungen', 'Betriebseinnahme', NULL, '41', 1), +('8130', 'Sonstige steuerfreie Umsätze', 'Betriebseinnahme', NULL, NULL, 1), +('8200', 'Erlöse aus sonstigen Leistungen 19 % USt', 'Betriebseinnahme', 'USt19', '81', 1), +('8300', 'Erlöse aus sonstigen Leistungen 7 % USt', 'Betriebseinnahme', 'USt7', '86', 1), +('8400', 'Erlöse aus Anlagenabgängen (steuerpflichtig)', 'Betriebseinnahme', 'USt19', '81', 1), +('8500', 'Zinsen und ähnliche Erträge', 'Betriebseinnahme', NULL, NULL, 1), +('8600', 'Erträge aus Beteiligungen', 'Betriebseinnahme', NULL, NULL, 1), +('8900', 'Sonstige betriebliche Erträge', 'Betriebseinnahme', NULL, NULL, 1), +('8910', 'Sonstige betriebliche Erträge (steuerfrei)', 'Betriebseinnahme', NULL, NULL, 1), +('8920', 'Erträge aus Auflösung Rückstellungen', 'Betriebseinnahme', NULL, NULL, 1), +('8940', 'Erträge aus Kursgewinnen', 'Betriebseinnahme', NULL, NULL, 1), -- ============================================================ -- KLASSE 9: Eigenkapital / Privat (Neutral) -- ============================================================ -('9000', 'Eigenkapital', 'Neutral', NULL, NULL, 1), -('9100', 'Privatentnahmen', 'Neutral', NULL, NULL, 1), -('9200', 'Privateinlagen', 'Neutral', NULL, NULL, 1), -('9300', 'Gewinnvortrag', 'Neutral', NULL, NULL, 1), -('9400', 'Verlustvortrag', 'Neutral', NULL, NULL, 1); +('9000', 'Eigenkapital', 'Privat', NULL, NULL, 1), +('9100', 'Privatentnahmen', 'Privat', NULL, NULL, 1), +('9200', 'Privateinlagen', 'Privat', NULL, NULL, 1), +('9300', 'Gewinnvortrag', 'Privat', NULL, NULL, 1), +('9400', 'Verlustvortrag', 'Privat', NULL, NULL, 1); diff --git a/public/static/js/app.js b/public/static/js/app.js index 85ed5ee..42dcf92 100644 --- a/public/static/js/app.js +++ b/public/static/js/app.js @@ -337,13 +337,13 @@ function transferKontoNr(select) { } document.addEventListener('dblclick', (e) => { - const select = e.target.closest('select#soll_konto_id, select#haben_konto_id'); + const select = e.target.closest('select#kategorie_konto_id, select#zahlungskonto_id'); if (select) transferKontoNr(select); }); document.addEventListener('keydown', (e) => { if (e.key !== 'Enter') return; - const select = e.target.closest('select#soll_konto_id, select#haben_konto_id'); + const select = e.target.closest('select#kategorie_konto_id, select#zahlungskonto_id'); if (select) { e.preventDefault(); transferKontoNr(select); diff --git a/src/Controllers/AccountController.php b/src/Controllers/AccountController.php index 891f629..e207983 100644 --- a/src/Controllers/AccountController.php +++ b/src/Controllers/AccountController.php @@ -19,24 +19,22 @@ class AccountController // Quick stats $stats = []; - // Umsatz (Ertrag-Konten) + // Einnahmen $umsatz = \Steuerkram\Database::fetchOne( 'SELECT COALESCE(SUM(bk.betrag_netto),0) as total FROM bookings bk - JOIN accounts a ON bk.haben_konto_id = a.id JOIN fiscal_years fy ON bk.fiscal_year_id = fy.id - WHERE a.kontenart = \'Ertrag\' AND fy.year = ?', + WHERE bk.buchungstyp = \'Einnahme\' AND bk.is_reversal = 0 AND fy.year = ?', [$year] ); $stats['umsatz'] = (float)($umsatz['total'] ?? 0); - // Ausgaben (Aufwand-Konten) + // Ausgaben $ausgaben = \Steuerkram\Database::fetchOne( 'SELECT COALESCE(SUM(bk.betrag_netto),0) as total FROM bookings bk - JOIN accounts a ON bk.soll_konto_id = a.id JOIN fiscal_years fy ON bk.fiscal_year_id = fy.id - WHERE a.kontenart = \'Aufwand\' AND fy.year = ?', + WHERE bk.buchungstyp = \'Ausgabe\' AND bk.is_reversal = 0 AND fy.year = ?', [$year] ); $stats['ausgaben'] = (float)($ausgaben['total'] ?? 0); @@ -59,12 +57,12 @@ class AccountController // Letzte Buchungen $recentBookings = \Steuerkram\Database::fetchAll( - 'SELECT bk.*, s.kontonummer as soll_nr, s.bezeichnung as soll_name, - h.kontonummer as haben_nr, h.bezeichnung as haben_name, + 'SELECT bk.*, k.kontonummer as kategorie_nr, k.bezeichnung as kategorie_name, + z.kontonummer as zahlungs_nr, z.bezeichnung as zahlungs_name, bl.beleg_nr FROM bookings bk - JOIN accounts s ON bk.soll_konto_id = s.id - JOIN accounts h ON bk.haben_konto_id = h.id + JOIN accounts k ON bk.kategorie_konto_id = k.id + JOIN accounts z ON bk.zahlungskonto_id = z.id JOIN belege bl ON bk.beleg_id = bl.id ORDER BY bk.created_at DESC LIMIT 10' ); diff --git a/src/Controllers/BookingController.php b/src/Controllers/BookingController.php index eb0d6a3..b73918a 100644 --- a/src/Controllers/BookingController.php +++ b/src/Controllers/BookingController.php @@ -55,8 +55,10 @@ class BookingController fprintf($out, chr(0xEF) . chr(0xBB) . chr(0xBF)); // UTF-8 BOM for Excel fputcsv($out, [ - 'Buchungsnr.', 'Datum', 'Belegnr.', 'Soll-Kto.', 'Soll-Bez.', - 'Haben-Kto.', 'Haben-Bez.', 'Netto (€)', 'USt (%)', 'USt (€)', + 'Buchungsnr.', 'Datum', 'Belegnr.', 'Typ', + 'Kategorie-Kto.', 'Kategorie-Bez.', + 'Zahlungskonto-Kto.', 'Zahlungskonto-Bez.', + 'Netto (€)', 'USt (%)', 'USt (€)', 'Brutto (€)', 'Buchungstext', 'Storno' ], ';'); @@ -65,10 +67,11 @@ class BookingController $this->csvSanitize((string)$row['buchungs_nr']), $row['buchungs_datum'], $this->csvSanitize((string)$row['beleg_nr']), - $row['soll_nr'], - $this->csvSanitize((string)$row['soll_name']), - $row['haben_nr'], - $this->csvSanitize((string)$row['haben_name']), + $row['buchungstyp'], + $row['kategorie_nr'], + $this->csvSanitize((string)$row['kategorie_name']), + $row['zahlungs_nr'], + $this->csvSanitize((string)$row['zahlungs_name']), number_format((float)$row['betrag_netto'], 2, ',', ''), number_format((float)$row['ust_satz'], 2, ',', ''), number_format((float)$row['betrag_ust'], 2, ',', ''), @@ -87,15 +90,16 @@ class BookingController $errors = []; // Validate - if (empty($data['soll_konto_id'])) $errors['soll_konto_id'] = 'Pflichtfeld'; - if (empty($data['haben_konto_id'])) $errors['haben_konto_id'] = 'Pflichtfeld'; - if (empty($data['buchungs_datum'])) $errors['buchungs_datum'] = 'Pflichtfeld'; + if (empty($data['buchungstyp'])) $errors['buchungstyp'] = 'Pflichtfeld'; + if (empty($data['kategorie_konto_id'])) $errors['kategorie_konto_id'] = 'Pflichtfeld'; + if (empty($data['zahlungskonto_id'])) $errors['zahlungskonto_id'] = 'Pflichtfeld'; + if (empty($data['buchungs_datum'])) $errors['buchungs_datum'] = 'Pflichtfeld'; if (!isset($data['betrag_netto']) || $data['betrag_netto'] === '') $errors['betrag_netto'] = 'Pflichtfeld'; - if (empty($data['buchungstext'])) $errors['buchungstext'] = 'Pflichtfeld'; + if (empty($data['buchungstext'])) $errors['buchungstext'] = 'Pflichtfeld'; - if (empty($errors) && $data['soll_konto_id'] === $data['haben_konto_id']) { - $errors['haben_konto_id'] = 'Soll- und Habenkonto dürfen nicht identisch sein.'; + if (empty($errors) && $data['kategorie_konto_id'] === $data['zahlungskonto_id']) { + $errors['zahlungskonto_id'] = 'Kategorie- und Zahlungskonto dürfen nicht identisch sein.'; } if ($errors) { diff --git a/src/Controllers/InvoiceController.php b/src/Controllers/InvoiceController.php index 96b3349..5a0f26f 100644 --- a/src/Controllers/InvoiceController.php +++ b/src/Controllers/InvoiceController.php @@ -246,11 +246,11 @@ class InvoiceController 'notes' => 'Automatisch erstellt aus Rechnung ' . $invoice['rechnungs_nr'], ]); - $debitorKonto = \Steuerkram\Database::fetchOne( + $zahlungskonto = \Steuerkram\Database::fetchOne( 'SELECT id FROM accounts WHERE kontonummer = ?', ['1400'] ); - if (!$debitorKonto) { - throw new \RuntimeException('Debitorenkonto 1400 nicht gefunden.'); + if (!$zahlungskonto) { + throw new \RuntimeException('Forderungskonto 1400 nicht gefunden.'); } $erloesKonten = []; @@ -270,24 +270,25 @@ class InvoiceController foreach ($gruppenNetto as $satz => $netto) { if (abs($satz - 19) < 0.01) { - $habenId = $erloesKonten['8000'] ?? null; + $kategorieId = $erloesKonten['8000'] ?? null; } elseif (abs($satz - 7) < 0.01) { - $habenId = $erloesKonten['8200'] ?? null; + $kategorieId = $erloesKonten['8200'] ?? null; } else { - $habenId = $erloesKonten['8400'] ?? null; + $kategorieId = $erloesKonten['8400'] ?? null; } - if (!$habenId) { + if (!$kategorieId) { throw new \RuntimeException("Erlöskonto für USt-Satz {$satz}% nicht gefunden."); } Booking::create([ - 'beleg_id' => $belegId, - 'buchungs_datum' => $invoice['rechnungs_datum'], - 'soll_konto_id' => $debitorKonto['id'], - 'haben_konto_id' => $habenId, - 'betrag_netto' => round($netto, 2), - 'ust_satz' => $satz, - 'buchungstext' => 'Ausgangsrechnung ' . $invoice['rechnungs_nr'], + 'beleg_id' => $belegId, + 'buchungs_datum' => $invoice['rechnungs_datum'], + 'buchungstyp' => 'Einnahme', + 'kategorie_konto_id' => $kategorieId, + 'zahlungskonto_id' => $zahlungskonto['id'], + 'betrag_netto' => round($netto, 2), + 'ust_satz' => $satz, + 'buchungstext' => 'Ausgangsrechnung ' . $invoice['rechnungs_nr'], ]); } diff --git a/src/Models/Account.php b/src/Models/Account.php index cec893b..b27a785 100644 --- a/src/Models/Account.php +++ b/src/Models/Account.php @@ -90,7 +90,7 @@ class Account { // Check if any bookings reference this account $used = Database::fetchOne( - 'SELECT COUNT(*) as cnt FROM bookings WHERE soll_konto_id = ? OR haben_konto_id = ?', + 'SELECT COUNT(*) as cnt FROM bookings WHERE kategorie_konto_id = ? OR zahlungskonto_id = ?', [$id, $id] ); if ($used && $used['cnt'] > 0) { diff --git a/src/Models/Booking.php b/src/Models/Booking.php index 46003e0..f336305 100644 --- a/src/Models/Booking.php +++ b/src/Models/Booking.php @@ -12,11 +12,11 @@ class Booking { return Database::fetchAll( 'SELECT bk.*, - s.kontonummer as soll_nr, s.bezeichnung as soll_name, - h.kontonummer as haben_nr, h.bezeichnung as haben_name + k.kontonummer as kategorie_nr, k.bezeichnung as kategorie_name, + z.kontonummer as zahlungs_nr, z.bezeichnung as zahlungs_name FROM bookings bk - JOIN accounts s ON bk.soll_konto_id = s.id - JOIN accounts h ON bk.haben_konto_id = h.id + JOIN accounts k ON bk.kategorie_konto_id = k.id + JOIN accounts z ON bk.zahlungskonto_id = z.id WHERE bk.beleg_id = ? ORDER BY bk.buchungs_datum, bk.id', [$belegId] @@ -37,7 +37,7 @@ class Booking $params[] = (int)$filter['month']; } if (!empty($filter['konto'])) { - $where[] = '(bk.soll_konto_id = ? OR bk.haben_konto_id = ?)'; + $where[] = '(bk.kategorie_konto_id = ? OR bk.zahlungskonto_id = ?)'; $params[] = (int)$filter['konto']; $params[] = (int)$filter['konto']; } @@ -48,12 +48,12 @@ class Booking $offset = ($page - 1) * $perPage; $sql = 'SELECT bk.*, - s.kontonummer as soll_nr, s.bezeichnung as soll_name, - h.kontonummer as haben_nr, h.bezeichnung as haben_name, + k.kontonummer as kategorie_nr, k.bezeichnung as kategorie_name, + z.kontonummer as zahlungs_nr, z.bezeichnung as zahlungs_name, bl.beleg_nr, fy.year as fiscal_year_nr FROM bookings bk - JOIN accounts s ON bk.soll_konto_id = s.id - JOIN accounts h ON bk.haben_konto_id = h.id + JOIN accounts k ON bk.kategorie_konto_id = k.id + JOIN accounts z ON bk.zahlungskonto_id = z.id JOIN belege bl ON bk.beleg_id = bl.id JOIN fiscal_years fy ON bk.fiscal_year_id = fy.id WHERE ' . implode(' AND ', $where) . ' @@ -97,15 +97,17 @@ class Booking $nr = self::generateBuchungsNr(); Database::execute( - 'INSERT INTO bookings (buchungs_nr, beleg_id, buchungs_datum, soll_konto_id, haben_konto_id, + 'INSERT INTO bookings (buchungs_nr, beleg_id, buchungs_datum, buchungstyp, + kategorie_konto_id, zahlungskonto_id, betrag_netto, ust_satz, betrag_ust, betrag_brutto, buchungstext, fiscal_year_id) - VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)', + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)', [ $nr, (int)$data['beleg_id'], $data['buchungs_datum'], - (int)$data['soll_konto_id'], - (int)$data['haben_konto_id'], + $data['buchungstyp'], + (int)$data['kategorie_konto_id'], + (int)$data['zahlungskonto_id'], $netto, (float)($data['ust_satz'] ?? 0), $ust, @@ -129,15 +131,15 @@ class Booking if (!$orig) throw new \RuntimeException('Buchung nicht gefunden.'); if ($orig['is_reversal']) throw new \RuntimeException('Stornobuchungen können nicht storniert werden.'); - // Create reversal: swap Soll/Haben, same amounts $reversalData = [ - 'beleg_id' => $orig['beleg_id'], - 'buchungs_datum' => date('Y-m-d'), - 'soll_konto_id' => $orig['haben_konto_id'], - 'haben_konto_id' => $orig['soll_konto_id'], - 'betrag_netto' => $orig['betrag_netto'], - 'ust_satz' => $orig['ust_satz'], - 'buchungstext' => 'STORNO zu ' . $orig['buchungs_nr'] . ': ' . $reason, + 'beleg_id' => $orig['beleg_id'], + 'buchungs_datum' => date('Y-m-d'), + 'buchungstyp' => $orig['buchungstyp'], + 'kategorie_konto_id' => $orig['kategorie_konto_id'], + 'zahlungskonto_id' => $orig['zahlungskonto_id'], + 'betrag_netto' => $orig['betrag_netto'], + 'ust_satz' => $orig['ust_satz'], + 'buchungstext' => 'STORNO zu ' . $orig['buchungs_nr'] . ': ' . $reason, ]; $year = (int)date('Y', strtotime($reversalData['buchungs_datum'])); @@ -146,15 +148,17 @@ class Booking $nr = self::generateBuchungsNr(); Database::execute( - 'INSERT INTO bookings (buchungs_nr, beleg_id, buchungs_datum, soll_konto_id, haben_konto_id, + 'INSERT INTO bookings (buchungs_nr, beleg_id, buchungs_datum, buchungstyp, + kategorie_konto_id, zahlungskonto_id, betrag_netto, ust_satz, betrag_ust, betrag_brutto, buchungstext, is_reversal, fiscal_year_id) - VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 1, ?)', + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 1, ?)', [ $nr, $reversalData['beleg_id'], $reversalData['buchungs_datum'], - $reversalData['soll_konto_id'], - $reversalData['haben_konto_id'], + $reversalData['buchungstyp'], + $reversalData['kategorie_konto_id'], + $reversalData['zahlungskonto_id'], $orig['betrag_netto'], $orig['ust_satz'], $orig['betrag_ust'], @@ -186,14 +190,14 @@ class Booking public static function sumByYear(int $year): array { return Database::fetchAll( - 'SELECT bk.soll_konto_id, bk.haben_konto_id, + 'SELECT bk.buchungstyp, bk.kategorie_konto_id, bk.zahlungskonto_id, SUM(bk.betrag_netto) as sum_netto, SUM(bk.betrag_ust) as sum_ust, SUM(bk.betrag_brutto) as sum_brutto FROM bookings bk JOIN fiscal_years fy ON bk.fiscal_year_id = fy.id WHERE fy.year = ? - GROUP BY bk.soll_konto_id, bk.haben_konto_id', + GROUP BY bk.buchungstyp, bk.kategorie_konto_id, bk.zahlungskonto_id', [$year] ); } diff --git a/src/Services/EuerCalculator.php b/src/Services/EuerCalculator.php index e5f2ad3..e1bfbd0 100644 --- a/src/Services/EuerCalculator.php +++ b/src/Services/EuerCalculator.php @@ -8,7 +8,7 @@ use Steuerkram\Database; /** * Berechnet die Einnahmen-Überschuss-Rechnung (EÜR) für ein Geschäftsjahr. - * Gruppiert alle Buchungen nach Konten und summiert Einnahmen / Ausgaben. + * Gruppiert alle Buchungen nach buchungstyp und Kategorie-Konto. */ class EuerCalculator { @@ -20,14 +20,14 @@ class EuerCalculator a.kontonummer, a.bezeichnung, a.kontenart, - SUM(CASE WHEN bk.haben_konto_id = a.id THEN bk.betrag_netto ELSE 0 END) as haben_summe, - SUM(CASE WHEN bk.soll_konto_id = a.id THEN bk.betrag_netto ELSE 0 END) as soll_summe, + bk.buchungstyp, + SUM(bk.betrag_netto) as summe_netto, COUNT(bk.id) as buchungsanzahl - FROM accounts a - JOIN bookings bk ON (bk.haben_konto_id = a.id OR bk.soll_konto_id = a.id) + FROM bookings bk + JOIN accounts a ON bk.kategorie_konto_id = a.id JOIN fiscal_years fy ON bk.fiscal_year_id = fy.id WHERE fy.year = ? AND bk.is_reversal = 0 - GROUP BY a.id, a.kontonummer, a.bezeichnung, a.kontenart + GROUP BY a.id, a.kontonummer, a.bezeichnung, a.kontenart, bk.buchungstyp HAVING buchungsanzahl > 0 ORDER BY a.kontonummer', [$year] @@ -37,12 +37,12 @@ class EuerCalculator $ausgaben = []; foreach ($rows as $row) { - $nettoWert = round((float)$row['haben_summe'] - (float)$row['soll_summe'], 2); + $netto = round(abs((float)$row['summe_netto']), 2); - if ($row['kontenart'] === 'Ertrag') { - $einnahmen[] = array_merge($row, ['netto' => abs($nettoWert)]); - } elseif ($row['kontenart'] === 'Aufwand') { - $ausgaben[] = array_merge($row, ['netto' => abs($nettoWert)]); + if ($row['buchungstyp'] === 'Einnahme') { + $einnahmen[] = array_merge($row, ['netto' => $netto]); + } elseif ($row['buchungstyp'] === 'Ausgabe') { + $ausgaben[] = array_merge($row, ['netto' => $netto]); } } diff --git a/src/Services/UstvaCalculator.php b/src/Services/UstvaCalculator.php index d74cace..478be4b 100644 --- a/src/Services/UstvaCalculator.php +++ b/src/Services/UstvaCalculator.php @@ -9,15 +9,12 @@ use Steuerkram\Database; /** * Berechnet alle ELSTER-Kennzahlen (Kz) für die Umsatzsteuervoranmeldung. * - * Maßgeblich sind ausschließlich ust_satz und betrag_ust der Buchung selbst – - * nicht der steuercode des gebuchten Kontos. Dadurch werden Buchungen mit - * ust_satz=0 (z.B. innergemeinschaftliche Erwerbe, steuerfreie Leistungen) - * korrekt mit 0 Vorsteuer/USt gewertet. + * Maßgeblich sind buchungstyp, ust_satz und betrag_ust der Buchung selbst. * * Buchungslogik: - * Umsatzsteuer (Kz 81/86): ust_satz > 0 auf Ertrag-Haben-Konten - * Vorsteuer (Kz 61/62): ust_satz > 0 auf Aufwand/Aktiv-Soll-Konten - * Steuerfreie Umsätze (Kz 35): Ertrag-Konten mit ust_satz = 0 + * Umsatzsteuer (Kz 81/86): buchungstyp = 'Einnahme' mit ust_satz > 0 + * Vorsteuer (Kz 61/62): buchungstyp = 'Ausgabe' mit ust_satz > 0 + * Steuerfreie Umsätze (Kz 35): buchungstyp = 'Einnahme' mit ust_satz = 0 */ class UstvaCalculator { @@ -39,15 +36,13 @@ class UstvaCalculator $bookings = $this->fetchBookings($year, $month); foreach ($bookings as $b) { - $ustSatz = (float)$b['ust_satz']; - $betragUst = abs((float)$b['betrag_ust']); - $betragNetto= abs((float)$b['betrag_netto']); - $habenArt = $b['haben_kontenart'] ?? ''; - $sollArt = $b['soll_kontenart'] ?? ''; + $ustSatz = (float)$b['ust_satz']; + $betragUst = abs((float)$b['betrag_ust']); + $betragNetto = abs((float)$b['betrag_netto']); + $buchungstyp = $b['buchungstyp']; - // ── Umsatzsteuer (Ausgangsseite) ────────────────────────────── - // Kriterium: Haben-Konto = Ertrag UND ust_satz > 0 - if ($habenArt === 'Ertrag' && $ustSatz > 0) { + // ── Umsatzsteuer (Einnahmen) ───────────────────────────── + if ($buchungstyp === 'Einnahme' && $ustSatz > 0) { if (abs($ustSatz - 19) < 0.01) { $kz['81']['betrag'] += $betragNetto; } elseif (abs($ustSatz - 7) < 0.01) { @@ -55,16 +50,13 @@ class UstvaCalculator } } - // Steuerfreie Umsätze: Haben-Konto = Ertrag UND ust_satz = 0 - if ($habenArt === 'Ertrag' && $ustSatz == 0) { + // Steuerfreie Umsätze: Einnahme mit ust_satz = 0 + if ($buchungstyp === 'Einnahme' && $ustSatz == 0) { $kz['35']['betrag'] += $betragNetto; } - // ── Vorsteuer (Eingangsseite) ────────────────────────────────── - // Kriterium: Soll-Konto = Aufwand oder Aktiv (Vorsteuerkonto) - // UND ust_satz > 0 UND betrag_ust > 0 - // Buchungen mit ust_satz=0 (z.B. innergemeinschaftlich) → kein Eintrag - if (in_array($sollArt, ['Aufwand', 'Aktiv']) && $ustSatz > 0 && $betragUst > 0) { + // ── Vorsteuer (Ausgaben) ───────────────────────────────── + if ($buchungstyp === 'Ausgabe' && $ustSatz > 0 && $betragUst > 0) { if (abs($ustSatz - 19) < 0.01) { $kz['61']['steuer'] += $betragUst; } elseif (abs($ustSatz - 7) < 0.01) { @@ -92,12 +84,8 @@ class UstvaCalculator private function fetchBookings(int $year, int $month): array { return Database::fetchAll( - 'SELECT bk.ust_satz, bk.betrag_netto, bk.betrag_ust, - s.kontenart AS soll_kontenart, - h.kontenart AS haben_kontenart + 'SELECT bk.buchungstyp, bk.ust_satz, bk.betrag_netto, bk.betrag_ust FROM bookings bk - JOIN accounts s ON bk.soll_konto_id = s.id - JOIN accounts h ON bk.haben_konto_id = h.id JOIN fiscal_years fy ON bk.fiscal_year_id = fy.id WHERE fy.year = ? AND MONTH(bk.buchungs_datum) = ? AND bk.is_reversal = 0', diff --git a/src/Views/accounts/form.php b/src/Views/accounts/form.php index ba07985..edad660 100644 --- a/src/Views/accounts/form.php +++ b/src/Views/accounts/form.php @@ -22,7 +22,7 @@ - + @@ -44,11 +44,11 @@ 'badge-green', - 'Aufwand' => 'badge-red', - 'Aktiv' => 'badge-blue', - 'Passiv' => 'badge-yellow', - default => 'badge-gray', + 'Betriebseinnahme' => 'badge-green', + 'Betriebsausgabe' => 'badge-red', + 'Zahlungskonto' => 'badge-blue', + 'Verbindlichkeit' => 'badge-yellow', + default => 'badge-gray', }; ?> diff --git a/src/Views/belege/detail.php b/src/Views/belege/detail.php index 9c36c9b..d2522a5 100644 --- a/src/Views/belege/detail.php +++ b/src/Views/belege/detail.php @@ -149,8 +149,9 @@ $testMode = Setting::isTestMode(); Buchungsnr. Datum - Soll - Haben + Typ + Kategorie + Zahlungskonto Netto USt Brutto @@ -166,8 +167,9 @@ $testMode = Setting::isTestMode(); -
-
+ +
+
% @@ -220,7 +222,7 @@ $testMode = Setting::isTestMode(); - Noch keine Buchungen. + Noch keine Buchungen. @@ -232,6 +234,17 @@ $testMode = Setting::isTestMode();
@@ -249,6 +262,25 @@ $testMode = Setting::isTestMode();
+ +
+ +
+ + +
+
+
@@ -266,28 +298,19 @@ $testMode = Setting::isTestMode();
- - $kontenGruppen = []; - foreach ($accounts as $acc) { - $kontenGruppen[$acc['kontenart']][] = $acc; - } - $gruppenReihenfolge = ['Aufwand', 'Ertrag', 'Aktiv', 'Passiv', 'Neutral']; - ?> - - +
-
- +
-
@@ -360,3 +381,30 @@ $testMode = Setting::isTestMode();
+ diff --git a/src/Views/bookings/journal.php b/src/Views/bookings/journal.php index d7837d4..030c87b 100644 --- a/src/Views/bookings/journal.php +++ b/src/Views/bookings/journal.php @@ -42,8 +42,9 @@ Buchungsnr. Datum Belegnr. - Soll-Konto - Haben-Konto + Typ + Kategorie + Zahlungskonto Netto USt (%) Brutto @@ -63,13 +64,14 @@ + -
- +
+ -
- +
+ % @@ -93,7 +95,7 @@ - Keine Buchungen gefunden. + Keine Buchungen gefunden. diff --git a/src/Views/dashboard.php b/src/Views/dashboard.php index 2d26440..44e20c9 100644 --- a/src/Views/dashboard.php +++ b/src/Views/dashboard.php @@ -151,8 +151,9 @@ $fmt = fn($v) => View::formatMoney($v); Datum Belegnr. - Soll - Haben + Typ + Kategorie + Zahlungskonto Buchungstext Brutto @@ -162,14 +163,15 @@ $fmt = fn($v) => View::formatMoney($v); - – - – + + – + – Storno - Noch keine Buchungen vorhanden. + Noch keine Buchungen vorhanden. diff --git a/tests/02_BelegBuchungTest.php b/tests/02_BelegBuchungTest.php index 9575330..7e82bcc 100644 --- a/tests/02_BelegBuchungTest.php +++ b/tests/02_BelegBuchungTest.php @@ -15,26 +15,26 @@ class BelegBuchungTest extends TestCase public function run(): void { // Konten aus der SKR03-Seed-Datei abrufen - $aufwandId = $this->findAccountId('4920'); // Bürobedarf (Aufwand) - $bankId = $this->findAccountId('1200'); // Bank (Aktiv) + $ausgabeId = $this->findAccountId('4920'); // Bürobedarf (Betriebsausgabe) + $bankId = $this->findAccountId('1200'); // Bank (Zahlungskonto) - if (!$aufwandId || !$bankId) { - // Fallback: irgendein Aufwand- und Aktiv-Konto suchen - $aufwandRow = \Steuerkram\Database::fetchOne( - "SELECT id FROM accounts WHERE kontenart = 'Aufwand' AND aktiv = 1 LIMIT 1" + if (!$ausgabeId || !$bankId) { + // Fallback: irgendein Betriebsausgabe- und Zahlungskonto suchen + $ausgabeRow = \Steuerkram\Database::fetchOne( + "SELECT id FROM accounts WHERE kontenart = 'Betriebsausgabe' AND aktiv = 1 LIMIT 1" ); $bankRow = \Steuerkram\Database::fetchOne( - "SELECT id FROM accounts WHERE kontenart = 'Aktiv' AND aktiv = 1 LIMIT 1" + "SELECT id FROM accounts WHERE kontenart = 'Zahlungskonto' AND aktiv = 1 LIMIT 1" ); - $aufwandId = $aufwandRow ? (int)$aufwandRow['id'] : null; + $ausgabeId = $ausgabeRow ? (int)$ausgabeRow['id'] : null; $bankId = $bankRow ? (int)$bankRow['id'] : null; } - $this->assertNotEmpty($aufwandId, 'Aufwandskonto gefunden'); - $this->assertNotEmpty($bankId, 'Aktiv-/Bankkonto gefunden'); + $this->assertNotEmpty($ausgabeId, 'Betriebsausgabe-Konto gefunden'); + $this->assertNotEmpty($bankId, 'Zahlungskonto gefunden'); - if (!$aufwandId || !$bankId) { - return; // Voraussetzungen nicht erfüllt, weitere Asserts wären sinnlos + if (!$ausgabeId || !$bankId) { + return; } // Beleg anlegen @@ -54,15 +54,16 @@ class BelegBuchungTest extends TestCase ]); $this->assertGreaterThan(0, $belegId, 'Beleg für Buchungstest angelegt'); - // Buchung erstellen + // Buchung erstellen (EÜR: Ausgabe mit Kategorie + Zahlungskonto) $buchungId = \Steuerkram\Models\Booking::create([ - 'beleg_id' => $belegId, - 'buchungs_datum' => $datum, - 'soll_konto_id' => $aufwandId, - 'haben_konto_id' => $bankId, - 'betrag_netto' => 50.00, - 'ust_satz' => 19.0, - 'buchungstext' => 'Testbuchung Bürobedarf', + 'beleg_id' => $belegId, + 'buchungs_datum' => $datum, + 'buchungstyp' => 'Ausgabe', + 'kategorie_konto_id' => $ausgabeId, + 'zahlungskonto_id' => $bankId, + 'betrag_netto' => 50.00, + 'ust_satz' => 19.0, + 'buchungstext' => 'Testbuchung Bürobedarf', ]); $this->assertGreaterThan(0, $buchungId, 'Buchung wurde angelegt (ID > 0)'); @@ -71,6 +72,7 @@ class BelegBuchungTest extends TestCase $this->assertNotEmpty($buchung, 'Booking::find() liefert die Buchung'); $this->assertEquals(50.00, (float)($buchung['betrag_netto'] ?? 0), 'Nettobetrag korrekt'); $this->assertEquals(19.0, (float)($buchung['ust_satz'] ?? 0), 'USt-Satz korrekt'); + $this->assertEquals('Ausgabe', $buchung['buchungstyp'] ?? '', 'Buchungstyp ist Ausgabe'); // Buchung via Booking::forBeleg() abrufbar $buchungen = \Steuerkram\Models\Booking::forBeleg($belegId); @@ -93,11 +95,11 @@ class BelegBuchungTest extends TestCase foreach ($journal['rows'] as $row) { if ((int)$row['id'] === $buchungId) { $foundInJournal = true; - // Vollständigkeit prüfen - $this->assertNotEmpty($row['buchungs_nr'], 'Buchungsnummer im Journal vorhanden'); - $this->assertNotEmpty($row['soll_nr'], 'Soll-Kontonummer im Journal vorhanden'); - $this->assertNotEmpty($row['haben_nr'], 'Haben-Kontonummer im Journal vorhanden'); - $this->assertNotEmpty($row['beleg_nr'], 'Belegnummer im Journal vorhanden'); + $this->assertNotEmpty($row['buchungs_nr'], 'Buchungsnummer im Journal vorhanden'); + $this->assertNotEmpty($row['kategorie_nr'], 'Kategorie-Kontonummer im Journal vorhanden'); + $this->assertNotEmpty($row['zahlungs_nr'], 'Zahlungskonto-Nr. im Journal vorhanden'); + $this->assertNotEmpty($row['beleg_nr'], 'Belegnummer im Journal vorhanden'); + $this->assertEquals('Ausgabe', $row['buchungstyp'], 'Buchungstyp im Journal korrekt'); break; } } diff --git a/tests/04_InvoiceBuchungTest.php b/tests/04_InvoiceBuchungTest.php index 259b58d..f3b098c 100644 --- a/tests/04_InvoiceBuchungTest.php +++ b/tests/04_InvoiceBuchungTest.php @@ -16,14 +16,14 @@ class InvoiceBuchungTest extends TestCase public function run(): void { // Benötigte Konten prüfen (SKR03) - $debitorId = $this->findAccountId('1400'); // Debitorenkonto - $erloesId = $this->findAccountId('8000'); // Erlöse 19 % + $zahlungsId = $this->findAccountId('1400'); // Forderungen (Zahlungskonto) + $kategorieId = $this->findAccountId('8000'); // Erlöse 19 % (Betriebseinnahme) - if (!$debitorId) { - $this->assert(false, 'Debitorenkonto 1400 nicht gefunden – SKR03-Seed vorhanden?'); + if (!$zahlungsId) { + $this->assert(false, 'Forderungskonto 1400 nicht gefunden – SKR03-Seed vorhanden?'); return; } - if (!$erloesId) { + if (!$kategorieId) { $this->assert(false, 'Erlöskonto 8000 nicht gefunden – SKR03-Seed vorhanden?'); return; } @@ -65,7 +65,7 @@ class InvoiceBuchungTest extends TestCase $invoice = \Steuerkram\Models\Invoice::find($invoiceId); $this->assertEquals('Gesendet', $invoice['status'] ?? null, 'Rechnungsstatus ist Gesendet'); - // Buchungen erzeugen (wie InvoiceController::createBelegAndBookings) + // Buchungen erzeugen (EÜR: Einnahme) $belegNr = \Steuerkram\Models\Beleg::generateBelegNr(); $belegId = \Steuerkram\Models\Beleg::create([ 'beleg_nr' => $belegNr, @@ -91,13 +91,14 @@ class InvoiceBuchungTest extends TestCase $buchungIds = []; foreach ($gruppenNetto as $satz => $netto) { $buchungIds[] = \Steuerkram\Models\Booking::create([ - 'beleg_id' => $belegId, - 'buchungs_datum' => $invoice['rechnungs_datum'], - 'soll_konto_id' => $debitorId, - 'haben_konto_id' => $erloesId, - 'betrag_netto' => round($netto, 2), - 'ust_satz' => $satz, - 'buchungstext' => 'Ausgangsrechnung ' . $nr, + 'beleg_id' => $belegId, + 'buchungs_datum' => $invoice['rechnungs_datum'], + 'buchungstyp' => 'Einnahme', + 'kategorie_konto_id' => $kategorieId, + 'zahlungskonto_id' => $zahlungsId, + 'betrag_netto' => round($netto, 2), + 'ust_satz' => $satz, + 'buchungstext' => 'Ausgangsrechnung ' . $nr, ]); } @@ -124,9 +125,10 @@ class InvoiceBuchungTest extends TestCase $this->assertEquals(19.0, (float)$row['ust_satz'], 'USt-Satz 19 % korrekt'); $this->assertEquals(190.00, (float)$row['betrag_ust'], 'USt-Betrag 190 € korrekt'); $this->assertEquals(1190.00, (float)$row['betrag_brutto'], 'Bruttobetrag 1190 € korrekt'); - $this->assertNotEmpty($row['beleg_nr'], 'Belegnummer im Journal vorhanden'); - $this->assertNotEmpty($row['soll_nr'], 'Soll-Konto im Journal vorhanden'); - $this->assertNotEmpty($row['haben_nr'], 'Haben-Konto im Journal vorhanden'); + $this->assertNotEmpty($row['beleg_nr'], 'Belegnummer im Journal vorhanden'); + $this->assertNotEmpty($row['kategorie_nr'], 'Kategorie-Konto im Journal vorhanden'); + $this->assertNotEmpty($row['zahlungs_nr'], 'Zahlungskonto im Journal vorhanden'); + $this->assertEquals('Einnahme', $row['buchungstyp'], 'Buchungstyp ist Einnahme'); break; } } diff --git a/tests/05_TaxSummenTest.php b/tests/05_TaxSummenTest.php index 4b78ab9..9ca8afd 100644 --- a/tests/05_TaxSummenTest.php +++ b/tests/05_TaxSummenTest.php @@ -19,46 +19,46 @@ class TaxSummenTest extends TestCase $datum = date('Y-m-d'); // Benötigte Konten (SKR03) - $debitorId = $this->findAccountId('1400'); // Forderungen (Aktiv) - $erloesId = $this->findAccountId('8000'); // Erlöse 19 % (Ertrag) - $aufwandId = $this->findAccountId('4920'); // Bürobedarf (Aufwand) - $bankId = $this->findAccountId('1200'); // Bank (Aktiv) + $zahlungsId = $this->findAccountId('1400'); // Forderungen (Zahlungskonto) + $erloesId = $this->findAccountId('8000'); // Erlöse 19 % (Betriebseinnahme) + $ausgabeId = $this->findAccountId('4920'); // Bürobedarf (Betriebsausgabe) + $bankId = $this->findAccountId('1200'); // Bank (Zahlungskonto) // Fallback auf beliebige Konten der passenden Art if (!$erloesId) { $row = \Steuerkram\Database::fetchOne( - "SELECT id FROM accounts WHERE kontenart = 'Ertrag' AND aktiv = 1 LIMIT 1" + "SELECT id FROM accounts WHERE kontenart = 'Betriebseinnahme' AND aktiv = 1 LIMIT 1" ); $erloesId = $row ? (int)$row['id'] : null; } - if (!$aufwandId) { + if (!$ausgabeId) { $row = \Steuerkram\Database::fetchOne( - "SELECT id FROM accounts WHERE kontenart = 'Aufwand' AND aktiv = 1 LIMIT 1" + "SELECT id FROM accounts WHERE kontenart = 'Betriebsausgabe' AND aktiv = 1 LIMIT 1" ); - $aufwandId = $row ? (int)$row['id'] : null; + $ausgabeId = $row ? (int)$row['id'] : null; } - if (!$debitorId) { + if (!$zahlungsId) { $row = \Steuerkram\Database::fetchOne( - "SELECT id FROM accounts WHERE kontenart = 'Aktiv' AND aktiv = 1 LIMIT 1" + "SELECT id FROM accounts WHERE kontenart = 'Zahlungskonto' AND aktiv = 1 LIMIT 1" ); - $debitorId = $row ? (int)$row['id'] : null; + $zahlungsId = $row ? (int)$row['id'] : null; } if (!$bankId) { $row = \Steuerkram\Database::fetchOne( - "SELECT id FROM accounts WHERE kontenart = 'Aktiv' AND aktiv = 1 AND id != ? LIMIT 1", - [$debitorId ?? 0] + "SELECT id FROM accounts WHERE kontenart = 'Zahlungskonto' AND aktiv = 1 AND id != ? LIMIT 1", + [$zahlungsId ?? 0] ); $bankId = $row ? (int)$row['id'] : null; } - if (!$debitorId || !$erloesId || !$aufwandId || !$bankId) { + if (!$zahlungsId || !$erloesId || !$ausgabeId || !$bankId) { $this->assert(false, 'Benötigte Konten nicht gefunden – SKR03-Seed vorhanden?'); return; } $this->assert(true, 'Alle benötigten Konten gefunden'); - // ── Ausgangsbuchung (Ertrag, 19 %) ──────────────────────────────── - // Debitor an Erlös 19%: Netto=500, USt=95, Brutto=595 + // ── Einnahme-Buchung (19 %) ───────────────────────────────── + // Erlöse 19%: Netto=500, USt=95, Brutto=595 $belegAr = \Steuerkram\Models\Beleg::create([ 'beleg_nr' => 'TEST-AR-' . uniqid(), 'filename' => 'test-ar.pdf', @@ -70,17 +70,18 @@ class TaxSummenTest extends TestCase 'belegtyp' => 'Ausgangsrechnung', ]); \Steuerkram\Models\Booking::create([ - 'beleg_id' => $belegAr, - 'buchungs_datum' => $datum, - 'soll_konto_id' => $debitorId, - 'haben_konto_id' => $erloesId, - 'betrag_netto' => 500.00, - 'ust_satz' => 19.0, - 'buchungstext' => 'Test Ausgangsbuchung USt 19%', + 'beleg_id' => $belegAr, + 'buchungs_datum' => $datum, + 'buchungstyp' => 'Einnahme', + 'kategorie_konto_id' => $erloesId, + 'zahlungskonto_id' => $zahlungsId, + 'betrag_netto' => 500.00, + 'ust_satz' => 19.0, + 'buchungstext' => 'Test Einnahme USt 19%', ]); - // ── Eingangsbuchung (Aufwand, 19 %) ─────────────────────────────── - // Aufwand an Bank: Netto=200, USt=38, Brutto=238 + // ── Ausgabe-Buchung (19 %) ────────────────────────────────── + // Bürobedarf: Netto=200, USt=38, Brutto=238 $belegEr = \Steuerkram\Models\Beleg::create([ 'beleg_nr' => 'TEST-ER-' . uniqid(), 'filename' => 'test-er.pdf', @@ -92,13 +93,14 @@ class TaxSummenTest extends TestCase 'belegtyp' => 'Eingangsrechnung', ]); \Steuerkram\Models\Booking::create([ - 'beleg_id' => $belegEr, - 'buchungs_datum' => $datum, - 'soll_konto_id' => $aufwandId, - 'haben_konto_id' => $bankId, - 'betrag_netto' => 200.00, - 'ust_satz' => 19.0, - 'buchungstext' => 'Test Eingangsbuchung USt 19%', + 'beleg_id' => $belegEr, + 'buchungs_datum' => $datum, + 'buchungstyp' => 'Ausgabe', + 'kategorie_konto_id' => $ausgabeId, + 'zahlungskonto_id' => $bankId, + 'betrag_netto' => 200.00, + 'ust_satz' => 19.0, + 'buchungstext' => 'Test Ausgabe USt 19%', ]); // ── UStVA prüfen ────────────────────────────────────────────────── @@ -108,7 +110,7 @@ class TaxSummenTest extends TestCase // Kz 81: Steuerpfl. Umsätze 19% – muss mindestens 500 € enthalten $this->assert( (float)$kz['81']['betrag'] >= 500.00, - 'UStVA Kz 81 (Umsätze 19%): Betrag enthält Ausgangsbuchung ≥ 500 €' + 'UStVA Kz 81 (Umsätze 19%): Betrag enthält Einnahme ≥ 500 €' ); // Kz 83: USt auf Umsätze 19% – abgeleitet (19% von Kz81-Betrag) @@ -120,7 +122,7 @@ class TaxSummenTest extends TestCase // Kz 61: Vorsteuer 19% – muss mindestens 38 € enthalten $this->assert( (float)$kz['61']['steuer'] >= 38.00, - 'UStVA Kz 61 (Vorsteuer 19%): Steuerbetrag enthält Eingangsbuchung ≥ 38 €' + 'UStVA Kz 61 (Vorsteuer 19%): Steuerbetrag enthält Ausgabe ≥ 38 €' ); // Kz 66: Vorsteuer gesamt – muss ≥ Kz 61 sein @@ -143,13 +145,13 @@ class TaxSummenTest extends TestCase // Einnahmen müssen vorhanden sein $this->assert( (float)$euer['total_einnahmen'] >= 500.00, - 'EÜR: Einnahmen ≥ 500 € (Ausgangsbuchung eingeflossen)' + 'EÜR: Einnahmen ≥ 500 € (Einnahme-Buchung eingeflossen)' ); // Ausgaben müssen vorhanden sein $this->assert( (float)$euer['total_ausgaben'] >= 200.00, - 'EÜR: Ausgaben ≥ 200 € (Eingangsbuchung eingeflossen)' + 'EÜR: Ausgaben ≥ 200 € (Ausgabe-Buchung eingeflossen)' ); // Gewinn = Einnahmen - Ausgaben diff --git a/tests/06_EuerBuchungsTest.php b/tests/06_EuerBuchungsTest.php new file mode 100644 index 0000000..8b70fbe --- /dev/null +++ b/tests/06_EuerBuchungsTest.php @@ -0,0 +1,138 @@ +assert( + in_array('Betriebseinnahme', $arten), + 'Kontenart "Betriebseinnahme" existiert' + ); + $this->assert( + in_array('Betriebsausgabe', $arten), + 'Kontenart "Betriebsausgabe" existiert' + ); + $this->assert( + in_array('Zahlungskonto', $arten), + 'Kontenart "Zahlungskonto" existiert' + ); + $this->assert( + !in_array('Aktiv', $arten) && !in_array('Passiv', $arten) + && !in_array('Aufwand', $arten) && !in_array('Ertrag', $arten), + 'Alte Kontenarten (Aktiv, Passiv, Aufwand, Ertrag) existieren nicht mehr' + ); + + // Konten laden + $erloesId = $this->findAccountId('8000'); // Betriebseinnahme + $ausgabeId = $this->findAccountId('4600'); // Bürobedarf (Betriebsausgabe) + $bankId = $this->findAccountId('1200'); // Bank (Zahlungskonto) + + if (!$erloesId || !$ausgabeId || !$bankId) { + $this->assert(false, 'Benötigte Konten (8000, 4600, 1200) nicht gefunden'); + return; + } + + // ── Einnahme-Buchung ───────────────────────────────────────── + $belegEinnahme = \Steuerkram\Models\Beleg::create([ + 'beleg_nr' => 'TEST-EIN-' . uniqid(), + 'filename' => 'test-einnahme.pdf', + 'storage_path' => '', + 'mime_type' => 'application/pdf', + 'file_hash' => hash('sha256', 'einnahme-' . uniqid()), + 'beleg_datum' => $datum, + 'eingangs_datum' => $datum, + 'belegtyp' => 'Ausgangsrechnung', + ]); + + $einnahmeId = \Steuerkram\Models\Booking::create([ + 'beleg_id' => $belegEinnahme, + 'buchungs_datum' => $datum, + 'buchungstyp' => 'Einnahme', + 'kategorie_konto_id' => $erloesId, + 'zahlungskonto_id' => $bankId, + 'betrag_netto' => 1000.00, + 'ust_satz' => 19.0, + 'buchungstext' => 'Test Einnahme', + ]); + + $einnahme = \Steuerkram\Models\Booking::find($einnahmeId); + $this->assertEquals('Einnahme', $einnahme['buchungstyp'], 'Einnahme-Buchung hat buchungstyp=Einnahme'); + $this->assertEquals($erloesId, (int)$einnahme['kategorie_konto_id'], 'Kategorie-Konto ist Erlöskonto'); + $this->assertEquals($bankId, (int)$einnahme['zahlungskonto_id'], 'Zahlungskonto ist Bankkonto'); + $this->assertEquals(1000.00, (float)$einnahme['betrag_netto'], 'Nettobetrag korrekt'); + $this->assertEquals(190.00, (float)$einnahme['betrag_ust'], 'USt-Betrag korrekt berechnet'); + $this->assertEquals(1190.00, (float)$einnahme['betrag_brutto'], 'Bruttobetrag korrekt berechnet'); + + // ── Ausgabe-Buchung ────────────────────────────────────────── + $belegAusgabe = \Steuerkram\Models\Beleg::create([ + 'beleg_nr' => 'TEST-AUS-' . uniqid(), + 'filename' => 'test-ausgabe.pdf', + 'storage_path' => '', + 'mime_type' => 'application/pdf', + 'file_hash' => hash('sha256', 'ausgabe-' . uniqid()), + 'beleg_datum' => $datum, + 'eingangs_datum' => $datum, + 'belegtyp' => 'Eingangsrechnung', + ]); + + $ausgabeBuchungId = \Steuerkram\Models\Booking::create([ + 'beleg_id' => $belegAusgabe, + 'buchungs_datum' => $datum, + 'buchungstyp' => 'Ausgabe', + 'kategorie_konto_id' => $ausgabeId, + 'zahlungskonto_id' => $bankId, + 'betrag_netto' => 300.00, + 'ust_satz' => 19.0, + 'buchungstext' => 'Test Ausgabe', + ]); + + $ausgabe = \Steuerkram\Models\Booking::find($ausgabeBuchungId); + $this->assertEquals('Ausgabe', $ausgabe['buchungstyp'], 'Ausgabe-Buchung hat buchungstyp=Ausgabe'); + $this->assertEquals($ausgabeId, (int)$ausgabe['kategorie_konto_id'], 'Kategorie-Konto ist Ausgabenkonto'); + + // ── Storno prüfen ──────────────────────────────────────────── + $stornoId = \Steuerkram\Models\Booking::reverse($ausgabeBuchungId, 'Testgrund'); + $storno = \Steuerkram\Models\Booking::find($stornoId); + + $this->assertEquals('Ausgabe', $storno['buchungstyp'], 'Storno behält buchungstyp der Originalbuchung'); + $this->assertEquals(1, (int)$storno['is_reversal'], 'Storno-Flag gesetzt'); + $this->assertEquals($ausgabeId, (int)$storno['kategorie_konto_id'], 'Storno behält Kategorie-Konto'); + $this->assertEquals($bankId, (int)$storno['zahlungskonto_id'], 'Storno behält Zahlungskonto'); + + // Original hat reversed_by + $origNeu = \Steuerkram\Models\Booking::find($ausgabeBuchungId); + $this->assertEquals($stornoId, (int)$origNeu['reversed_by'], 'Original verweist auf Storno-Buchung'); + + // ── EÜR-Berechnung ─────────────────────────────────────────── + $euer = (new \Steuerkram\Services\EuerCalculator())->calculate((int)date('Y')); + + $this->assert( + (float)$euer['total_einnahmen'] >= 1000.00, + 'EÜR: Einnahmen enthalten die 1000 € Einnahme-Buchung' + ); + // Ausgabe wurde storniert → sollte nicht in den Ausgaben erscheinen (is_reversal=0 Filter) + $this->assert( + (float)$euer['gewinn'] > 0, + 'EÜR: Gewinn ist positiv (Einnahmen > Ausgaben nach Storno)' + ); + } +}