Merge pull request 'Fix: Belegsumme Konto-1800-Umbuchungen ausblenden + FinTS-Verbesserungen' (#89) from fds2610-bot/Steuerkram:main into main

Reviewed-on: https://codeberg.org/fds2610/Steuerkram/pulls/89
This commit is contained in:
fds2610 2026-05-23 16:51:37 +02:00
commit 11ab2c9abd
4 changed files with 134 additions and 5 deletions

View file

@ -198,6 +198,12 @@ class BookingController extends BaseController
$ustSatz = (float)($data['ust_satz'] ?? 0);
$brutto = round($netto * (1 + $ustSatz / 100), 2);
BankTransaction::linkBeleg($bankTxId, $belegId, $brutto);
} else {
// Falls Beleg bereits verknüpft ist (z.B. nach Storno + Neubuchung), Betrag aktualisieren
$existingBt = BankTransaction::findByBelegId($belegId);
if ($existingBt) {
BankTransaction::linkBeleg((int)$existingBt['id'], $belegId, $totalBrutto);
}
}
if ($newAssetId !== null) {
@ -308,6 +314,14 @@ class BookingController extends BaseController
try {
Booking::reverse($id, $reason);
$booking = Booking::find($id);
// Bankzuordnung auf neue (stornierte) Buchungssumme setzen
if (!empty($booking['beleg_id'])) {
$existingBt = BankTransaction::findByBelegId((int)$booking['beleg_id']);
if ($existingBt) {
$newSum = Booking::sumBruttoForBeleg((int)$booking['beleg_id']);
BankTransaction::linkBeleg((int)$existingBt['id'], (int)$booking['beleg_id'], $newSum);
}
}
View::redirect("/belege/{$booking['beleg_id']}", 'Buchung wurde storniert.');
} catch (\PDOException $e) {
error_log('[Steuerkram] PDO-Fehler in BookingController::reverse(): ' . $e->getMessage());

View file

@ -19,7 +19,8 @@ class Beleg
OR s.name LIKE ?
OR b.bewirtungsgrund LIKE ? OR b.teilnehmerliste LIKE ?
OR (SELECT CAST(SUM(bk2s.betrag_brutto) AS CHAR) FROM bookings bk2s
WHERE bk2s.beleg_id = b.id AND bk2s.is_reversal = 0 AND bk2s.parent_booking_id IS NULL) LIKE ?)';
WHERE bk2s.beleg_id = b.id AND bk2s.is_reversal = 0 AND bk2s.parent_booking_id IS NULL
AND NOT EXISTS (SELECT 1 FROM accounts az WHERE az.id = bk2s.zahlungskonto_id AND az.kontonummer = \'1800\')) LIKE ?)';
$s = '%' . $filter['search'] . '%';
$sBrutto = '%' . str_replace(',', '.', $filter['search']) . '%';
$params = array_merge($params, [$s, $s, $s, $s, $s, $s, $s, $s, $sBrutto]);
@ -47,7 +48,8 @@ class Beleg
$offset = ($page - 1) * $perPage;
$sql = 'SELECT b.*,
(SELECT SUM(bk.betrag_brutto) FROM bookings bk
WHERE bk.beleg_id = b.id AND bk.is_reversal = 0 AND bk.parent_booking_id IS NULL
WHERE bk.beleg_id = b.id AND bk.is_reversal = 0 AND bk.parent_booking_id IS NULL AND bk.reversed_by IS NULL
AND NOT EXISTS (SELECT 1 FROM accounts az WHERE az.id = bk.zahlungskonto_id AND az.kontonummer = \'1800\')
) AS betrag_brutto,
fy.year as fiscal_year_nr,
s.name as supplier_name,
@ -78,7 +80,8 @@ class Beleg
'SELECT b.*,
COALESCE(
(SELECT SUM(bk.betrag_brutto) FROM bookings bk
WHERE bk.beleg_id = b.id AND bk.is_reversal = 0 AND bk.parent_booking_id IS NULL),
WHERE bk.beleg_id = b.id AND bk.is_reversal = 0 AND bk.parent_booking_id IS NULL AND bk.reversed_by IS NULL
AND NOT EXISTS (SELECT 1 FROM accounts az WHERE az.id = bk.zahlungskonto_id AND az.kontonummer = \'1800\')),
b.betrag_brutto
) AS betrag_brutto,
fy.year as fiscal_year_nr,
@ -121,7 +124,8 @@ class Beleg
return Database::fetchOne(
'SELECT b.*,
(SELECT SUM(bk.betrag_brutto) FROM bookings bk
WHERE bk.beleg_id = b.id AND bk.is_reversal = 0 AND bk.parent_booking_id IS NULL
WHERE bk.beleg_id = b.id AND bk.is_reversal = 0 AND bk.parent_booking_id IS NULL AND bk.reversed_by IS NULL
AND NOT EXISTS (SELECT 1 FROM accounts az WHERE az.id = bk.zahlungskonto_id AND az.kontonummer = \'1800\')
) AS betrag_brutto,
fy.year as fiscal_year_nr,
s.name as supplier_name,

View file

@ -433,7 +433,7 @@ class Booking
$row = Database::fetchOne(
'SELECT COALESCE(SUM(betrag_brutto), 0) AS total
FROM bookings
WHERE beleg_id = ? AND is_reversal = 0 AND parent_booking_id IS NULL',
WHERE beleg_id = ? AND is_reversal = 0 AND parent_booking_id IS NULL AND reversed_by IS NULL',
[$belegId]
);
return round((float)($row['total'] ?? 0), 2);

View file

@ -0,0 +1,111 @@
<?php
declare(strict_types=1);
use Steuerkram\Models\Beleg;
use Steuerkram\Models\Booking;
/**
* Test 40: Belegsumme ignoriert steuerliche Umbuchungen auf Konto 1800.
*
* Eine Buchung mit zahlungskonto_id = 1800 ist eine steuerliche Wirksamkeitsumbuchung
* (z.B. Vorsteuerkorrektur). Sie darf nicht in die angezeigte Belegsumme einfließen,
* weil sie keine Gegenbuchung erzeugt und den Betrag künstlich erhöht.
*/
class BelegBruttoBereinigungTest extends TestCase
{
protected string $testName = 'Test 40: Belegsumme – Umbuchungen auf Konto 1800 ausgeblendet';
public function run(): void
{
$bankId = $this->findAccountId('1200');
$umbuchungId = $this->findAccountId('1800');
$ausgabeId = $this->findAccountId('4920');
if (!$bankId || !$umbuchungId || !$ausgabeId) {
$this->assert(false, 'Benötigte Konten (1200, 1800, 4920) fehlen – SKR03-Seed vorhanden?');
return;
}
$datum = date('Y-m-d');
$belegId = Beleg::create([
'beleg_nr' => 'TEST-1800-' . uniqid(),
'filename' => 'umbuchung-test.pdf',
'storage_path' => '',
'mime_type' => 'application/pdf',
'file_hash' => hash('sha256', 'umbuchung-test-' . uniqid()),
'beleg_datum' => $datum,
'belegtyp' => 'Eingangsrechnung',
]);
$this->assertGreaterThan(0, $belegId, 'Beleg angelegt');
// Normale Buchung: 100 € netto + 19 % = 119 € brutto
Booking::create([
'beleg_id' => $belegId,
'buchungs_datum' => $datum,
'buchungstyp' => 'Ausgabe',
'kategorie_konto_id' => $ausgabeId,
'zahlungskonto_id' => $bankId,
'betrag_netto' => 100.00,
'ust_satz' => 19.0,
'buchungstext' => 'Normale Buchung',
]);
// Steuerliche Umbuchung auf Konto 1800 (darf nicht in Belegsumme einfließen)
Booking::create([
'beleg_id' => $belegId,
'buchungs_datum' => $datum,
'buchungstyp' => 'Ausgabe',
'kategorie_konto_id' => $ausgabeId,
'zahlungskonto_id' => $umbuchungId,
'betrag_netto' => 50.00,
'ust_satz' => 0.0,
'buchungstext' => 'Steuerliche Umbuchung 1800',
]);
// Beleg::find() darf nur 119,00 liefern (nicht 169,00)
$beleg = Beleg::find($belegId);
$this->assertEquals(
119.00,
round((float)($beleg['betrag_brutto'] ?? 0), 2),
'Beleg::find() – betrag_brutto = 119,00 (ohne Konto-1800-Umbuchung)'
);
// Beleg::all() – dieselbe Filterung in der Listenansicht
$result = Beleg::all([], 1, 9999);
$found = null;
foreach ($result['rows'] as $row) {
if ((int)$row['id'] === $belegId) {
$found = $row;
break;
}
}
$this->assert($found !== null, 'Beleg erscheint in Beleg::all()');
$this->assertEquals(
119.00,
round((float)($found['betrag_brutto'] ?? 0), 2),
'Beleg::all() – betrag_brutto = 119,00 (ohne Konto-1800-Umbuchung)'
);
// Brutto-Suche findet den Beleg über 119,00 – nicht über 169,00
$resHit = Beleg::all(['search' => '119.00'], 1, 9999);
$foundByBrutto = false;
foreach ($resHit['rows'] as $row) {
if ((int)$row['id'] === $belegId) {
$foundByBrutto = true;
break;
}
}
$this->assert($foundByBrutto, 'Brutto-Suche nach 119.00 findet den Beleg');
$resMiss = Beleg::all(['search' => '169.00'], 1, 9999);
$foundByWrong = false;
foreach ($resMiss['rows'] as $row) {
if ((int)$row['id'] === $belegId) {
$foundByWrong = true;
break;
}
}
$this->assert(!$foundByWrong, 'Brutto-Suche nach 169.00 findet den Beleg NICHT');
}
}