Fix #18: booked-Status aus Buchungen ableiten statt gespeichertem Flag

This commit is contained in:
fds2610 2026-03-30 21:04:43 +02:00
parent 926d4a5ee0
commit 06c9f184f9
5 changed files with 118 additions and 38 deletions

View file

@ -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;

View file

@ -51,7 +51,7 @@ class AccountController
// Ungebuchte Belege // Ungebuchte Belege
$ungebucht = \Steuerkram\Database::fetchOne( $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); $stats['ungebuchte_belege'] = (int)($ungebucht['cnt'] ?? 0);

View file

@ -27,8 +27,11 @@ class Beleg
$params[] = (int)$filter['month']; $params[] = (int)$filter['month'];
} }
if (isset($filter['booked']) && $filter['booked'] !== '') { if (isset($filter['booked']) && $filter['booked'] !== '') {
$where[] = 'b.booked = ?'; if ((int)$filter['booked'] === 1) {
$params[] = (int)$filter['booked']; $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'])) { if (!empty($filter['belegtyp'])) {
$where[] = 'b.belegtyp = ?'; $where[] = 'b.belegtyp = ?';
@ -41,9 +44,12 @@ class Beleg
(SELECT SUM(bk.betrag_brutto) FROM bookings bk (SELECT SUM(bk.betrag_brutto) FROM bookings bk
WHERE bk.beleg_id = b.id AND bk.is_reversal = 0) WHERE bk.beleg_id = b.id AND bk.is_reversal = 0)
) AS betrag_brutto, ) 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 FROM belege b
LEFT JOIN fiscal_years fy ON b.fiscal_year_id = fy.id 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) . ' WHERE ' . implode(' AND ', $where) . '
ORDER BY b.eingangs_datum DESC, b.id DESC ORDER BY b.eingangs_datum DESC, b.id DESC
LIMIT ? OFFSET ?'; LIMIT ? OFFSET ?';
@ -70,7 +76,8 @@ class Beleg
WHERE bk.beleg_id = b.id AND bk.is_reversal = 0) WHERE bk.beleg_id = b.id AND bk.is_reversal = 0)
) AS betrag_brutto, ) AS betrag_brutto,
fy.year as fiscal_year_nr, 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 FROM belege b
LEFT JOIN fiscal_years fy ON b.fiscal_year_id = fy.id LEFT JOIN fiscal_years fy ON b.fiscal_year_id = fy.id
LEFT JOIN suppliers s ON b.supplier_id = s.id LEFT JOIN suppliers s ON b.supplier_id = s.id
@ -128,7 +135,7 @@ class Beleg
Database::execute( Database::execute(
'UPDATE belege SET beleg_datum=?, rechnungs_nr=?, betrag_brutto=?, belegtyp=?, notes=?, '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', WHERE id=? AND locked=0',
[ [
$data['beleg_datum'] ?: null, $data['beleg_datum'] ?: null,
@ -136,7 +143,6 @@ class Beleg
$data['betrag_brutto'] ?? null, $data['betrag_brutto'] ?? null,
$data['belegtyp'] ?? 'Eingangsrechnung', $data['belegtyp'] ?? 'Eingangsrechnung',
$data['notes'] ?? null, $data['notes'] ?? null,
(int)($data['booked'] ?? 0),
$fyId, $fyId,
$id, $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 public static function delete(int $id): bool
{ {
$beleg = self::find($id); $beleg = self::find($id);

View file

@ -123,12 +123,7 @@ class Booking
] ]
); );
$id = (int)Database::lastInsertId(); return (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;
} }
public static function reverse(int $id, string $reason): int public static function reverse(int $id, string $reason): int
@ -190,7 +185,6 @@ class Booking
): void { ): void {
$acc8922 = Account::findByKontonummer('8922'); $acc8922 = Account::findByKontonummer('8922');
$acc1800 = Account::findByKontonummer('1800'); $acc1800 = Account::findByKontonummer('1800');
$acc1776 = Account::findByKontonummer('1776');
if (!$acc8922 || !$acc1800) { if (!$acc8922 || !$acc1800) {
throw new \RuntimeException('Konten 1800 und/oder 8922 nicht gefunden.'); throw new \RuntimeException('Konten 1800 und/oder 8922 nicht gefunden.');
@ -200,29 +194,18 @@ class Booking
$ust30 = round($betragNetto * $ustSatz / 100 * 0.3, 2); $ust30 = round($betragNetto * $ustSatz / 100 * 0.3, 2);
$isTest = Setting::isTestMode(); $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( self::upsertKorrekturbuchung(
belegId: $belegId, belegId: $belegId,
kategorieId: (int)$acc8922['id'], kategorieId: (int)$acc8922['id'],
zahlungsId: (int)$acc1800['id'], zahlungsId: (int)$acc1800['id'],
betragNetto: $netto30, betragNetto: $netto30,
ustSatz: $ustSatz,
betragUst: $ust30,
buchungsDatum: $buchungsDatum, buchungsDatum: $buchungsDatum,
buchungstext: '30% Eigenanteil (nicht abzugsfähig)', buchungstext: '30% Eigenanteil (nicht abzugsfähig)',
isTest: $isTest, 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( private static function upsertKorrekturbuchung(
@ -230,10 +213,14 @@ class Booking
int $kategorieId, int $kategorieId,
int $zahlungsId, int $zahlungsId,
float $betragNetto, float $betragNetto,
float $ustSatz,
float $betragUst,
string $buchungsDatum, string $buchungsDatum,
string $buchungstext, string $buchungstext,
bool $isTest, bool $isTest,
): void { ): void {
$betragBrutto = round($betragNetto + $betragUst, 2);
$existing = Database::fetchOne( $existing = Database::fetchOne(
'SELECT * FROM bookings 'SELECT * FROM bookings
WHERE beleg_id = ? AND kategorie_konto_id = ? AND zahlungskonto_id = ? WHERE beleg_id = ? AND kategorie_konto_id = ? AND zahlungskonto_id = ?
@ -246,10 +233,10 @@ class Booking
if ($isTest) { if ($isTest) {
Database::execute( Database::execute(
'UPDATE bookings 'UPDATE bookings
SET betrag_netto = ?, betrag_ust = 0, betrag_brutto = ?, SET betrag_netto = ?, betrag_ust = ?, betrag_brutto = ?,
buchungs_datum = ?, buchungstext = ? ust_satz = ?, buchungs_datum = ?, buchungstext = ?
WHERE id = ?', WHERE id = ?',
[$betragNetto, $betragNetto, $buchungsDatum, $buchungstext, $existing['id']] [$betragNetto, $betragUst, $betragBrutto, $ustSatz, $buchungsDatum, $buchungstext, $existing['id']]
); );
} else { } else {
self::reverse((int)$existing['id'], 'Anpassung 30% Eigenanteil'); self::reverse((int)$existing['id'], 'Anpassung 30% Eigenanteil');
@ -260,7 +247,7 @@ class Booking
'kategorie_konto_id' => $kategorieId, 'kategorie_konto_id' => $kategorieId,
'zahlungskonto_id' => $zahlungsId, 'zahlungskonto_id' => $zahlungsId,
'betrag_netto' => $betragNetto, 'betrag_netto' => $betragNetto,
'ust_satz' => 0, 'ust_satz' => $ustSatz,
'steuerart' => 'normal', 'steuerart' => 'normal',
'buchungstext' => $buchungstext, 'buchungstext' => $buchungstext,
]); ]);
@ -273,7 +260,7 @@ class Booking
'kategorie_konto_id' => $kategorieId, 'kategorie_konto_id' => $kategorieId,
'zahlungskonto_id' => $zahlungsId, 'zahlungskonto_id' => $zahlungsId,
'betrag_netto' => $betragNetto, 'betrag_netto' => $betragNetto,
'ust_satz' => 0, 'ust_satz' => $ustSatz,
'steuerart' => 'normal', 'steuerart' => 'normal',
'buchungstext' => $buchungstext, 'buchungstext' => $buchungstext,
]); ]);

View file

@ -0,0 +1,88 @@
<?php
declare(strict_types=1);
use Steuerkram\Models\Beleg;
use Steuerkram\Models\Booking;
use Steuerkram\Database;
class BelegGebuechtStatusTest extends TestCase
{
protected string $testName = 'Test 12: Gebucht-Status aus Buchungen abgeleitet';
public function run(): void
{
$kategorieId = $this->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');
}
}