Fix #16: Stornobuchungen mit negierten Beträgen, is_reversal-Filter in Calculatoren entfernt

This commit is contained in:
fds2610 2026-04-01 06:17:11 +02:00
parent 85343b1137
commit aef58154aa
6 changed files with 116 additions and 14 deletions

View file

@ -163,10 +163,10 @@ class Booking
$reversalData['buchungstyp'], $reversalData['buchungstyp'],
$reversalData['kategorie_konto_id'], $reversalData['kategorie_konto_id'],
$reversalData['zahlungskonto_id'], $reversalData['zahlungskonto_id'],
$orig['betrag_netto'], -1 * (float)$orig['betrag_netto'],
$orig['ust_satz'], $orig['ust_satz'],
$orig['betrag_ust'], -1 * (float)$orig['betrag_ust'],
$orig['betrag_brutto'], -1 * (float)$orig['betrag_brutto'],
$reversalData['buchungstext'], $reversalData['buchungstext'],
$fy['id'], $fy['id'],
] ]

View file

@ -35,7 +35,6 @@ class EuerCalculator
JOIN accounts a ON bk.kategorie_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 = ? WHERE fy.year = ?
AND bk.is_reversal = 0
AND a.kontenart IN (\'Betriebseinnahme\', \'Betriebsausgabe\') AND a.kontenart IN (\'Betriebseinnahme\', \'Betriebsausgabe\')
GROUP BY a.id, a.kontonummer, a.bezeichnung, a.kontenart, a.euer_zeile GROUP BY a.id, a.kontonummer, a.bezeichnung, a.kontenart, a.euer_zeile
HAVING buchungsanzahl > 0 HAVING buchungsanzahl > 0
@ -81,7 +80,7 @@ class EuerCalculator
return Database::fetchAll( return Database::fetchAll(
'SELECT bk.id, bk.buchungs_datum, bk.buchungstyp, bk.ust_satz, 'SELECT bk.id, bk.buchungs_datum, bk.buchungstyp, bk.ust_satz,
bk.betrag_netto, bk.betrag_ust, bk.betrag_brutto, bk.buchungstext, bk.betrag_netto, bk.betrag_ust, bk.betrag_brutto, bk.buchungstext,
bk.beleg_id, bl.beleg_nr, bk.is_reversal, bk.beleg_id, bl.beleg_nr,
k.kontonummer AS kategorie_nr, k.bezeichnung AS kategorie_name, k.kontonummer AS kategorie_nr, k.bezeichnung AS kategorie_name,
z.kontonummer AS zahlungs_nr, z.bezeichnung AS zahlungs_name z.kontonummer AS zahlungs_nr, z.bezeichnung AS zahlungs_name
FROM bookings bk FROM bookings bk
@ -89,7 +88,7 @@ class EuerCalculator
LEFT JOIN belege bl ON bk.beleg_id = bl.id LEFT JOIN belege bl ON bk.beleg_id = bl.id
JOIN accounts k ON bk.kategorie_konto_id = k.id JOIN accounts k ON bk.kategorie_konto_id = k.id
JOIN accounts z ON bk.zahlungskonto_id = z.id JOIN accounts z ON bk.zahlungskonto_id = z.id
WHERE fy.year = ? AND bk.kategorie_konto_id = ? AND bk.is_reversal = 0 WHERE fy.year = ? AND bk.kategorie_konto_id = ?
ORDER BY bk.buchungs_datum, bk.id', ORDER BY bk.buchungs_datum, bk.id',
[$year, $kontoId] [$year, $kontoId]
); );

View file

@ -61,7 +61,6 @@ class UstvaCalculator
JOIN fiscal_years fy ON bk.fiscal_year_id = fy.id JOIN fiscal_years fy ON bk.fiscal_year_id = fy.id
JOIN accounts a ON a.id = bk.kategorie_konto_id JOIN accounts a ON a.id = bk.kategorie_konto_id
WHERE fy.year = ? AND MONTH(bk.buchungs_datum) = ? WHERE fy.year = ? AND MONTH(bk.buchungs_datum) = ?
AND bk.is_reversal = 0
AND a.ustva_kz IS NOT NULL AND a.ustva_kz != \'\' AND a.ustva_kz IS NOT NULL AND a.ustva_kz != \'\'
GROUP BY a.ustva_kz', GROUP BY a.ustva_kz',
[$year, $month] [$year, $month]
@ -75,8 +74,8 @@ class UstvaCalculator
]; ];
} }
$n = fn(string $kz): float => abs($byKz[$kz]['netto'] ?? 0.0); $n = fn(string $kz): float => (float)($byKz[$kz]['netto'] ?? 0.0);
$u = fn(string $kz): float => abs($byKz[$kz]['ust'] ?? 0.0); $u = fn(string $kz): float => (float)($byKz[$kz]['ust'] ?? 0.0);
$kz81 = $n('81'); $kz83 = $u('81'); $kz81 = $n('81'); $kz83 = $u('81');
$kz86 = $n('86'); $kz84 = $u('86'); $kz86 = $n('86'); $kz84 = $u('86');
@ -124,7 +123,7 @@ class UstvaCalculator
return Database::fetchAll( return Database::fetchAll(
'SELECT bk.id, bk.buchungs_datum, bk.buchungstyp, bk.steuerart, bk.ust_satz, 'SELECT bk.id, bk.buchungs_datum, bk.buchungstyp, bk.steuerart, bk.ust_satz,
bk.betrag_netto, bk.betrag_ust, bk.betrag_brutto, bk.buchungstext, bk.betrag_netto, bk.betrag_ust, bk.betrag_brutto, bk.buchungstext,
bk.beleg_id, bl.beleg_nr, bk.is_reversal, bk.beleg_id, bl.beleg_nr,
k.kontonummer AS kategorie_nr, k.bezeichnung AS kategorie_name, k.kontonummer AS kategorie_nr, k.bezeichnung AS kategorie_name,
z.kontonummer AS zahlungs_nr, z.bezeichnung AS zahlungs_name z.kontonummer AS zahlungs_nr, z.bezeichnung AS zahlungs_name
FROM bookings bk FROM bookings bk
@ -133,7 +132,6 @@ class UstvaCalculator
JOIN accounts k ON bk.kategorie_konto_id = k.id JOIN accounts k ON bk.kategorie_konto_id = k.id
JOIN accounts z ON bk.zahlungskonto_id = z.id JOIN accounts z ON bk.zahlungskonto_id = z.id
WHERE fy.year = ? AND MONTH(bk.buchungs_datum) = ? WHERE fy.year = ? AND MONTH(bk.buchungs_datum) = ?
AND bk.is_reversal = 0
AND k.ustva_kz IN (' . $placeholders . ') AND k.ustva_kz IN (' . $placeholders . ')
ORDER BY bk.buchungs_datum, bk.id', ORDER BY bk.buchungs_datum, bk.id',
$params $params

View file

@ -46,7 +46,7 @@ $fmt = fn($v) => number_format((float)$v, 2, ',', '.') . ' €';
$sumUst += $ust; $sumUst += $ust;
$sumBrutto += $brutto; $sumBrutto += $brutto;
?> ?>
<tr> <tr<?= $b['is_reversal'] ? ' style="font-style:italic;color:#888"' : '' ?>>
<td class="td-mono text-sm"><?= $h($b['buchungs_datum']) ?></td> <td class="td-mono text-sm"><?= $h($b['buchungs_datum']) ?></td>
<td> <td>
<?php if ($b['beleg_id']): ?> <?php if ($b['beleg_id']): ?>

View file

@ -20,8 +20,9 @@ $ustBeitragKz69 = function(array $b): float {
}; };
// Ausgaben werden als negative Beträge angezeigt (Geld fließt ab). // Ausgaben werden als negative Beträge angezeigt (Geld fließt ab).
// Stornobuchungen haben bereits negative Beträge in der DB – kein abs().
$signedBetrag = fn(array $b, string $field): float => $signedBetrag = fn(array $b, string $field): float =>
$b['buchungstyp'] === 'Ausgabe' ? -abs((float)$b[$field]) : abs((float)$b[$field]); $b['buchungstyp'] === 'Ausgabe' ? -(float)$b[$field] : (float)$b[$field];
?> ?>
<div class="card"> <div class="card">
@ -65,7 +66,7 @@ $signedBetrag = fn(array $b, string $field): float =>
$sumUst += $ust; $sumUst += $ust;
$sumBrutto += $brutto; $sumBrutto += $brutto;
?> ?>
<tr> <tr<?= $b['is_reversal'] ? ' style="font-style:italic;color:#888"' : '' ?>>
<td class="td-mono text-sm"><?= $h($b['buchungs_datum']) ?></td> <td class="td-mono text-sm"><?= $h($b['buchungs_datum']) ?></td>
<td> <td>
<?php if ($b['beleg_id']): ?> <?php if ($b['beleg_id']): ?>

View file

@ -0,0 +1,104 @@
<?php
declare(strict_types=1);
/**
* Test 18: Stornobuchungen in UStVA und EÜR.
*
* Prüft, dass eine stornierte Buchung korrekt mit negativem Betrag angelegt wird
* und sich mit der Originalbuchung in den Berichten gegenseitig auf null aufhebt.
*/
class StornoBuchungTest extends TestCase
{
protected string $testName = 'Test 18: Stornobuchungen in UStVA und EÜR';
public function run(): void
{
$year = (int)date('Y');
$month = (int)date('n');
$datum = date('Y-m-d');
$zahlungsId = $this->findAccountId('1200'); // Bank
$ausgabeId = $this->findAccountId('4920'); // Telefon (ustva_kz=61)
if (!$zahlungsId || !$ausgabeId) {
$this->assert(false, 'Benötigte Konten nicht gefunden – SKR03-Seed vorhanden?');
return;
}
// ── Beleg anlegen ──────────────────────────────────────────────
$belegId = \Steuerkram\Models\Beleg::create([
'beleg_nr' => 'STORNO-TEST-' . uniqid(),
'filename' => 'storno-test.pdf',
'storage_path' => '',
'mime_type' => 'application/pdf',
'file_hash' => hash('sha256', 'storno-test-' . uniqid()),
'beleg_datum' => $datum,
'eingangs_datum' => $datum,
'belegtyp' => 'Ausgangsrechnung',
]);
// ── Ausgabe-Buchung anlegen (Netto=100, USt 19%=19, Brutto=119) ─
$bookingId = \Steuerkram\Models\Booking::create([
'beleg_id' => $belegId,
'buchungs_datum' => $datum,
'buchungstyp' => 'Ausgabe',
'kategorie_konto_id' => $ausgabeId,
'zahlungskonto_id' => $zahlungsId,
'betrag_netto' => 100.00,
'ust_satz' => 19.0,
'buchungstext' => 'Test-Ausgabe für Storno',
]);
$original = \Steuerkram\Models\Booking::find($bookingId);
$this->assertEquals(100.00, round((float)$original['betrag_netto'], 2), 'Originalbuchung: betrag_netto = 100');
$this->assertEquals(19.00, round((float)$original['betrag_ust'], 2), 'Originalbuchung: betrag_ust = 19');
$this->assertEquals(119.00, round((float)$original['betrag_brutto'], 2),'Originalbuchung: betrag_brutto = 119');
// ── Stornieren ────────────────────────────────────────────────
$reversalId = \Steuerkram\Models\Booking::reverse($bookingId, 'Testfall Storno');
$reversal = \Steuerkram\Models\Booking::find($reversalId);
$this->assertEquals(1, (int)$reversal['is_reversal'], 'Stornobuchung: is_reversal = 1');
$this->assertEquals(-100.00, round((float)$reversal['betrag_netto'], 2), 'Stornobuchung: betrag_netto = -100');
$this->assertEquals(-19.00, round((float)$reversal['betrag_ust'], 2), 'Stornobuchung: betrag_ust = -19');
$this->assertEquals(-119.00, round((float)$reversal['betrag_brutto'], 2), 'Stornobuchung: betrag_brutto = -119');
// ── EÜR: storniertes Konto hat Netto-Summe = 0 ───────────────
$euerCalc = new \Steuerkram\Services\EuerCalculator();
$euer = $euerCalc->calculate($year);
$kontoNetto = null;
foreach ($euer['ausgaben'] as $row) {
if ((int)$row['id'] === (int)$ausgabeId) {
$kontoNetto = (float)$row['netto'];
break;
}
}
// Wenn das Konto nicht mehr auftaucht (alle Buchungen = 0), ist das ebenfalls korrekt.
if ($kontoNetto !== null) {
$this->assertEquals(0.0, round($kontoNetto, 2), 'EÜR: Storno + Original = 0 auf Ausgabe-Konto');
} else {
$this->assert(true, 'EÜR: Konto taucht bei Summe=0 nicht auf (HAVING buchungsanzahl > 0 greift)');
}
// ── UStVA: Vorsteuer-KZ hebt sich auf ─────────────────────────
// Kz 61 enthält Vorsteuer aus dem Ausgabe-Konto (ustva_kz='61').
// Original: +19, Storno: -19 → Summe = 0 (oder weniger als andere Tests einbringen).
// Da weitere Tests parallel laufen können, prüfen wir nur, dass die Storno-Buchung
// als eigenständige Zeile in fetchBookingsForKz erscheint.
$ustvaCalc = new \Steuerkram\Services\UstvaCalculator();
$bookings = $ustvaCalc->fetchBookingsForKz($year, $month, '61');
$reversalFound = false;
foreach ($bookings as $b) {
if ((int)$b['id'] === $reversalId) {
$reversalFound = true;
$this->assertEquals(1, (int)$b['is_reversal'], 'UStVA-Drilldown: Stornobuchung is_reversal = 1');
$this->assertEquals(-19.00, round((float)$b['betrag_ust'], 2), 'UStVA-Drilldown: Storno betrag_ust = -19');
break;
}
}
$this->assert($reversalFound, 'UStVA-Drilldown Kz 61: Stornobuchung erscheint in Detailansicht');
}
}