Fix #5: Buchungsmodell von Soll/Haben auf reines EÜR-System umgestellt
Schema: bookings mit buchungstyp (Einnahme/Ausgabe), kategorie_konto_id, zahlungskonto_id statt soll_konto_id/haben_konto_id. Kontenarten umbenannt (Ertrag→Betriebseinnahme, Aufwand→Betriebsausgabe, Aktiv→Zahlungskonto, Passiv→Verbindlichkeit, Neutral→Privat). UI, Services, Controller, Tests vollständig auf EÜR-Terminologie umgestellt. Alle 78 Tests bestanden.
This commit is contained in:
parent
632cbeea12
commit
fb579c585c
19 changed files with 536 additions and 343 deletions
|
|
@ -23,7 +23,7 @@ CREATE TABLE IF NOT EXISTS accounts (
|
||||||
id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
|
id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
|
||||||
kontonummer VARCHAR(10) NOT NULL UNIQUE,
|
kontonummer VARCHAR(10) NOT NULL UNIQUE,
|
||||||
bezeichnung VARCHAR(255) NOT NULL,
|
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,
|
beschreibung TEXT,
|
||||||
steuercode VARCHAR(20) NULL COMMENT 'z.B. USt19, VSt19, USt7, VSt7',
|
steuercode VARCHAR(20) NULL COMMENT 'z.B. USt19, VSt19, USt7, VSt7',
|
||||||
ustva_kz VARCHAR(10) NULL COMMENT 'ELSTER Kennzahl z.B. 81, 35',
|
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;
|
) 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
|
-- GoBD: KEINE UPDATE/DELETE auf gesperrte Buchungen
|
||||||
-- ============================================================
|
-- ============================================================
|
||||||
CREATE TABLE IF NOT EXISTS bookings (
|
CREATE TABLE IF NOT EXISTS bookings (
|
||||||
id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
|
id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
|
||||||
buchungs_nr VARCHAR(30) NOT NULL UNIQUE,
|
buchungs_nr VARCHAR(30) NOT NULL UNIQUE,
|
||||||
beleg_id INT UNSIGNED NOT NULL,
|
beleg_id INT UNSIGNED NOT NULL,
|
||||||
buchungs_datum DATE NOT NULL,
|
buchungs_datum DATE NOT NULL,
|
||||||
soll_konto_id INT UNSIGNED NOT NULL,
|
buchungstyp ENUM('Einnahme','Ausgabe') NOT NULL,
|
||||||
haben_konto_id INT UNSIGNED NOT NULL,
|
kategorie_konto_id INT UNSIGNED NOT NULL COMMENT 'Was? z.B. Büromaterial, Honorar',
|
||||||
betrag_netto DECIMAL(12,2) NOT NULL,
|
zahlungskonto_id INT UNSIGNED NOT NULL COMMENT 'Wie bezahlt? z.B. Bank, Kasse',
|
||||||
ust_satz DECIMAL(5,2) NOT NULL DEFAULT 0.00 COMMENT 'Steuersatz in % (0, 7, 19)',
|
betrag_netto DECIMAL(12,2) NOT NULL,
|
||||||
betrag_ust DECIMAL(12,2) NOT NULL DEFAULT 0.00,
|
ust_satz DECIMAL(5,2) NOT NULL DEFAULT 0.00 COMMENT 'Steuersatz in % (0, 7, 19)',
|
||||||
betrag_brutto DECIMAL(12,2) NOT NULL,
|
betrag_ust DECIMAL(12,2) NOT NULL DEFAULT 0.00,
|
||||||
buchungstext VARCHAR(500) NOT NULL,
|
betrag_brutto DECIMAL(12,2) NOT NULL,
|
||||||
is_reversal TINYINT(1) NOT NULL DEFAULT 0 COMMENT '1 = Stornobuchung',
|
buchungstext VARCHAR(500) NOT NULL,
|
||||||
reversed_by INT UNSIGNED NULL COMMENT 'ID der Stornobuchung',
|
is_reversal TINYINT(1) NOT NULL DEFAULT 0 COMMENT '1 = Stornobuchung',
|
||||||
locked TINYINT(1) NOT NULL DEFAULT 0 COMMENT 'GoBD: unveränderlich',
|
reversed_by INT UNSIGNED NULL COMMENT 'ID der Stornobuchung',
|
||||||
fiscal_year_id INT UNSIGNED NOT NULL,
|
locked TINYINT(1) NOT NULL DEFAULT 0 COMMENT 'GoBD: unveränderlich',
|
||||||
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
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_beleg FOREIGN KEY (beleg_id) REFERENCES belege(id),
|
||||||
CONSTRAINT fk_booking_soll FOREIGN KEY (soll_konto_id) REFERENCES accounts(id),
|
CONSTRAINT fk_booking_kategorie FOREIGN KEY (kategorie_konto_id) REFERENCES accounts(id),
|
||||||
CONSTRAINT fk_booking_haben FOREIGN KEY (haben_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),
|
CONSTRAINT fk_booking_year FOREIGN KEY (fiscal_year_id) REFERENCES fiscal_years(id),
|
||||||
INDEX idx_buchungs_datum (buchungs_datum),
|
INDEX idx_buchungs_datum (buchungs_datum),
|
||||||
INDEX idx_fiscal_year (fiscal_year_id),
|
INDEX idx_fiscal_year (fiscal_year_id),
|
||||||
INDEX idx_soll_konto (soll_konto_id),
|
INDEX idx_buchungstyp (buchungstyp),
|
||||||
INDEX idx_haben_konto (haben_konto_id)
|
INDEX idx_kategorie_konto (kategorie_konto_id),
|
||||||
|
INDEX idx_zahlungskonto (zahlungskonto_id)
|
||||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||||
|
|
||||||
-- GoBD-Trigger: Gesperrte Buchungen dürfen nicht geändert werden
|
-- GoBD-Trigger: Gesperrte Buchungen dürfen nicht geändert werden
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
-- SKR03 Kontenrahmen - Wichtigste Konten für EÜR / Freiberufler
|
-- 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)
|
-- steuercode: USt19, USt7, VSt19, VSt7, NULL (kein Steuercode)
|
||||||
-- ustva_kz: ELSTER-Kennzahl für UStVA
|
-- ustva_kz: ELSTER-Kennzahl für UStVA
|
||||||
|
|
||||||
|
|
@ -8,116 +8,116 @@ INSERT INTO accounts (kontonummer, bezeichnung, kontenart, steuercode, ustva_kz,
|
||||||
-- ============================================================
|
-- ============================================================
|
||||||
-- KLASSE 0: Anlagevermögen
|
-- KLASSE 0: Anlagevermögen
|
||||||
-- ============================================================
|
-- ============================================================
|
||||||
('0410', 'EDV-Software', 'Aktiv', NULL, NULL, 1),
|
('0410', 'EDV-Software', 'Zahlungskonto', NULL, NULL, 1),
|
||||||
('0420', 'Büroeinrichtung', 'Aktiv', NULL, NULL, 1),
|
('0420', 'Büroeinrichtung', 'Zahlungskonto', NULL, NULL, 1),
|
||||||
('0430', 'Technische Anlagen und Maschinen', 'Aktiv', NULL, NULL, 1),
|
('0430', 'Technische Anlagen und Maschinen', 'Zahlungskonto', NULL, NULL, 1),
|
||||||
('0480', 'Betriebs- und Geschäftsausstattung', 'Aktiv', NULL, NULL, 1),
|
('0480', 'Betriebs- und Geschäftsausstattung', 'Zahlungskonto', NULL, NULL, 1),
|
||||||
('0490', 'Geringwertige Wirtschaftsgüter', 'Aktiv', NULL, NULL, 1),
|
('0490', 'Geringwertige Wirtschaftsgüter', 'Zahlungskonto', NULL, NULL, 1),
|
||||||
|
|
||||||
-- ============================================================
|
-- ============================================================
|
||||||
-- KLASSE 1: Umlaufvermögen / Geldkonten
|
-- KLASSE 1: Umlaufvermögen / Geldkonten
|
||||||
-- ============================================================
|
-- ============================================================
|
||||||
('1000', 'Kasse', 'Aktiv', NULL, NULL, 1),
|
('1000', 'Kasse', 'Zahlungskonto', NULL, NULL, 1),
|
||||||
('1200', 'Bank', 'Aktiv', NULL, NULL, 1),
|
('1200', 'Bank', 'Zahlungskonto', NULL, NULL, 1),
|
||||||
('1361', 'Abziehbare Vorsteuer 19 %', 'Aktiv', 'VSt19', '66', 1),
|
('1361', 'Abziehbare Vorsteuer 19 %', 'Zahlungskonto', 'VSt19', '66', 1),
|
||||||
('1371', 'Abziehbare Vorsteuer 7 %', 'Aktiv', 'VSt7', '66', 1),
|
('1371', 'Abziehbare Vorsteuer 7 %', 'Zahlungskonto', 'VSt7', '66', 1),
|
||||||
('1400', 'Forderungen aus Lieferungen und Leistungen', 'Aktiv', NULL, NULL, 1),
|
('1400', 'Forderungen aus Lieferungen und Leistungen', 'Zahlungskonto', NULL, NULL, 1),
|
||||||
('1460', 'Forderungen aus Lieferungen und Leistungen (steuerfrei)', 'Aktiv', NULL, NULL, 1),
|
('1460', 'Forderungen aus Lieferungen und Leistungen (steuerfrei)', 'Zahlungskonto', NULL, NULL, 1),
|
||||||
('1576', 'Abziehbare Vorsteuer 19 % (Eingangsrechnungen)', 'Aktiv', 'VSt19', '66', 1),
|
('1576', 'Abziehbare Vorsteuer 19 % (Eingangsrechnungen)', 'Zahlungskonto', 'VSt19', '66', 1),
|
||||||
('1571', 'Abziehbare Vorsteuer 7 % (Eingangsrechnungen)', 'Aktiv', 'VSt7', '66', 1),
|
('1571', 'Abziehbare Vorsteuer 7 % (Eingangsrechnungen)', 'Zahlungskonto', 'VSt7', '66', 1),
|
||||||
|
|
||||||
-- ============================================================
|
-- ============================================================
|
||||||
-- KLASSE 1: Verbindlichkeiten (Passiv)
|
-- KLASSE 1: Verbindlichkeiten (Passiv)
|
||||||
-- ============================================================
|
-- ============================================================
|
||||||
('1600', 'Verbindlichkeiten aus Lieferungen und Leistungen', 'Passiv', NULL, NULL, 1),
|
('1600', 'Verbindlichkeiten aus Lieferungen und Leistungen', 'Verbindlichkeit', NULL, NULL, 1),
|
||||||
('1700', 'Sonstige Verbindlichkeiten', 'Passiv', NULL, NULL, 1),
|
('1700', 'Sonstige Verbindlichkeiten', 'Verbindlichkeit', NULL, NULL, 1),
|
||||||
('1741', 'Umsatzsteuer 19 %', 'Passiv', 'USt19', '83', 1),
|
('1741', 'Umsatzsteuer 19 %', 'Verbindlichkeit', 'USt19', '83', 1),
|
||||||
('1742', 'Umsatzsteuer 7 %', 'Passiv', 'USt7', '84', 1),
|
('1742', 'Umsatzsteuer 7 %', 'Verbindlichkeit', 'USt7', '84', 1),
|
||||||
('1771', 'Umsatzsteuer-Vorauszahlungen', 'Passiv', NULL, NULL, 1),
|
('1771', 'Umsatzsteuer-Vorauszahlungen', 'Verbindlichkeit', NULL, NULL, 1),
|
||||||
('1780', 'Umsatzsteuer-Vorauszahlungen lfd. Jahr', 'Passiv', NULL, NULL, 1),
|
('1780', 'Umsatzsteuer-Vorauszahlungen lfd. Jahr', 'Verbindlichkeit', NULL, NULL, 1),
|
||||||
|
|
||||||
-- ============================================================
|
-- ============================================================
|
||||||
-- KLASSE 3: Wareneinkauf (Aufwand)
|
-- KLASSE 3: Wareneinkauf (Aufwand)
|
||||||
-- ============================================================
|
-- ============================================================
|
||||||
('3100', 'Wareneinkauf 19 % Vorsteuer', 'Aufwand', 'VSt19', '66', 1),
|
('3100', 'Wareneinkauf 19 % Vorsteuer', 'Betriebsausgabe', 'VSt19', '66', 1),
|
||||||
('3110', 'Wareneinkauf 7 % Vorsteuer', 'Aufwand', 'VSt7', '66', 1),
|
('3110', 'Wareneinkauf 7 % Vorsteuer', 'Betriebsausgabe', 'VSt7', '66', 1),
|
||||||
('3120', 'Wareneinkauf ohne Vorsteuer', 'Aufwand', NULL, NULL, 1),
|
('3120', 'Wareneinkauf ohne Vorsteuer', 'Betriebsausgabe', NULL, NULL, 1),
|
||||||
|
|
||||||
-- ============================================================
|
-- ============================================================
|
||||||
-- KLASSE 4: Betriebliche Aufwendungen
|
-- KLASSE 4: Betriebliche Aufwendungen
|
||||||
-- ============================================================
|
-- ============================================================
|
||||||
('4100', 'Gehälter', 'Aufwand', NULL, NULL, 1),
|
('4100', 'Gehälter', 'Betriebsausgabe', NULL, NULL, 1),
|
||||||
('4110', 'Löhne', 'Aufwand', NULL, NULL, 1),
|
('4110', 'Löhne', 'Betriebsausgabe', NULL, NULL, 1),
|
||||||
('4120', 'Gesetzliche soziale Aufwendungen', 'Aufwand', NULL, NULL, 1),
|
('4120', 'Gesetzliche soziale Aufwendungen', 'Betriebsausgabe', NULL, NULL, 1),
|
||||||
('4130', 'Freiwillige soziale Aufwendungen', 'Aufwand', NULL, NULL, 1),
|
('4130', 'Freiwillige soziale Aufwendungen', 'Betriebsausgabe', NULL, NULL, 1),
|
||||||
('4140', 'Aufwendungen für Altersversorgung', 'Aufwand', NULL, NULL, 1),
|
('4140', 'Aufwendungen für Altersversorgung', 'Betriebsausgabe', NULL, NULL, 1),
|
||||||
('4200', 'Raumkosten (Miete)', 'Aufwand', 'VSt19', '66', 1),
|
('4200', 'Raumkosten (Miete)', 'Betriebsausgabe', 'VSt19', '66', 1),
|
||||||
('4210', 'Heizung', 'Aufwand', 'VSt19', '66', 1),
|
('4210', 'Heizung', 'Betriebsausgabe', 'VSt19', '66', 1),
|
||||||
('4240', 'Gas, Strom, Wasser', 'Aufwand', 'VSt19', '66', 1),
|
('4240', 'Gas, Strom, Wasser', 'Betriebsausgabe', 'VSt19', '66', 1),
|
||||||
('4250', 'Reinigung', 'Aufwand', 'VSt19', '66', 1),
|
('4250', 'Reinigung', 'Betriebsausgabe', 'VSt19', '66', 1),
|
||||||
('4260', 'Gebäudeversicherungen', 'Aufwand', NULL, NULL, 1),
|
('4260', 'Gebäudeversicherungen', 'Betriebsausgabe', NULL, NULL, 1),
|
||||||
('4300', 'Betriebliche Versicherungen', 'Aufwand', NULL, NULL, 1),
|
('4300', 'Betriebliche Versicherungen', 'Betriebsausgabe', NULL, NULL, 1),
|
||||||
('4320', 'Beiträge', 'Aufwand', NULL, NULL, 1),
|
('4320', 'Beiträge', 'Betriebsausgabe', NULL, NULL, 1),
|
||||||
('4330', 'Berufsgenossenschaft', 'Aufwand', NULL, NULL, 1),
|
('4330', 'Berufsgenossenschaft', 'Betriebsausgabe', NULL, NULL, 1),
|
||||||
('4360', 'Beiträge zu Berufsverbänden', 'Aufwand', NULL, NULL, 1),
|
('4360', 'Beiträge zu Berufsverbänden', 'Betriebsausgabe', NULL, NULL, 1),
|
||||||
('4380', 'Verwaltungs-Gemeinkosten', 'Aufwand', 'VSt19', '66', 1),
|
('4380', 'Verwaltungs-Gemeinkosten', 'Betriebsausgabe', 'VSt19', '66', 1),
|
||||||
('4390', 'Aufwand für Arbeitnehmer-Überlassung', 'Aufwand', 'VSt19', '66', 1),
|
('4390', 'Aufwand für Arbeitnehmer-Überlassung', 'Betriebsausgabe', 'VSt19', '66', 1),
|
||||||
('4400', 'PKW-Kosten', 'Aufwand', 'VSt19', '66', 1),
|
('4400', 'PKW-Kosten', 'Betriebsausgabe', 'VSt19', '66', 1),
|
||||||
('4410', 'LKW-Kosten', 'Aufwand', 'VSt19', '66', 1),
|
('4410', 'LKW-Kosten', 'Betriebsausgabe', 'VSt19', '66', 1),
|
||||||
('4430', 'Kraftfahrzeugsteuer', 'Aufwand', NULL, NULL, 1),
|
('4430', 'Kraftfahrzeugsteuer', 'Betriebsausgabe', NULL, NULL, 1),
|
||||||
('4460', 'Reisekosten Arbeitnehmer', 'Aufwand', 'VSt19', '66', 1),
|
('4460', 'Reisekosten Arbeitnehmer', 'Betriebsausgabe', 'VSt19', '66', 1),
|
||||||
('4500', 'Werbekosten', 'Aufwand', 'VSt19', '66', 1),
|
('4500', 'Werbekosten', 'Betriebsausgabe', 'VSt19', '66', 1),
|
||||||
('4510', 'Kosten der Warenabgabe (Muster, Proben)', 'Aufwand', 'VSt19', '66', 1),
|
('4510', 'Kosten der Warenabgabe (Muster, Proben)', 'Betriebsausgabe', 'VSt19', '66', 1),
|
||||||
('4520', 'Repräsentationskosten', 'Aufwand', 'VSt19', '66', 1),
|
('4520', 'Repräsentationskosten', 'Betriebsausgabe', 'VSt19', '66', 1),
|
||||||
('4530', 'Messekosten', 'Aufwand', 'VSt19', '66', 1),
|
('4530', 'Messekosten', 'Betriebsausgabe', 'VSt19', '66', 1),
|
||||||
('4540', 'Bewirtungskosten', 'Aufwand', 'VSt19', '66', 1),
|
('4540', 'Bewirtungskosten', 'Betriebsausgabe', 'VSt19', '66', 1),
|
||||||
('4560', 'Buchführungskosten', 'Aufwand', 'VSt19', '66', 1),
|
('4560', 'Buchführungskosten', 'Betriebsausgabe', 'VSt19', '66', 1),
|
||||||
('4570', 'Jahresabschluss- und Prüfungskosten', 'Aufwand', 'VSt19', '66', 1),
|
('4570', 'Jahresabschluss- und Prüfungskosten', 'Betriebsausgabe', 'VSt19', '66', 1),
|
||||||
('4580', 'Beratungskosten', 'Aufwand', 'VSt19', '66', 1),
|
('4580', 'Beratungskosten', 'Betriebsausgabe', 'VSt19', '66', 1),
|
||||||
('4600', 'Bürobedarf', 'Aufwand', 'VSt19', '66', 1),
|
('4600', 'Bürobedarf', 'Betriebsausgabe', 'VSt19', '66', 1),
|
||||||
('4610', 'Zeitschriften, Bücher', 'Aufwand', 'VSt7', '66', 1),
|
('4610', 'Zeitschriften, Bücher', 'Betriebsausgabe', 'VSt7', '66', 1),
|
||||||
('4620', 'Portokosten', 'Aufwand', NULL, NULL, 1),
|
('4620', 'Portokosten', 'Betriebsausgabe', NULL, NULL, 1),
|
||||||
('4630', 'Telefon', 'Aufwand', 'VSt19', '66', 1),
|
('4630', 'Telefon', 'Betriebsausgabe', 'VSt19', '66', 1),
|
||||||
('4640', 'Internetkosten', 'Aufwand', 'VSt19', '66', 1),
|
('4640', 'Internetkosten', 'Betriebsausgabe', 'VSt19', '66', 1),
|
||||||
('4650', 'Büromaschinen (Wartung/Reparatur)', 'Aufwand', 'VSt19', '66', 1),
|
('4650', 'Büromaschinen (Wartung/Reparatur)', 'Betriebsausgabe', 'VSt19', '66', 1),
|
||||||
('4660', 'EDV-Kosten (Software, Lizenzen)', 'Aufwand', 'VSt19', '66', 1),
|
('4660', 'EDV-Kosten (Software, Lizenzen)', 'Betriebsausgabe', 'VSt19', '66', 1),
|
||||||
('4670', 'Betriebliche Fortbildungskosten', 'Aufwand', 'VSt19', '66', 1),
|
('4670', 'Betriebliche Fortbildungskosten', 'Betriebsausgabe', 'VSt19', '66', 1),
|
||||||
('4680', 'Fachliteratur', 'Aufwand', 'VSt7', '66', 1),
|
('4680', 'Fachliteratur', 'Betriebsausgabe', 'VSt7', '66', 1),
|
||||||
('4700', 'Abschreibungen auf Anlagevermögen', 'Aufwand', NULL, NULL, 1),
|
('4700', 'Abschreibungen auf Anlagevermögen', 'Betriebsausgabe', NULL, NULL, 1),
|
||||||
('4710', 'Abschreibungen auf GWG', 'Aufwand', NULL, NULL, 1),
|
('4710', 'Abschreibungen auf GWG', 'Betriebsausgabe', NULL, NULL, 1),
|
||||||
('4800', 'Bankgebühren', 'Aufwand', NULL, NULL, 1),
|
('4800', 'Bankgebühren', 'Betriebsausgabe', NULL, NULL, 1),
|
||||||
('4820', 'Zinsen und ähnliche Aufwendungen', 'Aufwand', NULL, NULL, 1),
|
('4820', 'Zinsen und ähnliche Aufwendungen', 'Betriebsausgabe', NULL, NULL, 1),
|
||||||
('4830', 'Mahngebühren, Inkassokosten', 'Aufwand', NULL, NULL, 1),
|
('4830', 'Mahngebühren, Inkassokosten', 'Betriebsausgabe', NULL, NULL, 1),
|
||||||
('4900', 'Sonstige betriebliche Aufwendungen', 'Aufwand', 'VSt19', '66', 1),
|
('4900', 'Sonstige betriebliche Aufwendungen', 'Betriebsausgabe', 'VSt19', '66', 1),
|
||||||
('4920', 'Gewerbesteuer', 'Aufwand', NULL, NULL, 1),
|
('4920', 'Gewerbesteuer', 'Betriebsausgabe', NULL, NULL, 1),
|
||||||
('4930', 'Kraftfahrzeugsteuer (betrieblich)', 'Aufwand', NULL, NULL, 1),
|
('4930', 'Kraftfahrzeugsteuer (betrieblich)', 'Betriebsausgabe', NULL, NULL, 1),
|
||||||
('4950', 'Sonstige Steuern', 'Aufwand', NULL, NULL, 1),
|
('4950', 'Sonstige Steuern', 'Betriebsausgabe', NULL, NULL, 1),
|
||||||
('4960', 'Sonstige betriebliche Aufwendungen (ohne VSt)', 'Aufwand', NULL, NULL, 1),
|
('4960', 'Sonstige betriebliche Aufwendungen (ohne VSt)', 'Betriebsausgabe', NULL, NULL, 1),
|
||||||
('4970', 'Aufwendungen aus Verlust Forderungen', 'Aufwand', NULL, NULL, 1),
|
('4970', 'Aufwendungen aus Verlust Forderungen', 'Betriebsausgabe', NULL, NULL, 1),
|
||||||
|
|
||||||
-- ============================================================
|
-- ============================================================
|
||||||
-- KLASSE 8: Erträge
|
-- KLASSE 8: Erträge
|
||||||
-- ============================================================
|
-- ============================================================
|
||||||
('8000', 'Erlöse aus Lieferungen und Leistungen 19 % USt', 'Ertrag', 'USt19', '81', 1),
|
('8000', 'Erlöse aus Lieferungen und Leistungen 19 % USt', 'Betriebseinnahme', 'USt19', '81', 1),
|
||||||
('8100', 'Erlöse aus Lieferungen und Leistungen 7 % USt', 'Ertrag', 'USt7', '86', 1),
|
('8100', 'Erlöse aus Lieferungen und Leistungen 7 % USt', 'Betriebseinnahme', 'USt7', '86', 1),
|
||||||
('8120', 'Erlöse aus Lieferungen und Leistungen (steuerfrei)', 'Ertrag', NULL, '35', 1),
|
('8120', 'Erlöse aus Lieferungen und Leistungen (steuerfrei)', 'Betriebseinnahme', NULL, '35', 1),
|
||||||
('8125', 'Steuerfreie EU-Lieferungen', 'Ertrag', NULL, '41', 1),
|
('8125', 'Steuerfreie EU-Lieferungen', 'Betriebseinnahme', NULL, '41', 1),
|
||||||
('8130', 'Sonstige steuerfreie Umsätze', 'Ertrag', NULL, NULL, 1),
|
('8130', 'Sonstige steuerfreie Umsätze', 'Betriebseinnahme', NULL, NULL, 1),
|
||||||
('8200', 'Erlöse aus sonstigen Leistungen 19 % USt', 'Ertrag', 'USt19', '81', 1),
|
('8200', 'Erlöse aus sonstigen Leistungen 19 % USt', 'Betriebseinnahme', 'USt19', '81', 1),
|
||||||
('8300', 'Erlöse aus sonstigen Leistungen 7 % USt', 'Ertrag', 'USt7', '86', 1),
|
('8300', 'Erlöse aus sonstigen Leistungen 7 % USt', 'Betriebseinnahme', 'USt7', '86', 1),
|
||||||
('8400', 'Erlöse aus Anlagenabgängen (steuerpflichtig)', 'Ertrag', 'USt19', '81', 1),
|
('8400', 'Erlöse aus Anlagenabgängen (steuerpflichtig)', 'Betriebseinnahme', 'USt19', '81', 1),
|
||||||
('8500', 'Zinsen und ähnliche Erträge', 'Ertrag', NULL, NULL, 1),
|
('8500', 'Zinsen und ähnliche Erträge', 'Betriebseinnahme', NULL, NULL, 1),
|
||||||
('8600', 'Erträge aus Beteiligungen', 'Ertrag', NULL, NULL, 1),
|
('8600', 'Erträge aus Beteiligungen', 'Betriebseinnahme', NULL, NULL, 1),
|
||||||
('8900', 'Sonstige betriebliche Erträge', 'Ertrag', NULL, NULL, 1),
|
('8900', 'Sonstige betriebliche Erträge', 'Betriebseinnahme', NULL, NULL, 1),
|
||||||
('8910', 'Sonstige betriebliche Erträge (steuerfrei)', 'Ertrag', NULL, NULL, 1),
|
('8910', 'Sonstige betriebliche Erträge (steuerfrei)', 'Betriebseinnahme', NULL, NULL, 1),
|
||||||
('8920', 'Erträge aus Auflösung Rückstellungen', 'Ertrag', NULL, NULL, 1),
|
('8920', 'Erträge aus Auflösung Rückstellungen', 'Betriebseinnahme', NULL, NULL, 1),
|
||||||
('8940', 'Erträge aus Kursgewinnen', 'Ertrag', NULL, NULL, 1),
|
('8940', 'Erträge aus Kursgewinnen', 'Betriebseinnahme', NULL, NULL, 1),
|
||||||
|
|
||||||
-- ============================================================
|
-- ============================================================
|
||||||
-- KLASSE 9: Eigenkapital / Privat (Neutral)
|
-- KLASSE 9: Eigenkapital / Privat (Neutral)
|
||||||
-- ============================================================
|
-- ============================================================
|
||||||
('9000', 'Eigenkapital', 'Neutral', NULL, NULL, 1),
|
('9000', 'Eigenkapital', 'Privat', NULL, NULL, 1),
|
||||||
('9100', 'Privatentnahmen', 'Neutral', NULL, NULL, 1),
|
('9100', 'Privatentnahmen', 'Privat', NULL, NULL, 1),
|
||||||
('9200', 'Privateinlagen', 'Neutral', NULL, NULL, 1),
|
('9200', 'Privateinlagen', 'Privat', NULL, NULL, 1),
|
||||||
('9300', 'Gewinnvortrag', 'Neutral', NULL, NULL, 1),
|
('9300', 'Gewinnvortrag', 'Privat', NULL, NULL, 1),
|
||||||
('9400', 'Verlustvortrag', 'Neutral', NULL, NULL, 1);
|
('9400', 'Verlustvortrag', 'Privat', NULL, NULL, 1);
|
||||||
|
|
|
||||||
|
|
@ -337,13 +337,13 @@ function transferKontoNr(select) {
|
||||||
}
|
}
|
||||||
|
|
||||||
document.addEventListener('dblclick', (e) => {
|
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);
|
if (select) transferKontoNr(select);
|
||||||
});
|
});
|
||||||
|
|
||||||
document.addEventListener('keydown', (e) => {
|
document.addEventListener('keydown', (e) => {
|
||||||
if (e.key !== 'Enter') return;
|
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) {
|
if (select) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
transferKontoNr(select);
|
transferKontoNr(select);
|
||||||
|
|
|
||||||
|
|
@ -19,24 +19,22 @@ class AccountController
|
||||||
// Quick stats
|
// Quick stats
|
||||||
$stats = [];
|
$stats = [];
|
||||||
|
|
||||||
// Umsatz (Ertrag-Konten)
|
// Einnahmen
|
||||||
$umsatz = \Steuerkram\Database::fetchOne(
|
$umsatz = \Steuerkram\Database::fetchOne(
|
||||||
'SELECT COALESCE(SUM(bk.betrag_netto),0) as total
|
'SELECT COALESCE(SUM(bk.betrag_netto),0) as total
|
||||||
FROM bookings bk
|
FROM bookings bk
|
||||||
JOIN accounts a ON bk.haben_konto_id = a.id
|
|
||||||
JOIN fiscal_years fy ON bk.fiscal_year_id = fy.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]
|
[$year]
|
||||||
);
|
);
|
||||||
$stats['umsatz'] = (float)($umsatz['total'] ?? 0);
|
$stats['umsatz'] = (float)($umsatz['total'] ?? 0);
|
||||||
|
|
||||||
// Ausgaben (Aufwand-Konten)
|
// Ausgaben
|
||||||
$ausgaben = \Steuerkram\Database::fetchOne(
|
$ausgaben = \Steuerkram\Database::fetchOne(
|
||||||
'SELECT COALESCE(SUM(bk.betrag_netto),0) as total
|
'SELECT COALESCE(SUM(bk.betrag_netto),0) as total
|
||||||
FROM bookings bk
|
FROM bookings bk
|
||||||
JOIN accounts a ON bk.soll_konto_id = a.id
|
|
||||||
JOIN fiscal_years fy ON bk.fiscal_year_id = fy.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]
|
[$year]
|
||||||
);
|
);
|
||||||
$stats['ausgaben'] = (float)($ausgaben['total'] ?? 0);
|
$stats['ausgaben'] = (float)($ausgaben['total'] ?? 0);
|
||||||
|
|
@ -59,12 +57,12 @@ class AccountController
|
||||||
|
|
||||||
// Letzte Buchungen
|
// Letzte Buchungen
|
||||||
$recentBookings = \Steuerkram\Database::fetchAll(
|
$recentBookings = \Steuerkram\Database::fetchAll(
|
||||||
'SELECT bk.*, s.kontonummer as soll_nr, s.bezeichnung as soll_name,
|
'SELECT bk.*, k.kontonummer as kategorie_nr, k.bezeichnung as kategorie_name,
|
||||||
h.kontonummer as haben_nr, h.bezeichnung as haben_name,
|
z.kontonummer as zahlungs_nr, z.bezeichnung as zahlungs_name,
|
||||||
bl.beleg_nr
|
bl.beleg_nr
|
||||||
FROM bookings bk
|
FROM bookings bk
|
||||||
JOIN accounts s ON bk.soll_konto_id = s.id
|
JOIN accounts k ON bk.kategorie_konto_id = k.id
|
||||||
JOIN accounts h ON bk.haben_konto_id = h.id
|
JOIN accounts z ON bk.zahlungskonto_id = z.id
|
||||||
JOIN belege bl ON bk.beleg_id = bl.id
|
JOIN belege bl ON bk.beleg_id = bl.id
|
||||||
ORDER BY bk.created_at DESC LIMIT 10'
|
ORDER BY bk.created_at DESC LIMIT 10'
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -55,8 +55,10 @@ class BookingController
|
||||||
fprintf($out, chr(0xEF) . chr(0xBB) . chr(0xBF)); // UTF-8 BOM for Excel
|
fprintf($out, chr(0xEF) . chr(0xBB) . chr(0xBF)); // UTF-8 BOM for Excel
|
||||||
|
|
||||||
fputcsv($out, [
|
fputcsv($out, [
|
||||||
'Buchungsnr.', 'Datum', 'Belegnr.', 'Soll-Kto.', 'Soll-Bez.',
|
'Buchungsnr.', 'Datum', 'Belegnr.', 'Typ',
|
||||||
'Haben-Kto.', 'Haben-Bez.', 'Netto (€)', 'USt (%)', 'USt (€)',
|
'Kategorie-Kto.', 'Kategorie-Bez.',
|
||||||
|
'Zahlungskonto-Kto.', 'Zahlungskonto-Bez.',
|
||||||
|
'Netto (€)', 'USt (%)', 'USt (€)',
|
||||||
'Brutto (€)', 'Buchungstext', 'Storno'
|
'Brutto (€)', 'Buchungstext', 'Storno'
|
||||||
], ';');
|
], ';');
|
||||||
|
|
||||||
|
|
@ -65,10 +67,11 @@ class BookingController
|
||||||
$this->csvSanitize((string)$row['buchungs_nr']),
|
$this->csvSanitize((string)$row['buchungs_nr']),
|
||||||
$row['buchungs_datum'],
|
$row['buchungs_datum'],
|
||||||
$this->csvSanitize((string)$row['beleg_nr']),
|
$this->csvSanitize((string)$row['beleg_nr']),
|
||||||
$row['soll_nr'],
|
$row['buchungstyp'],
|
||||||
$this->csvSanitize((string)$row['soll_name']),
|
$row['kategorie_nr'],
|
||||||
$row['haben_nr'],
|
$this->csvSanitize((string)$row['kategorie_name']),
|
||||||
$this->csvSanitize((string)$row['haben_name']),
|
$row['zahlungs_nr'],
|
||||||
|
$this->csvSanitize((string)$row['zahlungs_name']),
|
||||||
number_format((float)$row['betrag_netto'], 2, ',', ''),
|
number_format((float)$row['betrag_netto'], 2, ',', ''),
|
||||||
number_format((float)$row['ust_satz'], 2, ',', ''),
|
number_format((float)$row['ust_satz'], 2, ',', ''),
|
||||||
number_format((float)$row['betrag_ust'], 2, ',', ''),
|
number_format((float)$row['betrag_ust'], 2, ',', ''),
|
||||||
|
|
@ -87,15 +90,16 @@ class BookingController
|
||||||
$errors = [];
|
$errors = [];
|
||||||
|
|
||||||
// Validate
|
// Validate
|
||||||
if (empty($data['soll_konto_id'])) $errors['soll_konto_id'] = 'Pflichtfeld';
|
if (empty($data['buchungstyp'])) $errors['buchungstyp'] = 'Pflichtfeld';
|
||||||
if (empty($data['haben_konto_id'])) $errors['haben_konto_id'] = 'Pflichtfeld';
|
if (empty($data['kategorie_konto_id'])) $errors['kategorie_konto_id'] = 'Pflichtfeld';
|
||||||
if (empty($data['buchungs_datum'])) $errors['buchungs_datum'] = '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'] === '')
|
if (!isset($data['betrag_netto']) || $data['betrag_netto'] === '')
|
||||||
$errors['betrag_netto'] = 'Pflichtfeld';
|
$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']) {
|
if (empty($errors) && $data['kategorie_konto_id'] === $data['zahlungskonto_id']) {
|
||||||
$errors['haben_konto_id'] = 'Soll- und Habenkonto dürfen nicht identisch sein.';
|
$errors['zahlungskonto_id'] = 'Kategorie- und Zahlungskonto dürfen nicht identisch sein.';
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($errors) {
|
if ($errors) {
|
||||||
|
|
|
||||||
|
|
@ -246,11 +246,11 @@ class InvoiceController
|
||||||
'notes' => 'Automatisch erstellt aus Rechnung ' . $invoice['rechnungs_nr'],
|
'notes' => 'Automatisch erstellt aus Rechnung ' . $invoice['rechnungs_nr'],
|
||||||
]);
|
]);
|
||||||
|
|
||||||
$debitorKonto = \Steuerkram\Database::fetchOne(
|
$zahlungskonto = \Steuerkram\Database::fetchOne(
|
||||||
'SELECT id FROM accounts WHERE kontonummer = ?', ['1400']
|
'SELECT id FROM accounts WHERE kontonummer = ?', ['1400']
|
||||||
);
|
);
|
||||||
if (!$debitorKonto) {
|
if (!$zahlungskonto) {
|
||||||
throw new \RuntimeException('Debitorenkonto 1400 nicht gefunden.');
|
throw new \RuntimeException('Forderungskonto 1400 nicht gefunden.');
|
||||||
}
|
}
|
||||||
|
|
||||||
$erloesKonten = [];
|
$erloesKonten = [];
|
||||||
|
|
@ -270,24 +270,25 @@ class InvoiceController
|
||||||
|
|
||||||
foreach ($gruppenNetto as $satz => $netto) {
|
foreach ($gruppenNetto as $satz => $netto) {
|
||||||
if (abs($satz - 19) < 0.01) {
|
if (abs($satz - 19) < 0.01) {
|
||||||
$habenId = $erloesKonten['8000'] ?? null;
|
$kategorieId = $erloesKonten['8000'] ?? null;
|
||||||
} elseif (abs($satz - 7) < 0.01) {
|
} elseif (abs($satz - 7) < 0.01) {
|
||||||
$habenId = $erloesKonten['8200'] ?? null;
|
$kategorieId = $erloesKonten['8200'] ?? null;
|
||||||
} else {
|
} 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.");
|
throw new \RuntimeException("Erlöskonto für USt-Satz {$satz}% nicht gefunden.");
|
||||||
}
|
}
|
||||||
|
|
||||||
Booking::create([
|
Booking::create([
|
||||||
'beleg_id' => $belegId,
|
'beleg_id' => $belegId,
|
||||||
'buchungs_datum' => $invoice['rechnungs_datum'],
|
'buchungs_datum' => $invoice['rechnungs_datum'],
|
||||||
'soll_konto_id' => $debitorKonto['id'],
|
'buchungstyp' => 'Einnahme',
|
||||||
'haben_konto_id' => $habenId,
|
'kategorie_konto_id' => $kategorieId,
|
||||||
'betrag_netto' => round($netto, 2),
|
'zahlungskonto_id' => $zahlungskonto['id'],
|
||||||
'ust_satz' => $satz,
|
'betrag_netto' => round($netto, 2),
|
||||||
'buchungstext' => 'Ausgangsrechnung ' . $invoice['rechnungs_nr'],
|
'ust_satz' => $satz,
|
||||||
|
'buchungstext' => 'Ausgangsrechnung ' . $invoice['rechnungs_nr'],
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -90,7 +90,7 @@ class Account
|
||||||
{
|
{
|
||||||
// Check if any bookings reference this account
|
// Check if any bookings reference this account
|
||||||
$used = Database::fetchOne(
|
$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]
|
[$id, $id]
|
||||||
);
|
);
|
||||||
if ($used && $used['cnt'] > 0) {
|
if ($used && $used['cnt'] > 0) {
|
||||||
|
|
|
||||||
|
|
@ -12,11 +12,11 @@ class Booking
|
||||||
{
|
{
|
||||||
return Database::fetchAll(
|
return Database::fetchAll(
|
||||||
'SELECT bk.*,
|
'SELECT bk.*,
|
||||||
s.kontonummer as soll_nr, s.bezeichnung as soll_name,
|
k.kontonummer as kategorie_nr, k.bezeichnung as kategorie_name,
|
||||||
h.kontonummer as haben_nr, h.bezeichnung as haben_name
|
z.kontonummer as zahlungs_nr, z.bezeichnung as zahlungs_name
|
||||||
FROM bookings bk
|
FROM bookings bk
|
||||||
JOIN accounts s ON bk.soll_konto_id = s.id
|
JOIN accounts k ON bk.kategorie_konto_id = k.id
|
||||||
JOIN accounts h ON bk.haben_konto_id = h.id
|
JOIN accounts z ON bk.zahlungskonto_id = z.id
|
||||||
WHERE bk.beleg_id = ?
|
WHERE bk.beleg_id = ?
|
||||||
ORDER BY bk.buchungs_datum, bk.id',
|
ORDER BY bk.buchungs_datum, bk.id',
|
||||||
[$belegId]
|
[$belegId]
|
||||||
|
|
@ -37,7 +37,7 @@ class Booking
|
||||||
$params[] = (int)$filter['month'];
|
$params[] = (int)$filter['month'];
|
||||||
}
|
}
|
||||||
if (!empty($filter['konto'])) {
|
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'];
|
||||||
$params[] = (int)$filter['konto'];
|
$params[] = (int)$filter['konto'];
|
||||||
}
|
}
|
||||||
|
|
@ -48,12 +48,12 @@ class Booking
|
||||||
|
|
||||||
$offset = ($page - 1) * $perPage;
|
$offset = ($page - 1) * $perPage;
|
||||||
$sql = 'SELECT bk.*,
|
$sql = 'SELECT bk.*,
|
||||||
s.kontonummer as soll_nr, s.bezeichnung as soll_name,
|
k.kontonummer as kategorie_nr, k.bezeichnung as kategorie_name,
|
||||||
h.kontonummer as haben_nr, h.bezeichnung as haben_name,
|
z.kontonummer as zahlungs_nr, z.bezeichnung as zahlungs_name,
|
||||||
bl.beleg_nr, fy.year as fiscal_year_nr
|
bl.beleg_nr, fy.year as fiscal_year_nr
|
||||||
FROM bookings bk
|
FROM bookings bk
|
||||||
JOIN accounts s ON bk.soll_konto_id = s.id
|
JOIN accounts k ON bk.kategorie_konto_id = k.id
|
||||||
JOIN accounts h ON bk.haben_konto_id = h.id
|
JOIN accounts z ON bk.zahlungskonto_id = z.id
|
||||||
JOIN belege bl ON bk.beleg_id = bl.id
|
JOIN belege bl ON bk.beleg_id = bl.id
|
||||||
JOIN fiscal_years fy ON bk.fiscal_year_id = fy.id
|
JOIN fiscal_years fy ON bk.fiscal_year_id = fy.id
|
||||||
WHERE ' . implode(' AND ', $where) . '
|
WHERE ' . implode(' AND ', $where) . '
|
||||||
|
|
@ -97,15 +97,17 @@ class Booking
|
||||||
$nr = self::generateBuchungsNr();
|
$nr = self::generateBuchungsNr();
|
||||||
|
|
||||||
Database::execute(
|
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)
|
betrag_netto, ust_satz, betrag_ust, betrag_brutto, buchungstext, fiscal_year_id)
|
||||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)',
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)',
|
||||||
[
|
[
|
||||||
$nr,
|
$nr,
|
||||||
(int)$data['beleg_id'],
|
(int)$data['beleg_id'],
|
||||||
$data['buchungs_datum'],
|
$data['buchungs_datum'],
|
||||||
(int)$data['soll_konto_id'],
|
$data['buchungstyp'],
|
||||||
(int)$data['haben_konto_id'],
|
(int)$data['kategorie_konto_id'],
|
||||||
|
(int)$data['zahlungskonto_id'],
|
||||||
$netto,
|
$netto,
|
||||||
(float)($data['ust_satz'] ?? 0),
|
(float)($data['ust_satz'] ?? 0),
|
||||||
$ust,
|
$ust,
|
||||||
|
|
@ -129,15 +131,15 @@ class Booking
|
||||||
if (!$orig) throw new \RuntimeException('Buchung nicht gefunden.');
|
if (!$orig) throw new \RuntimeException('Buchung nicht gefunden.');
|
||||||
if ($orig['is_reversal']) throw new \RuntimeException('Stornobuchungen können nicht storniert werden.');
|
if ($orig['is_reversal']) throw new \RuntimeException('Stornobuchungen können nicht storniert werden.');
|
||||||
|
|
||||||
// Create reversal: swap Soll/Haben, same amounts
|
|
||||||
$reversalData = [
|
$reversalData = [
|
||||||
'beleg_id' => $orig['beleg_id'],
|
'beleg_id' => $orig['beleg_id'],
|
||||||
'buchungs_datum' => date('Y-m-d'),
|
'buchungs_datum' => date('Y-m-d'),
|
||||||
'soll_konto_id' => $orig['haben_konto_id'],
|
'buchungstyp' => $orig['buchungstyp'],
|
||||||
'haben_konto_id' => $orig['soll_konto_id'],
|
'kategorie_konto_id' => $orig['kategorie_konto_id'],
|
||||||
'betrag_netto' => $orig['betrag_netto'],
|
'zahlungskonto_id' => $orig['zahlungskonto_id'],
|
||||||
'ust_satz' => $orig['ust_satz'],
|
'betrag_netto' => $orig['betrag_netto'],
|
||||||
'buchungstext' => 'STORNO zu ' . $orig['buchungs_nr'] . ': ' . $reason,
|
'ust_satz' => $orig['ust_satz'],
|
||||||
|
'buchungstext' => 'STORNO zu ' . $orig['buchungs_nr'] . ': ' . $reason,
|
||||||
];
|
];
|
||||||
|
|
||||||
$year = (int)date('Y', strtotime($reversalData['buchungs_datum']));
|
$year = (int)date('Y', strtotime($reversalData['buchungs_datum']));
|
||||||
|
|
@ -146,15 +148,17 @@ class Booking
|
||||||
$nr = self::generateBuchungsNr();
|
$nr = self::generateBuchungsNr();
|
||||||
|
|
||||||
Database::execute(
|
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)
|
betrag_netto, ust_satz, betrag_ust, betrag_brutto, buchungstext, is_reversal, fiscal_year_id)
|
||||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 1, ?)',
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 1, ?)',
|
||||||
[
|
[
|
||||||
$nr,
|
$nr,
|
||||||
$reversalData['beleg_id'],
|
$reversalData['beleg_id'],
|
||||||
$reversalData['buchungs_datum'],
|
$reversalData['buchungs_datum'],
|
||||||
$reversalData['soll_konto_id'],
|
$reversalData['buchungstyp'],
|
||||||
$reversalData['haben_konto_id'],
|
$reversalData['kategorie_konto_id'],
|
||||||
|
$reversalData['zahlungskonto_id'],
|
||||||
$orig['betrag_netto'],
|
$orig['betrag_netto'],
|
||||||
$orig['ust_satz'],
|
$orig['ust_satz'],
|
||||||
$orig['betrag_ust'],
|
$orig['betrag_ust'],
|
||||||
|
|
@ -186,14 +190,14 @@ class Booking
|
||||||
public static function sumByYear(int $year): array
|
public static function sumByYear(int $year): array
|
||||||
{
|
{
|
||||||
return Database::fetchAll(
|
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_netto) as sum_netto,
|
||||||
SUM(bk.betrag_ust) as sum_ust,
|
SUM(bk.betrag_ust) as sum_ust,
|
||||||
SUM(bk.betrag_brutto) as sum_brutto
|
SUM(bk.betrag_brutto) as sum_brutto
|
||||||
FROM bookings bk
|
FROM bookings bk
|
||||||
JOIN fiscal_years fy ON bk.fiscal_year_id = fy.id
|
JOIN fiscal_years fy ON bk.fiscal_year_id = fy.id
|
||||||
WHERE fy.year = ?
|
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]
|
[$year]
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,7 @@ use Steuerkram\Database;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Berechnet die Einnahmen-Überschuss-Rechnung (EÜR) für ein Geschäftsjahr.
|
* 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
|
class EuerCalculator
|
||||||
{
|
{
|
||||||
|
|
@ -20,14 +20,14 @@ class EuerCalculator
|
||||||
a.kontonummer,
|
a.kontonummer,
|
||||||
a.bezeichnung,
|
a.bezeichnung,
|
||||||
a.kontenart,
|
a.kontenart,
|
||||||
SUM(CASE WHEN bk.haben_konto_id = a.id THEN bk.betrag_netto ELSE 0 END) as haben_summe,
|
bk.buchungstyp,
|
||||||
SUM(CASE WHEN bk.soll_konto_id = a.id THEN bk.betrag_netto ELSE 0 END) as soll_summe,
|
SUM(bk.betrag_netto) as summe_netto,
|
||||||
COUNT(bk.id) as buchungsanzahl
|
COUNT(bk.id) as buchungsanzahl
|
||||||
FROM accounts a
|
FROM bookings bk
|
||||||
JOIN bookings bk ON (bk.haben_konto_id = a.id OR bk.soll_konto_id = a.id)
|
JOIN accounts a ON bk.kategorie_konto_id = a.id
|
||||||
JOIN fiscal_years fy ON bk.fiscal_year_id = fy.id
|
JOIN fiscal_years fy ON bk.fiscal_year_id = fy.id
|
||||||
WHERE fy.year = ? AND bk.is_reversal = 0
|
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
|
HAVING buchungsanzahl > 0
|
||||||
ORDER BY a.kontonummer',
|
ORDER BY a.kontonummer',
|
||||||
[$year]
|
[$year]
|
||||||
|
|
@ -37,12 +37,12 @@ class EuerCalculator
|
||||||
$ausgaben = [];
|
$ausgaben = [];
|
||||||
|
|
||||||
foreach ($rows as $row) {
|
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') {
|
if ($row['buchungstyp'] === 'Einnahme') {
|
||||||
$einnahmen[] = array_merge($row, ['netto' => abs($nettoWert)]);
|
$einnahmen[] = array_merge($row, ['netto' => $netto]);
|
||||||
} elseif ($row['kontenart'] === 'Aufwand') {
|
} elseif ($row['buchungstyp'] === 'Ausgabe') {
|
||||||
$ausgaben[] = array_merge($row, ['netto' => abs($nettoWert)]);
|
$ausgaben[] = array_merge($row, ['netto' => $netto]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -9,15 +9,12 @@ use Steuerkram\Database;
|
||||||
/**
|
/**
|
||||||
* Berechnet alle ELSTER-Kennzahlen (Kz) für die Umsatzsteuervoranmeldung.
|
* Berechnet alle ELSTER-Kennzahlen (Kz) für die Umsatzsteuervoranmeldung.
|
||||||
*
|
*
|
||||||
* Maßgeblich sind ausschließlich ust_satz und betrag_ust der Buchung selbst –
|
* Maßgeblich sind buchungstyp, 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.
|
|
||||||
*
|
*
|
||||||
* Buchungslogik:
|
* Buchungslogik:
|
||||||
* Umsatzsteuer (Kz 81/86): ust_satz > 0 auf Ertrag-Haben-Konten
|
* Umsatzsteuer (Kz 81/86): buchungstyp = 'Einnahme' mit ust_satz > 0
|
||||||
* Vorsteuer (Kz 61/62): ust_satz > 0 auf Aufwand/Aktiv-Soll-Konten
|
* Vorsteuer (Kz 61/62): buchungstyp = 'Ausgabe' mit ust_satz > 0
|
||||||
* Steuerfreie Umsätze (Kz 35): Ertrag-Konten mit ust_satz = 0
|
* Steuerfreie Umsätze (Kz 35): buchungstyp = 'Einnahme' mit ust_satz = 0
|
||||||
*/
|
*/
|
||||||
class UstvaCalculator
|
class UstvaCalculator
|
||||||
{
|
{
|
||||||
|
|
@ -39,15 +36,13 @@ class UstvaCalculator
|
||||||
$bookings = $this->fetchBookings($year, $month);
|
$bookings = $this->fetchBookings($year, $month);
|
||||||
|
|
||||||
foreach ($bookings as $b) {
|
foreach ($bookings as $b) {
|
||||||
$ustSatz = (float)$b['ust_satz'];
|
$ustSatz = (float)$b['ust_satz'];
|
||||||
$betragUst = abs((float)$b['betrag_ust']);
|
$betragUst = abs((float)$b['betrag_ust']);
|
||||||
$betragNetto= abs((float)$b['betrag_netto']);
|
$betragNetto = abs((float)$b['betrag_netto']);
|
||||||
$habenArt = $b['haben_kontenart'] ?? '';
|
$buchungstyp = $b['buchungstyp'];
|
||||||
$sollArt = $b['soll_kontenart'] ?? '';
|
|
||||||
|
|
||||||
// ── Umsatzsteuer (Ausgangsseite) ──────────────────────────────
|
// ── Umsatzsteuer (Einnahmen) ─────────────────────────────
|
||||||
// Kriterium: Haben-Konto = Ertrag UND ust_satz > 0
|
if ($buchungstyp === 'Einnahme' && $ustSatz > 0) {
|
||||||
if ($habenArt === 'Ertrag' && $ustSatz > 0) {
|
|
||||||
if (abs($ustSatz - 19) < 0.01) {
|
if (abs($ustSatz - 19) < 0.01) {
|
||||||
$kz['81']['betrag'] += $betragNetto;
|
$kz['81']['betrag'] += $betragNetto;
|
||||||
} elseif (abs($ustSatz - 7) < 0.01) {
|
} elseif (abs($ustSatz - 7) < 0.01) {
|
||||||
|
|
@ -55,16 +50,13 @@ class UstvaCalculator
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Steuerfreie Umsätze: Haben-Konto = Ertrag UND ust_satz = 0
|
// Steuerfreie Umsätze: Einnahme mit ust_satz = 0
|
||||||
if ($habenArt === 'Ertrag' && $ustSatz == 0) {
|
if ($buchungstyp === 'Einnahme' && $ustSatz == 0) {
|
||||||
$kz['35']['betrag'] += $betragNetto;
|
$kz['35']['betrag'] += $betragNetto;
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Vorsteuer (Eingangsseite) ──────────────────────────────────
|
// ── Vorsteuer (Ausgaben) ─────────────────────────────────
|
||||||
// Kriterium: Soll-Konto = Aufwand oder Aktiv (Vorsteuerkonto)
|
if ($buchungstyp === 'Ausgabe' && $ustSatz > 0 && $betragUst > 0) {
|
||||||
// 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) {
|
|
||||||
if (abs($ustSatz - 19) < 0.01) {
|
if (abs($ustSatz - 19) < 0.01) {
|
||||||
$kz['61']['steuer'] += $betragUst;
|
$kz['61']['steuer'] += $betragUst;
|
||||||
} elseif (abs($ustSatz - 7) < 0.01) {
|
} elseif (abs($ustSatz - 7) < 0.01) {
|
||||||
|
|
@ -92,12 +84,8 @@ class UstvaCalculator
|
||||||
private function fetchBookings(int $year, int $month): array
|
private function fetchBookings(int $year, int $month): array
|
||||||
{
|
{
|
||||||
return Database::fetchAll(
|
return Database::fetchAll(
|
||||||
'SELECT bk.ust_satz, bk.betrag_netto, bk.betrag_ust,
|
'SELECT bk.buchungstyp, bk.ust_satz, bk.betrag_netto, bk.betrag_ust
|
||||||
s.kontenart AS soll_kontenart,
|
|
||||||
h.kontenart AS haben_kontenart
|
|
||||||
FROM bookings bk
|
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
|
JOIN fiscal_years fy ON bk.fiscal_year_id = fy.id
|
||||||
WHERE fy.year = ? AND MONTH(bk.buchungs_datum) = ?
|
WHERE fy.year = ? AND MONTH(bk.buchungs_datum) = ?
|
||||||
AND bk.is_reversal = 0',
|
AND bk.is_reversal = 0',
|
||||||
|
|
|
||||||
|
|
@ -22,7 +22,7 @@
|
||||||
<select id="kontenart" name="kontenart"
|
<select id="kontenart" name="kontenart"
|
||||||
class="<?= isset($errors['kontenart']) ? 'input-error' : '' ?>" required>
|
class="<?= isset($errors['kontenart']) ? 'input-error' : '' ?>" required>
|
||||||
<option value="">– wählen –</option>
|
<option value="">– wählen –</option>
|
||||||
<?php foreach (['Aktiv','Passiv','Aufwand','Ertrag','Neutral'] as $art): ?>
|
<?php foreach (['Betriebseinnahme','Betriebsausgabe','Zahlungskonto','Verbindlichkeit','Privat'] as $art): ?>
|
||||||
<option value="<?= $h($art) ?>"<?= ($account['kontenart'] ?? '') === $art ? ' selected' : '' ?>>
|
<option value="<?= $h($art) ?>"<?= ($account['kontenart'] ?? '') === $art ? ' selected' : '' ?>>
|
||||||
<?= $h($art) ?>
|
<?= $h($art) ?>
|
||||||
</option>
|
</option>
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,7 @@
|
||||||
value="<?= $h($filter['search'] ?? '') ?>" style="width:260px">
|
value="<?= $h($filter['search'] ?? '') ?>" style="width:260px">
|
||||||
<select name="kontenart">
|
<select name="kontenart">
|
||||||
<option value="">Alle Kontenarten</option>
|
<option value="">Alle Kontenarten</option>
|
||||||
<?php foreach (['Aktiv','Passiv','Aufwand','Ertrag','Neutral'] as $art): ?>
|
<?php foreach (['Betriebseinnahme','Betriebsausgabe','Zahlungskonto','Verbindlichkeit','Privat'] as $art): ?>
|
||||||
<option value="<?= $h($art) ?>"<?= ($filter['kontenart'] ?? '') === $art ? ' selected' : '' ?>><?= $h($art) ?></option>
|
<option value="<?= $h($art) ?>"<?= ($filter['kontenart'] ?? '') === $art ? ' selected' : '' ?>><?= $h($art) ?></option>
|
||||||
<?php endforeach; ?>
|
<?php endforeach; ?>
|
||||||
</select>
|
</select>
|
||||||
|
|
@ -44,11 +44,11 @@
|
||||||
<td>
|
<td>
|
||||||
<?php
|
<?php
|
||||||
$badgeClass = match($acc['kontenart']) {
|
$badgeClass = match($acc['kontenart']) {
|
||||||
'Ertrag' => 'badge-green',
|
'Betriebseinnahme' => 'badge-green',
|
||||||
'Aufwand' => 'badge-red',
|
'Betriebsausgabe' => 'badge-red',
|
||||||
'Aktiv' => 'badge-blue',
|
'Zahlungskonto' => 'badge-blue',
|
||||||
'Passiv' => 'badge-yellow',
|
'Verbindlichkeit' => 'badge-yellow',
|
||||||
default => 'badge-gray',
|
default => 'badge-gray',
|
||||||
};
|
};
|
||||||
?>
|
?>
|
||||||
<span class="badge <?= $badgeClass ?>"><?= $h($acc['kontenart']) ?></span>
|
<span class="badge <?= $badgeClass ?>"><?= $h($acc['kontenart']) ?></span>
|
||||||
|
|
|
||||||
|
|
@ -149,8 +149,9 @@ $testMode = Setting::isTestMode();
|
||||||
<tr>
|
<tr>
|
||||||
<th>Buchungsnr.</th>
|
<th>Buchungsnr.</th>
|
||||||
<th>Datum</th>
|
<th>Datum</th>
|
||||||
<th>Soll</th>
|
<th>Typ</th>
|
||||||
<th>Haben</th>
|
<th>Kategorie</th>
|
||||||
|
<th>Zahlungskonto</th>
|
||||||
<th class="td-right">Netto</th>
|
<th class="td-right">Netto</th>
|
||||||
<th class="td-right">USt</th>
|
<th class="td-right">USt</th>
|
||||||
<th class="td-right">Brutto</th>
|
<th class="td-right">Brutto</th>
|
||||||
|
|
@ -166,8 +167,9 @@ $testMode = Setting::isTestMode();
|
||||||
<tr style="<?= ($isReversed || $isReversal) ? 'opacity:.55;background:#fff5f5' : '' ?>">
|
<tr style="<?= ($isReversed || $isReversal) ? 'opacity:.55;background:#fff5f5' : '' ?>">
|
||||||
<td class="td-mono text-sm"><?= $h($bk['buchungs_nr']) ?></td>
|
<td class="td-mono text-sm"><?= $h($bk['buchungs_nr']) ?></td>
|
||||||
<td class="td-muted"><?= View::formatDate($bk['buchungs_datum']) ?></td>
|
<td class="td-muted"><?= View::formatDate($bk['buchungs_datum']) ?></td>
|
||||||
<td class="td-mono text-sm"><?= $h($bk['soll_nr']) ?><br><span class="td-muted"><?= $h($bk['soll_name']) ?></span></td>
|
<td><span class="badge <?= $bk['buchungstyp'] === 'Einnahme' ? 'badge-green' : 'badge-red' ?>"><?= $h($bk['buchungstyp']) ?></span></td>
|
||||||
<td class="td-mono text-sm"><?= $h($bk['haben_nr']) ?><br><span class="td-muted"><?= $h($bk['haben_name']) ?></span></td>
|
<td class="td-mono text-sm"><?= $h($bk['kategorie_nr']) ?><br><span class="td-muted"><?= $h($bk['kategorie_name']) ?></span></td>
|
||||||
|
<td class="td-mono text-sm"><?= $h($bk['zahlungs_nr']) ?><br><span class="td-muted"><?= $h($bk['zahlungs_name']) ?></span></td>
|
||||||
<td class="td-right td-mono"><?= $fmt($bk['betrag_netto']) ?></td>
|
<td class="td-right td-mono"><?= $fmt($bk['betrag_netto']) ?></td>
|
||||||
<td class="td-right td-mono"><?= number_format((float)$bk['ust_satz'], 0) ?> %</td>
|
<td class="td-right td-mono"><?= number_format((float)$bk['ust_satz'], 0) ?> %</td>
|
||||||
<td class="td-right td-mono"><?= $fmt($bk['betrag_brutto']) ?></td>
|
<td class="td-right td-mono"><?= $fmt($bk['betrag_brutto']) ?></td>
|
||||||
|
|
@ -220,7 +222,7 @@ $testMode = Setting::isTestMode();
|
||||||
</tr>
|
</tr>
|
||||||
<?php endforeach; ?>
|
<?php endforeach; ?>
|
||||||
<?php if (empty($bookings)): ?>
|
<?php if (empty($bookings)): ?>
|
||||||
<tr><td colspan="9" class="text-center text-muted" style="padding:1rem">Noch keine Buchungen.</td></tr>
|
<tr><td colspan="10" class="text-center text-muted" style="padding:1rem">Noch keine Buchungen.</td></tr>
|
||||||
<?php endif; ?>
|
<?php endif; ?>
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
|
|
@ -232,6 +234,17 @@ $testMode = Setting::isTestMode();
|
||||||
<?php
|
<?php
|
||||||
$formErrors = $formErrors ?? [];
|
$formErrors = $formErrors ?? [];
|
||||||
$formData = $formData ?? [];
|
$formData = $formData ?? [];
|
||||||
|
|
||||||
|
// Default buchungstyp aus Belegtyp ableiten
|
||||||
|
$defaultTyp = $formData['buchungstyp'] ?? (
|
||||||
|
$beleg['belegtyp'] === 'Ausgangsrechnung' ? 'Einnahme' : 'Ausgabe'
|
||||||
|
);
|
||||||
|
|
||||||
|
// Konten nach Art gruppieren
|
||||||
|
$kontenGruppen = [];
|
||||||
|
foreach ($accounts as $acc) {
|
||||||
|
$kontenGruppen[$acc['kontenart']][] = $acc;
|
||||||
|
}
|
||||||
?>
|
?>
|
||||||
<div class="card">
|
<div class="card">
|
||||||
<div class="card-header">
|
<div class="card-header">
|
||||||
|
|
@ -249,6 +262,25 @@ $testMode = Setting::isTestMode();
|
||||||
<?php endif; ?>
|
<?php endif; ?>
|
||||||
<form method="post" action="/belege/<?= $h($beleg['id']) ?>/bookings">
|
<form method="post" action="/belege/<?= $h($beleg['id']) ?>/bookings">
|
||||||
|
|
||||||
|
<!-- Buchungstyp -->
|
||||||
|
<div class="form-group">
|
||||||
|
<label>Buchungstyp *</label>
|
||||||
|
<div style="display:flex;gap:1rem">
|
||||||
|
<label style="display:flex;align-items:center;gap:.4rem;cursor:pointer">
|
||||||
|
<input type="radio" name="buchungstyp" value="Ausgabe"
|
||||||
|
<?= $defaultTyp === 'Ausgabe' ? 'checked' : '' ?>
|
||||||
|
onchange="filterKategorieKonten(this.value)">
|
||||||
|
<span class="badge badge-red">Betriebsausgabe</span>
|
||||||
|
</label>
|
||||||
|
<label style="display:flex;align-items:center;gap:.4rem;cursor:pointer">
|
||||||
|
<input type="radio" name="buchungstyp" value="Einnahme"
|
||||||
|
<?= $defaultTyp === 'Einnahme' ? 'checked' : '' ?>
|
||||||
|
onchange="filterKategorieKonten(this.value)">
|
||||||
|
<span class="badge badge-green">Betriebseinnahme</span>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="form-grid form-grid-2">
|
<div class="form-grid form-grid-2">
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label>Buchungsdatum *</label>
|
<label>Buchungsdatum *</label>
|
||||||
|
|
@ -266,28 +298,19 @@ $testMode = Setting::isTestMode();
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<?php
|
<!-- Kategorie-Konto -->
|
||||||
// Konten nach Art gruppieren für <optgroup>
|
|
||||||
$kontenGruppen = [];
|
|
||||||
foreach ($accounts as $acc) {
|
|
||||||
$kontenGruppen[$acc['kontenart']][] = $acc;
|
|
||||||
}
|
|
||||||
$gruppenReihenfolge = ['Aufwand', 'Ertrag', 'Aktiv', 'Passiv', 'Neutral'];
|
|
||||||
?>
|
|
||||||
|
|
||||||
<!-- Soll-Konto -->
|
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label for="soll_konto_id">Soll-Konto *
|
<label for="kategorie_konto_id">Kategorie *
|
||||||
<span id="soll_konto_art" class="badge badge-gray" style="margin-left:.5rem;font-size:.7rem"></span>
|
<span id="kategorie_konto_art" class="badge badge-gray" style="margin-left:.5rem;font-size:.7rem"></span>
|
||||||
</label>
|
</label>
|
||||||
<input type="search" placeholder="Kontonr. oder Bezeichnung filtern…" class="konto-filter"
|
<input type="search" placeholder="Kontonr. oder Bezeichnung filtern…" class="konto-filter"
|
||||||
data-target="soll_konto_id" autocomplete="off" style="margin-bottom:.3rem">
|
data-target="kategorie_konto_id" autocomplete="off" style="margin-bottom:.3rem">
|
||||||
<select id="soll_konto_id" name="soll_konto_id" required size="6"
|
<select id="kategorie_konto_id" name="kategorie_konto_id" required size="6"
|
||||||
onchange="showKontoArt('soll_konto_id','soll_konto_art',this)">
|
onchange="showKontoArt('kategorie_konto_id','kategorie_konto_art',this)">
|
||||||
<option value="">– Konto wählen –</option>
|
<option value="">– Konto wählen –</option>
|
||||||
<?php foreach ($gruppenReihenfolge as $art): ?>
|
<?php foreach (['Betriebsausgabe', 'Betriebseinnahme', 'Privat'] as $art): ?>
|
||||||
<?php if (empty($kontenGruppen[$art])) continue; ?>
|
<?php if (empty($kontenGruppen[$art])) continue; ?>
|
||||||
<optgroup label="── <?= $h($art) ?> ──">
|
<optgroup label="── <?= $h($art) ?> ──" data-kontenart="<?= $h($art) ?>">
|
||||||
<?php foreach ($kontenGruppen[$art] as $acc): ?>
|
<?php foreach ($kontenGruppen[$art] as $acc): ?>
|
||||||
<option value="<?= $h($acc['id']) ?>"
|
<option value="<?= $h($acc['id']) ?>"
|
||||||
data-art="<?= $h($acc['kontenart']) ?>"
|
data-art="<?= $h($acc['kontenart']) ?>"
|
||||||
|
|
@ -298,20 +321,19 @@ $testMode = Setting::isTestMode();
|
||||||
</optgroup>
|
</optgroup>
|
||||||
<?php endforeach; ?>
|
<?php endforeach; ?>
|
||||||
</select>
|
</select>
|
||||||
<div class="form-hint">Typisch: <strong>Aufwand</strong>-Konto (Eingangsrechnung) oder <strong>Aktiv</strong>-Konto (Forderung/Bank)</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Haben-Konto -->
|
<!-- Zahlungskonto -->
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label for="haben_konto_id">Haben-Konto *
|
<label for="zahlungskonto_id">Zahlungskonto *
|
||||||
<span id="haben_konto_art" class="badge badge-gray" style="margin-left:.5rem;font-size:.7rem"></span>
|
<span id="zahlungskonto_art" class="badge badge-gray" style="margin-left:.5rem;font-size:.7rem"></span>
|
||||||
</label>
|
</label>
|
||||||
<input type="search" placeholder="Kontonr. oder Bezeichnung filtern…" class="konto-filter"
|
<input type="search" placeholder="Kontonr. oder Bezeichnung filtern…" class="konto-filter"
|
||||||
data-target="haben_konto_id" autocomplete="off" style="margin-bottom:.3rem">
|
data-target="zahlungskonto_id" autocomplete="off" style="margin-bottom:.3rem">
|
||||||
<select id="haben_konto_id" name="haben_konto_id" required size="6"
|
<select id="zahlungskonto_id" name="zahlungskonto_id" required size="4"
|
||||||
onchange="showKontoArt('haben_konto_id','haben_konto_art',this)">
|
onchange="showKontoArt('zahlungskonto_id','zahlungskonto_art',this)">
|
||||||
<option value="">– Konto wählen –</option>
|
<option value="">– Konto wählen –</option>
|
||||||
<?php foreach ($gruppenReihenfolge as $art): ?>
|
<?php foreach (['Zahlungskonto', 'Verbindlichkeit'] as $art): ?>
|
||||||
<?php if (empty($kontenGruppen[$art])) continue; ?>
|
<?php if (empty($kontenGruppen[$art])) continue; ?>
|
||||||
<optgroup label="── <?= $h($art) ?> ──">
|
<optgroup label="── <?= $h($art) ?> ──">
|
||||||
<?php foreach ($kontenGruppen[$art] as $acc): ?>
|
<?php foreach ($kontenGruppen[$art] as $acc): ?>
|
||||||
|
|
@ -324,7 +346,6 @@ $testMode = Setting::isTestMode();
|
||||||
</optgroup>
|
</optgroup>
|
||||||
<?php endforeach; ?>
|
<?php endforeach; ?>
|
||||||
</select>
|
</select>
|
||||||
<div class="form-hint">Typisch: <strong>Passiv</strong>-Konto (Verbindlichkeit) oder <strong>Ertrag</strong>-Konto (Ausgangsrechnung)</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="form-grid form-grid-2">
|
<div class="form-grid form-grid-2">
|
||||||
|
|
@ -360,3 +381,30 @@ $testMode = Setting::isTestMode();
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
function filterKategorieKonten(buchungstyp) {
|
||||||
|
const select = document.getElementById('kategorie_konto_id');
|
||||||
|
const groups = select.querySelectorAll('optgroup');
|
||||||
|
const zielArt = buchungstyp === 'Einnahme' ? 'Betriebseinnahme' : 'Betriebsausgabe';
|
||||||
|
|
||||||
|
groups.forEach(function(g) {
|
||||||
|
const art = g.getAttribute('data-kontenart');
|
||||||
|
if (!art) return;
|
||||||
|
// Privat immer anzeigen, Kategorie nach Typ filtern
|
||||||
|
g.style.display = (art === zielArt || art === 'Privat') ? '' : 'none';
|
||||||
|
});
|
||||||
|
|
||||||
|
// Auswahl zurücksetzen wenn aktuell gewähltes Konto nicht mehr sichtbar
|
||||||
|
if (select.selectedIndex > 0) {
|
||||||
|
const selected = select.options[select.selectedIndex];
|
||||||
|
if (selected && selected.closest('optgroup') && selected.closest('optgroup').style.display === 'none') {
|
||||||
|
select.selectedIndex = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Initial filtern
|
||||||
|
document.addEventListener('DOMContentLoaded', function() {
|
||||||
|
const checked = document.querySelector('input[name="buchungstyp"]:checked');
|
||||||
|
if (checked) filterKategorieKonten(checked.value);
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
|
||||||
|
|
@ -42,8 +42,9 @@
|
||||||
<th>Buchungsnr.</th>
|
<th>Buchungsnr.</th>
|
||||||
<th>Datum</th>
|
<th>Datum</th>
|
||||||
<th>Belegnr.</th>
|
<th>Belegnr.</th>
|
||||||
<th>Soll-Konto</th>
|
<th>Typ</th>
|
||||||
<th>Haben-Konto</th>
|
<th>Kategorie</th>
|
||||||
|
<th>Zahlungskonto</th>
|
||||||
<th class="td-right">Netto</th>
|
<th class="td-right">Netto</th>
|
||||||
<th class="td-right">USt (%)</th>
|
<th class="td-right">USt (%)</th>
|
||||||
<th class="td-right">Brutto</th>
|
<th class="td-right">Brutto</th>
|
||||||
|
|
@ -63,13 +64,14 @@
|
||||||
</td>
|
</td>
|
||||||
<td class="td-muted"><?= View::formatDate($bk['buchungs_datum']) ?></td>
|
<td class="td-muted"><?= View::formatDate($bk['buchungs_datum']) ?></td>
|
||||||
<td><a href="/belege/<?= $h($bk['beleg_id']) ?>" class="td-mono text-sm"><?= $h($bk['beleg_nr']) ?></a></td>
|
<td><a href="/belege/<?= $h($bk['beleg_id']) ?>" class="td-mono text-sm"><?= $h($bk['beleg_nr']) ?></a></td>
|
||||||
|
<td><span class="badge <?= $bk['buchungstyp'] === 'Einnahme' ? 'badge-green' : 'badge-red' ?>"><?= $h($bk['buchungstyp']) ?></span></td>
|
||||||
<td class="td-mono text-sm">
|
<td class="td-mono text-sm">
|
||||||
<?= $h($bk['soll_nr']) ?><br>
|
<?= $h($bk['kategorie_nr']) ?><br>
|
||||||
<span class="td-muted"><?= $h(mb_strimwidth($bk['soll_name'], 0, 25, '…')) ?></span>
|
<span class="td-muted"><?= $h(mb_strimwidth($bk['kategorie_name'], 0, 25, '…')) ?></span>
|
||||||
</td>
|
</td>
|
||||||
<td class="td-mono text-sm">
|
<td class="td-mono text-sm">
|
||||||
<?= $h($bk['haben_nr']) ?><br>
|
<?= $h($bk['zahlungs_nr']) ?><br>
|
||||||
<span class="td-muted"><?= $h(mb_strimwidth($bk['haben_name'], 0, 25, '…')) ?></span>
|
<span class="td-muted"><?= $h(mb_strimwidth($bk['zahlungs_name'], 0, 25, '…')) ?></span>
|
||||||
</td>
|
</td>
|
||||||
<td class="td-right td-mono"><?= View::formatMoney($bk['betrag_netto']) ?></td>
|
<td class="td-right td-mono"><?= View::formatMoney($bk['betrag_netto']) ?></td>
|
||||||
<td class="td-right td-muted"><?= number_format((float)$bk['ust_satz'], 0) ?> %</td>
|
<td class="td-right td-muted"><?= number_format((float)$bk['ust_satz'], 0) ?> %</td>
|
||||||
|
|
@ -93,7 +95,7 @@
|
||||||
</tr>
|
</tr>
|
||||||
<?php endforeach; ?>
|
<?php endforeach; ?>
|
||||||
<?php if (empty($rows)): ?>
|
<?php if (empty($rows)): ?>
|
||||||
<tr><td colspan="<?= $testMode ? 10 : 9 ?>" class="text-center text-muted" style="padding:2rem">Keine Buchungen gefunden.</td></tr>
|
<tr><td colspan="<?= $testMode ? 11 : 10 ?>" class="text-center text-muted" style="padding:2rem">Keine Buchungen gefunden.</td></tr>
|
||||||
<?php endif; ?>
|
<?php endif; ?>
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
|
|
|
||||||
|
|
@ -151,8 +151,9 @@ $fmt = fn($v) => View::formatMoney($v);
|
||||||
<tr>
|
<tr>
|
||||||
<th>Datum</th>
|
<th>Datum</th>
|
||||||
<th>Belegnr.</th>
|
<th>Belegnr.</th>
|
||||||
<th>Soll</th>
|
<th>Typ</th>
|
||||||
<th>Haben</th>
|
<th>Kategorie</th>
|
||||||
|
<th>Zahlungskonto</th>
|
||||||
<th>Buchungstext</th>
|
<th>Buchungstext</th>
|
||||||
<th class="td-right">Brutto</th>
|
<th class="td-right">Brutto</th>
|
||||||
</tr>
|
</tr>
|
||||||
|
|
@ -162,14 +163,15 @@ $fmt = fn($v) => View::formatMoney($v);
|
||||||
<tr>
|
<tr>
|
||||||
<td class="td-muted"><?= View::formatDate($b['buchungs_datum']) ?></td>
|
<td class="td-muted"><?= View::formatDate($b['buchungs_datum']) ?></td>
|
||||||
<td><a href="/belege/<?= $h($b['beleg_id']) ?>"><?= $h($b['beleg_nr']) ?></a></td>
|
<td><a href="/belege/<?= $h($b['beleg_id']) ?>"><?= $h($b['beleg_nr']) ?></a></td>
|
||||||
<td class="td-mono"><?= $h($b['soll_nr']) ?> <span class="text-muted">– <?= $h($b['soll_name']) ?></span></td>
|
<td><span class="badge <?= $b['buchungstyp'] === 'Einnahme' ? 'badge-green' : 'badge-red' ?>"><?= $h($b['buchungstyp']) ?></span></td>
|
||||||
<td class="td-mono"><?= $h($b['haben_nr']) ?> <span class="text-muted">– <?= $h($b['haben_name']) ?></span></td>
|
<td class="td-mono"><?= $h($b['kategorie_nr']) ?> <span class="text-muted">– <?= $h($b['kategorie_name']) ?></span></td>
|
||||||
|
<td class="td-mono"><?= $h($b['zahlungs_nr']) ?> <span class="text-muted">– <?= $h($b['zahlungs_name']) ?></span></td>
|
||||||
<td><?= $h($b['buchungstext']) ?><?php if ($b['is_reversal']): ?> <span class="badge badge-red">Storno</span><?php endif; ?></td>
|
<td><?= $h($b['buchungstext']) ?><?php if ($b['is_reversal']): ?> <span class="badge badge-red">Storno</span><?php endif; ?></td>
|
||||||
<td class="td-right td-mono"><?= $fmt($b['betrag_brutto']) ?></td>
|
<td class="td-right td-mono"><?= $fmt($b['betrag_brutto']) ?></td>
|
||||||
</tr>
|
</tr>
|
||||||
<?php endforeach; ?>
|
<?php endforeach; ?>
|
||||||
<?php if (empty($recentBookings)): ?>
|
<?php if (empty($recentBookings)): ?>
|
||||||
<tr><td colspan="6" class="text-center text-muted" style="padding:2rem">Noch keine Buchungen vorhanden.</td></tr>
|
<tr><td colspan="7" class="text-center text-muted" style="padding:2rem">Noch keine Buchungen vorhanden.</td></tr>
|
||||||
<?php endif; ?>
|
<?php endif; ?>
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
|
|
|
||||||
|
|
@ -15,26 +15,26 @@ class BelegBuchungTest extends TestCase
|
||||||
public function run(): void
|
public function run(): void
|
||||||
{
|
{
|
||||||
// Konten aus der SKR03-Seed-Datei abrufen
|
// Konten aus der SKR03-Seed-Datei abrufen
|
||||||
$aufwandId = $this->findAccountId('4920'); // Bürobedarf (Aufwand)
|
$ausgabeId = $this->findAccountId('4920'); // Bürobedarf (Betriebsausgabe)
|
||||||
$bankId = $this->findAccountId('1200'); // Bank (Aktiv)
|
$bankId = $this->findAccountId('1200'); // Bank (Zahlungskonto)
|
||||||
|
|
||||||
if (!$aufwandId || !$bankId) {
|
if (!$ausgabeId || !$bankId) {
|
||||||
// Fallback: irgendein Aufwand- und Aktiv-Konto suchen
|
// Fallback: irgendein Betriebsausgabe- und Zahlungskonto suchen
|
||||||
$aufwandRow = \Steuerkram\Database::fetchOne(
|
$ausgabeRow = \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"
|
||||||
);
|
);
|
||||||
$bankRow = \Steuerkram\Database::fetchOne(
|
$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;
|
$bankId = $bankRow ? (int)$bankRow['id'] : null;
|
||||||
}
|
}
|
||||||
|
|
||||||
$this->assertNotEmpty($aufwandId, 'Aufwandskonto gefunden');
|
$this->assertNotEmpty($ausgabeId, 'Betriebsausgabe-Konto gefunden');
|
||||||
$this->assertNotEmpty($bankId, 'Aktiv-/Bankkonto gefunden');
|
$this->assertNotEmpty($bankId, 'Zahlungskonto gefunden');
|
||||||
|
|
||||||
if (!$aufwandId || !$bankId) {
|
if (!$ausgabeId || !$bankId) {
|
||||||
return; // Voraussetzungen nicht erfüllt, weitere Asserts wären sinnlos
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Beleg anlegen
|
// Beleg anlegen
|
||||||
|
|
@ -54,15 +54,16 @@ class BelegBuchungTest extends TestCase
|
||||||
]);
|
]);
|
||||||
$this->assertGreaterThan(0, $belegId, 'Beleg für Buchungstest angelegt');
|
$this->assertGreaterThan(0, $belegId, 'Beleg für Buchungstest angelegt');
|
||||||
|
|
||||||
// Buchung erstellen
|
// Buchung erstellen (EÜR: Ausgabe mit Kategorie + Zahlungskonto)
|
||||||
$buchungId = \Steuerkram\Models\Booking::create([
|
$buchungId = \Steuerkram\Models\Booking::create([
|
||||||
'beleg_id' => $belegId,
|
'beleg_id' => $belegId,
|
||||||
'buchungs_datum' => $datum,
|
'buchungs_datum' => $datum,
|
||||||
'soll_konto_id' => $aufwandId,
|
'buchungstyp' => 'Ausgabe',
|
||||||
'haben_konto_id' => $bankId,
|
'kategorie_konto_id' => $ausgabeId,
|
||||||
'betrag_netto' => 50.00,
|
'zahlungskonto_id' => $bankId,
|
||||||
'ust_satz' => 19.0,
|
'betrag_netto' => 50.00,
|
||||||
'buchungstext' => 'Testbuchung Bürobedarf',
|
'ust_satz' => 19.0,
|
||||||
|
'buchungstext' => 'Testbuchung Bürobedarf',
|
||||||
]);
|
]);
|
||||||
$this->assertGreaterThan(0, $buchungId, 'Buchung wurde angelegt (ID > 0)');
|
$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->assertNotEmpty($buchung, 'Booking::find() liefert die Buchung');
|
||||||
$this->assertEquals(50.00, (float)($buchung['betrag_netto'] ?? 0), 'Nettobetrag korrekt');
|
$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(19.0, (float)($buchung['ust_satz'] ?? 0), 'USt-Satz korrekt');
|
||||||
|
$this->assertEquals('Ausgabe', $buchung['buchungstyp'] ?? '', 'Buchungstyp ist Ausgabe');
|
||||||
|
|
||||||
// Buchung via Booking::forBeleg() abrufbar
|
// Buchung via Booking::forBeleg() abrufbar
|
||||||
$buchungen = \Steuerkram\Models\Booking::forBeleg($belegId);
|
$buchungen = \Steuerkram\Models\Booking::forBeleg($belegId);
|
||||||
|
|
@ -93,11 +95,11 @@ class BelegBuchungTest extends TestCase
|
||||||
foreach ($journal['rows'] as $row) {
|
foreach ($journal['rows'] as $row) {
|
||||||
if ((int)$row['id'] === $buchungId) {
|
if ((int)$row['id'] === $buchungId) {
|
||||||
$foundInJournal = true;
|
$foundInJournal = true;
|
||||||
// Vollständigkeit prüfen
|
$this->assertNotEmpty($row['buchungs_nr'], 'Buchungsnummer 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['soll_nr'], 'Soll-Kontonummer im Journal vorhanden');
|
$this->assertNotEmpty($row['zahlungs_nr'], 'Zahlungskonto-Nr. 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['beleg_nr'], 'Belegnummer im Journal vorhanden');
|
$this->assertEquals('Ausgabe', $row['buchungstyp'], 'Buchungstyp im Journal korrekt');
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -16,14 +16,14 @@ class InvoiceBuchungTest extends TestCase
|
||||||
public function run(): void
|
public function run(): void
|
||||||
{
|
{
|
||||||
// Benötigte Konten prüfen (SKR03)
|
// Benötigte Konten prüfen (SKR03)
|
||||||
$debitorId = $this->findAccountId('1400'); // Debitorenkonto
|
$zahlungsId = $this->findAccountId('1400'); // Forderungen (Zahlungskonto)
|
||||||
$erloesId = $this->findAccountId('8000'); // Erlöse 19 %
|
$kategorieId = $this->findAccountId('8000'); // Erlöse 19 % (Betriebseinnahme)
|
||||||
|
|
||||||
if (!$debitorId) {
|
if (!$zahlungsId) {
|
||||||
$this->assert(false, 'Debitorenkonto 1400 nicht gefunden – SKR03-Seed vorhanden?');
|
$this->assert(false, 'Forderungskonto 1400 nicht gefunden – SKR03-Seed vorhanden?');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (!$erloesId) {
|
if (!$kategorieId) {
|
||||||
$this->assert(false, 'Erlöskonto 8000 nicht gefunden – SKR03-Seed vorhanden?');
|
$this->assert(false, 'Erlöskonto 8000 nicht gefunden – SKR03-Seed vorhanden?');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
@ -65,7 +65,7 @@ class InvoiceBuchungTest extends TestCase
|
||||||
$invoice = \Steuerkram\Models\Invoice::find($invoiceId);
|
$invoice = \Steuerkram\Models\Invoice::find($invoiceId);
|
||||||
$this->assertEquals('Gesendet', $invoice['status'] ?? null, 'Rechnungsstatus ist Gesendet');
|
$this->assertEquals('Gesendet', $invoice['status'] ?? null, 'Rechnungsstatus ist Gesendet');
|
||||||
|
|
||||||
// Buchungen erzeugen (wie InvoiceController::createBelegAndBookings)
|
// Buchungen erzeugen (EÜR: Einnahme)
|
||||||
$belegNr = \Steuerkram\Models\Beleg::generateBelegNr();
|
$belegNr = \Steuerkram\Models\Beleg::generateBelegNr();
|
||||||
$belegId = \Steuerkram\Models\Beleg::create([
|
$belegId = \Steuerkram\Models\Beleg::create([
|
||||||
'beleg_nr' => $belegNr,
|
'beleg_nr' => $belegNr,
|
||||||
|
|
@ -91,13 +91,14 @@ class InvoiceBuchungTest extends TestCase
|
||||||
$buchungIds = [];
|
$buchungIds = [];
|
||||||
foreach ($gruppenNetto as $satz => $netto) {
|
foreach ($gruppenNetto as $satz => $netto) {
|
||||||
$buchungIds[] = \Steuerkram\Models\Booking::create([
|
$buchungIds[] = \Steuerkram\Models\Booking::create([
|
||||||
'beleg_id' => $belegId,
|
'beleg_id' => $belegId,
|
||||||
'buchungs_datum' => $invoice['rechnungs_datum'],
|
'buchungs_datum' => $invoice['rechnungs_datum'],
|
||||||
'soll_konto_id' => $debitorId,
|
'buchungstyp' => 'Einnahme',
|
||||||
'haben_konto_id' => $erloesId,
|
'kategorie_konto_id' => $kategorieId,
|
||||||
'betrag_netto' => round($netto, 2),
|
'zahlungskonto_id' => $zahlungsId,
|
||||||
'ust_satz' => $satz,
|
'betrag_netto' => round($netto, 2),
|
||||||
'buchungstext' => 'Ausgangsrechnung ' . $nr,
|
'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(19.0, (float)$row['ust_satz'], 'USt-Satz 19 % korrekt');
|
||||||
$this->assertEquals(190.00, (float)$row['betrag_ust'], 'USt-Betrag 190 € 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->assertEquals(1190.00, (float)$row['betrag_brutto'], 'Bruttobetrag 1190 € korrekt');
|
||||||
$this->assertNotEmpty($row['beleg_nr'], 'Belegnummer im Journal vorhanden');
|
$this->assertNotEmpty($row['beleg_nr'], 'Belegnummer im Journal vorhanden');
|
||||||
$this->assertNotEmpty($row['soll_nr'], 'Soll-Konto im Journal vorhanden');
|
$this->assertNotEmpty($row['kategorie_nr'], 'Kategorie-Konto im Journal vorhanden');
|
||||||
$this->assertNotEmpty($row['haben_nr'], 'Haben-Konto im Journal vorhanden');
|
$this->assertNotEmpty($row['zahlungs_nr'], 'Zahlungskonto im Journal vorhanden');
|
||||||
|
$this->assertEquals('Einnahme', $row['buchungstyp'], 'Buchungstyp ist Einnahme');
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -19,46 +19,46 @@ class TaxSummenTest extends TestCase
|
||||||
$datum = date('Y-m-d');
|
$datum = date('Y-m-d');
|
||||||
|
|
||||||
// Benötigte Konten (SKR03)
|
// Benötigte Konten (SKR03)
|
||||||
$debitorId = $this->findAccountId('1400'); // Forderungen (Aktiv)
|
$zahlungsId = $this->findAccountId('1400'); // Forderungen (Zahlungskonto)
|
||||||
$erloesId = $this->findAccountId('8000'); // Erlöse 19 % (Ertrag)
|
$erloesId = $this->findAccountId('8000'); // Erlöse 19 % (Betriebseinnahme)
|
||||||
$aufwandId = $this->findAccountId('4920'); // Bürobedarf (Aufwand)
|
$ausgabeId = $this->findAccountId('4920'); // Bürobedarf (Betriebsausgabe)
|
||||||
$bankId = $this->findAccountId('1200'); // Bank (Aktiv)
|
$bankId = $this->findAccountId('1200'); // Bank (Zahlungskonto)
|
||||||
|
|
||||||
// Fallback auf beliebige Konten der passenden Art
|
// Fallback auf beliebige Konten der passenden Art
|
||||||
if (!$erloesId) {
|
if (!$erloesId) {
|
||||||
$row = \Steuerkram\Database::fetchOne(
|
$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;
|
$erloesId = $row ? (int)$row['id'] : null;
|
||||||
}
|
}
|
||||||
if (!$aufwandId) {
|
if (!$ausgabeId) {
|
||||||
$row = \Steuerkram\Database::fetchOne(
|
$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(
|
$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) {
|
if (!$bankId) {
|
||||||
$row = \Steuerkram\Database::fetchOne(
|
$row = \Steuerkram\Database::fetchOne(
|
||||||
"SELECT id FROM accounts WHERE kontenart = 'Aktiv' AND aktiv = 1 AND id != ? LIMIT 1",
|
"SELECT id FROM accounts WHERE kontenart = 'Zahlungskonto' AND aktiv = 1 AND id != ? LIMIT 1",
|
||||||
[$debitorId ?? 0]
|
[$zahlungsId ?? 0]
|
||||||
);
|
);
|
||||||
$bankId = $row ? (int)$row['id'] : null;
|
$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?');
|
$this->assert(false, 'Benötigte Konten nicht gefunden – SKR03-Seed vorhanden?');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
$this->assert(true, 'Alle benötigten Konten gefunden');
|
$this->assert(true, 'Alle benötigten Konten gefunden');
|
||||||
|
|
||||||
// ── Ausgangsbuchung (Ertrag, 19 %) ────────────────────────────────
|
// ── Einnahme-Buchung (19 %) ─────────────────────────────────
|
||||||
// Debitor an Erlös 19%: Netto=500, USt=95, Brutto=595
|
// Erlöse 19%: Netto=500, USt=95, Brutto=595
|
||||||
$belegAr = \Steuerkram\Models\Beleg::create([
|
$belegAr = \Steuerkram\Models\Beleg::create([
|
||||||
'beleg_nr' => 'TEST-AR-' . uniqid(),
|
'beleg_nr' => 'TEST-AR-' . uniqid(),
|
||||||
'filename' => 'test-ar.pdf',
|
'filename' => 'test-ar.pdf',
|
||||||
|
|
@ -70,17 +70,18 @@ class TaxSummenTest extends TestCase
|
||||||
'belegtyp' => 'Ausgangsrechnung',
|
'belegtyp' => 'Ausgangsrechnung',
|
||||||
]);
|
]);
|
||||||
\Steuerkram\Models\Booking::create([
|
\Steuerkram\Models\Booking::create([
|
||||||
'beleg_id' => $belegAr,
|
'beleg_id' => $belegAr,
|
||||||
'buchungs_datum' => $datum,
|
'buchungs_datum' => $datum,
|
||||||
'soll_konto_id' => $debitorId,
|
'buchungstyp' => 'Einnahme',
|
||||||
'haben_konto_id' => $erloesId,
|
'kategorie_konto_id' => $erloesId,
|
||||||
'betrag_netto' => 500.00,
|
'zahlungskonto_id' => $zahlungsId,
|
||||||
'ust_satz' => 19.0,
|
'betrag_netto' => 500.00,
|
||||||
'buchungstext' => 'Test Ausgangsbuchung USt 19%',
|
'ust_satz' => 19.0,
|
||||||
|
'buchungstext' => 'Test Einnahme USt 19%',
|
||||||
]);
|
]);
|
||||||
|
|
||||||
// ── Eingangsbuchung (Aufwand, 19 %) ───────────────────────────────
|
// ── Ausgabe-Buchung (19 %) ──────────────────────────────────
|
||||||
// Aufwand an Bank: Netto=200, USt=38, Brutto=238
|
// Bürobedarf: Netto=200, USt=38, Brutto=238
|
||||||
$belegEr = \Steuerkram\Models\Beleg::create([
|
$belegEr = \Steuerkram\Models\Beleg::create([
|
||||||
'beleg_nr' => 'TEST-ER-' . uniqid(),
|
'beleg_nr' => 'TEST-ER-' . uniqid(),
|
||||||
'filename' => 'test-er.pdf',
|
'filename' => 'test-er.pdf',
|
||||||
|
|
@ -92,13 +93,14 @@ class TaxSummenTest extends TestCase
|
||||||
'belegtyp' => 'Eingangsrechnung',
|
'belegtyp' => 'Eingangsrechnung',
|
||||||
]);
|
]);
|
||||||
\Steuerkram\Models\Booking::create([
|
\Steuerkram\Models\Booking::create([
|
||||||
'beleg_id' => $belegEr,
|
'beleg_id' => $belegEr,
|
||||||
'buchungs_datum' => $datum,
|
'buchungs_datum' => $datum,
|
||||||
'soll_konto_id' => $aufwandId,
|
'buchungstyp' => 'Ausgabe',
|
||||||
'haben_konto_id' => $bankId,
|
'kategorie_konto_id' => $ausgabeId,
|
||||||
'betrag_netto' => 200.00,
|
'zahlungskonto_id' => $bankId,
|
||||||
'ust_satz' => 19.0,
|
'betrag_netto' => 200.00,
|
||||||
'buchungstext' => 'Test Eingangsbuchung USt 19%',
|
'ust_satz' => 19.0,
|
||||||
|
'buchungstext' => 'Test Ausgabe USt 19%',
|
||||||
]);
|
]);
|
||||||
|
|
||||||
// ── UStVA prüfen ──────────────────────────────────────────────────
|
// ── UStVA prüfen ──────────────────────────────────────────────────
|
||||||
|
|
@ -108,7 +110,7 @@ class TaxSummenTest extends TestCase
|
||||||
// Kz 81: Steuerpfl. Umsätze 19% – muss mindestens 500 € enthalten
|
// Kz 81: Steuerpfl. Umsätze 19% – muss mindestens 500 € enthalten
|
||||||
$this->assert(
|
$this->assert(
|
||||||
(float)$kz['81']['betrag'] >= 500.00,
|
(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)
|
// 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
|
// Kz 61: Vorsteuer 19% – muss mindestens 38 € enthalten
|
||||||
$this->assert(
|
$this->assert(
|
||||||
(float)$kz['61']['steuer'] >= 38.00,
|
(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
|
// Kz 66: Vorsteuer gesamt – muss ≥ Kz 61 sein
|
||||||
|
|
@ -143,13 +145,13 @@ class TaxSummenTest extends TestCase
|
||||||
// Einnahmen müssen vorhanden sein
|
// Einnahmen müssen vorhanden sein
|
||||||
$this->assert(
|
$this->assert(
|
||||||
(float)$euer['total_einnahmen'] >= 500.00,
|
(float)$euer['total_einnahmen'] >= 500.00,
|
||||||
'EÜR: Einnahmen ≥ 500 € (Ausgangsbuchung eingeflossen)'
|
'EÜR: Einnahmen ≥ 500 € (Einnahme-Buchung eingeflossen)'
|
||||||
);
|
);
|
||||||
|
|
||||||
// Ausgaben müssen vorhanden sein
|
// Ausgaben müssen vorhanden sein
|
||||||
$this->assert(
|
$this->assert(
|
||||||
(float)$euer['total_ausgaben'] >= 200.00,
|
(float)$euer['total_ausgaben'] >= 200.00,
|
||||||
'EÜR: Ausgaben ≥ 200 € (Eingangsbuchung eingeflossen)'
|
'EÜR: Ausgaben ≥ 200 € (Ausgabe-Buchung eingeflossen)'
|
||||||
);
|
);
|
||||||
|
|
||||||
// Gewinn = Einnahmen - Ausgaben
|
// Gewinn = Einnahmen - Ausgaben
|
||||||
|
|
|
||||||
138
tests/06_EuerBuchungsTest.php
Normal file
138
tests/06_EuerBuchungsTest.php
Normal file
|
|
@ -0,0 +1,138 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Test 6: EÜR-Buchungsmodell – Einnahmen und Ausgaben.
|
||||||
|
*
|
||||||
|
* Prüft das EÜR-Buchungsmodell: buchungstyp, kategorie_konto_id, zahlungskonto_id.
|
||||||
|
* Stellt sicher, dass Stornobuchungen korrekt funktionieren und
|
||||||
|
* die Kontenarten den neuen EÜR-Bezeichnungen entsprechen.
|
||||||
|
*/
|
||||||
|
class EuerBuchungsTest extends TestCase
|
||||||
|
{
|
||||||
|
protected string $testName = 'Test 6: EÜR-Buchungsmodell (Einnahme/Ausgabe)';
|
||||||
|
|
||||||
|
public function run(): void
|
||||||
|
{
|
||||||
|
$datum = date('Y-m-d');
|
||||||
|
|
||||||
|
// ── Kontenarten prüfen ───────────────────────────────────────
|
||||||
|
$kontenarten = \Steuerkram\Database::fetchAll(
|
||||||
|
"SELECT DISTINCT kontenart FROM accounts ORDER BY kontenart"
|
||||||
|
);
|
||||||
|
$arten = array_column($kontenarten, 'kontenart');
|
||||||
|
|
||||||
|
$this->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)'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Add table
Reference in a new issue