Fix #18: booked-Status aus Buchungen ableiten statt gespeichertem Flag
This commit is contained in:
parent
926d4a5ee0
commit
06c9f184f9
5 changed files with 118 additions and 38 deletions
4
migrations/014_remove_booked_column.sql
Normal file
4
migrations/014_remove_booked_column.sql
Normal 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;
|
||||
|
|
@ -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);
|
||||
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
]);
|
||||
|
|
|
|||
88
tests/12_BelegGebuechtStatusTest.php
Normal file
88
tests/12_BelegGebuechtStatusTest.php
Normal 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');
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Reference in a new issue