diff --git a/migrations/014_remove_booked_column.sql b/migrations/014_remove_booked_column.sql new file mode 100644 index 0000000..466b193 --- /dev/null +++ b/migrations/014_remove_booked_column.sql @@ -0,0 +1,4 @@ +-- Migration 014: booked-Spalte aus belege entfernen +-- Der gebuchte Status wird jetzt aus dem Vorhandensein von Buchungen abgeleitet. + +ALTER TABLE belege DROP COLUMN IF EXISTS booked; diff --git a/src/Controllers/AccountController.php b/src/Controllers/AccountController.php index e207983..37124e3 100644 --- a/src/Controllers/AccountController.php +++ b/src/Controllers/AccountController.php @@ -51,7 +51,7 @@ class AccountController // Ungebuchte Belege $ungebucht = \Steuerkram\Database::fetchOne( - 'SELECT COUNT(*) as cnt FROM belege WHERE booked=0' + 'SELECT COUNT(*) as cnt FROM belege WHERE NOT EXISTS (SELECT 1 FROM bookings WHERE beleg_id = belege.id)' ); $stats['ungebuchte_belege'] = (int)($ungebucht['cnt'] ?? 0); diff --git a/src/Models/Beleg.php b/src/Models/Beleg.php index 5ab34ae..6de166f 100644 --- a/src/Models/Beleg.php +++ b/src/Models/Beleg.php @@ -27,8 +27,11 @@ class Beleg $params[] = (int)$filter['month']; } if (isset($filter['booked']) && $filter['booked'] !== '') { - $where[] = 'b.booked = ?'; - $params[] = (int)$filter['booked']; + if ((int)$filter['booked'] === 1) { + $where[] = 'EXISTS (SELECT 1 FROM bookings bk2 WHERE bk2.beleg_id = b.id)'; + } else { + $where[] = 'NOT EXISTS (SELECT 1 FROM bookings bk2 WHERE bk2.beleg_id = b.id)'; + } } if (!empty($filter['belegtyp'])) { $where[] = 'b.belegtyp = ?'; @@ -41,9 +44,12 @@ class Beleg (SELECT SUM(bk.betrag_brutto) FROM bookings bk WHERE bk.beleg_id = b.id AND bk.is_reversal = 0) ) AS betrag_brutto, - fy.year as fiscal_year_nr + fy.year as fiscal_year_nr, + s.name as supplier_name, + (EXISTS (SELECT 1 FROM bookings bk3 WHERE bk3.beleg_id = b.id)) AS booked FROM belege b LEFT JOIN fiscal_years fy ON b.fiscal_year_id = fy.id + LEFT JOIN suppliers s ON b.supplier_id = s.id WHERE ' . implode(' AND ', $where) . ' ORDER BY b.eingangs_datum DESC, b.id DESC LIMIT ? OFFSET ?'; @@ -70,7 +76,8 @@ class Beleg WHERE bk.beleg_id = b.id AND bk.is_reversal = 0) ) AS betrag_brutto, fy.year as fiscal_year_nr, - s.name as supplier_name + s.name as supplier_name, + (EXISTS (SELECT 1 FROM bookings bk2 WHERE bk2.beleg_id = b.id)) AS booked FROM belege b LEFT JOIN fiscal_years fy ON b.fiscal_year_id = fy.id LEFT JOIN suppliers s ON b.supplier_id = s.id @@ -128,7 +135,7 @@ class Beleg Database::execute( 'UPDATE belege SET beleg_datum=?, rechnungs_nr=?, betrag_brutto=?, belegtyp=?, notes=?, - booked=?, fiscal_year_id=?, updated_at=NOW() + fiscal_year_id=?, updated_at=NOW() WHERE id=? AND locked=0', [ $data['beleg_datum'] ?: null, @@ -136,7 +143,6 @@ class Beleg $data['betrag_brutto'] ?? null, $data['belegtyp'] ?? 'Eingangsrechnung', $data['notes'] ?? null, - (int)($data['booked'] ?? 0), $fyId, $id, ] @@ -174,11 +180,6 @@ class Beleg ); } - public static function markBooked(int $id, bool $booked): void - { - Database::execute('UPDATE belege SET booked=?, updated_at=NOW() WHERE id=?', [(int)$booked, $id]); - } - public static function delete(int $id): bool { $beleg = self::find($id); diff --git a/src/Models/Booking.php b/src/Models/Booking.php index 04c9d0a..dfafd98 100644 --- a/src/Models/Booking.php +++ b/src/Models/Booking.php @@ -123,12 +123,7 @@ class Booking ] ); - $id = (int)Database::lastInsertId(); - - // Mark Beleg as booked - Database::execute('UPDATE belege SET booked=1, updated_at=NOW() WHERE id=?', [(int)$data['beleg_id']]); - - return $id; + return (int)Database::lastInsertId(); } public static function reverse(int $id, string $reason): int @@ -190,7 +185,6 @@ class Booking ): void { $acc8922 = Account::findByKontonummer('8922'); $acc1800 = Account::findByKontonummer('1800'); - $acc1776 = Account::findByKontonummer('1776'); if (!$acc8922 || !$acc1800) { throw new \RuntimeException('Konten 1800 und/oder 8922 nicht gefunden.'); @@ -200,29 +194,18 @@ class Booking $ust30 = round($betragNetto * $ustSatz / 100 * 0.3, 2); $isTest = Setting::isTestMode(); - // Buchung 1: 30% Netto an 8922 (Eigenanteil Betriebsausgabe) + // Einzige Buchung: 30% Brutto an 8922 inkl. USt-Anteil → fließt korrekt in UStVA KZ 81/83 self::upsertKorrekturbuchung( belegId: $belegId, kategorieId: (int)$acc8922['id'], zahlungsId: (int)$acc1800['id'], betragNetto: $netto30, + ustSatz: $ustSatz, + betragUst: $ust30, buchungsDatum: $buchungsDatum, buchungstext: '30% Eigenanteil (nicht abzugsfähig)', isTest: $isTest, ); - - // Buchung 2: 30% USt-Korrektur 1800→1776 (nur wenn USt > 0) - if ($ustSatz > 0 && $acc1776) { - self::upsertKorrekturbuchung( - belegId: $belegId, - kategorieId: (int)$acc1776['id'], - zahlungsId: (int)$acc1800['id'], - betragNetto: $ust30, - buchungsDatum: $buchungsDatum, - buchungstext: '30% Vorsteuerkorrektur Eigenanteil', - isTest: $isTest, - ); - } } private static function upsertKorrekturbuchung( @@ -230,10 +213,14 @@ class Booking int $kategorieId, int $zahlungsId, float $betragNetto, + float $ustSatz, + float $betragUst, string $buchungsDatum, string $buchungstext, bool $isTest, ): void { + $betragBrutto = round($betragNetto + $betragUst, 2); + $existing = Database::fetchOne( 'SELECT * FROM bookings WHERE beleg_id = ? AND kategorie_konto_id = ? AND zahlungskonto_id = ? @@ -246,10 +233,10 @@ class Booking if ($isTest) { Database::execute( 'UPDATE bookings - SET betrag_netto = ?, betrag_ust = 0, betrag_brutto = ?, - buchungs_datum = ?, buchungstext = ? + SET betrag_netto = ?, betrag_ust = ?, betrag_brutto = ?, + ust_satz = ?, buchungs_datum = ?, buchungstext = ? WHERE id = ?', - [$betragNetto, $betragNetto, $buchungsDatum, $buchungstext, $existing['id']] + [$betragNetto, $betragUst, $betragBrutto, $ustSatz, $buchungsDatum, $buchungstext, $existing['id']] ); } else { self::reverse((int)$existing['id'], 'Anpassung 30% Eigenanteil'); @@ -260,7 +247,7 @@ class Booking 'kategorie_konto_id' => $kategorieId, 'zahlungskonto_id' => $zahlungsId, 'betrag_netto' => $betragNetto, - 'ust_satz' => 0, + 'ust_satz' => $ustSatz, 'steuerart' => 'normal', 'buchungstext' => $buchungstext, ]); @@ -273,7 +260,7 @@ class Booking 'kategorie_konto_id' => $kategorieId, 'zahlungskonto_id' => $zahlungsId, 'betrag_netto' => $betragNetto, - 'ust_satz' => 0, + 'ust_satz' => $ustSatz, 'steuerart' => 'normal', 'buchungstext' => $buchungstext, ]); diff --git a/tests/12_BelegGebuechtStatusTest.php b/tests/12_BelegGebuechtStatusTest.php new file mode 100644 index 0000000..6e3860e --- /dev/null +++ b/tests/12_BelegGebuechtStatusTest.php @@ -0,0 +1,88 @@ +findAccountId('4980'); + $zahlungsId = $this->findAccountId('1800'); + + if (!$kategorieId || !$zahlungsId) { + $this->assert(false, 'Benötigte Konten (4980, 1800) nicht gefunden'); + return; + } + + // Beleg anlegen + $belegId = Beleg::create([ + 'beleg_nr' => 'TEST-' . uniqid(), + 'filename' => 'test.pdf', + 'storage_path' => 'uploads/test.pdf', + 'mime_type' => 'application/pdf', + 'file_hash' => sha1('test'), + 'beleg_datum' => date('Y-m-d'), + 'eingangs_datum'=> date('Y-m-d'), + 'belegtyp' => 'Eingangsrechnung', + 'betrag_brutto' => 119.00, + ]); + $this->assertGreaterThan(0, $belegId, 'Beleg angelegt (ID > 0)'); + + // Vor Buchung: booked = 0 + $beleg = Beleg::find($belegId); + $this->assertEquals(0, (int)($beleg['booked'] ?? 0), 'Vor Buchung: booked = 0'); + + // Buchung anlegen + $buchungId = Booking::create([ + 'beleg_id' => $belegId, + 'buchungs_datum' => date('Y-m-d'), + 'buchungstyp' => 'Ausgabe', + 'kategorie_konto_id' => $kategorieId, + 'zahlungskonto_id' => $zahlungsId, + 'betrag_netto' => 100.00, + 'ust_satz' => 19, + 'steuerart' => 'normal', + 'buchungstext' => 'Test Buchung', + ]); + $this->assertGreaterThan(0, $buchungId, 'Buchung angelegt (ID > 0)'); + + // Nach Buchung: booked = 1 + $beleg = Beleg::find($belegId); + $this->assertEquals(1, (int)($beleg['booked'] ?? 0), 'Nach Buchung: booked = 1 (abgeleitet)'); + + // Buchung löschen + Database::execute('DELETE FROM bookings WHERE id = ?', [$buchungId]); + + // Nach Löschen der Buchung: booked = 0 + $beleg = Beleg::find($belegId); + $this->assertEquals(0, (int)($beleg['booked'] ?? 0), 'Nach Löschen der Buchung: booked = 0'); + + // Belegübersicht: Filter booked=1 enthält den Beleg nicht mehr + $result = Beleg::all(['booked' => '1']); + $found = false; + foreach ($result['rows'] as $row) { + if ((int)$row['id'] === $belegId) { + $found = true; + break; + } + } + $this->assert(!$found, 'Beleg nicht in booked=1-Filter nach Buchungslöschung'); + + // Filter booked=0 enthält den Beleg + $result = Beleg::all(['booked' => '0']); + $found = false; + foreach ($result['rows'] as $row) { + if ((int)$row['id'] === $belegId) { + $found = true; + break; + } + } + $this->assert($found, 'Beleg erscheint in booked=0-Filter nach Buchungslöschung'); + } +}