Fix #16: Stornobuchungen mit negierten Beträgen, is_reversal-Filter in Calculatoren entfernt
This commit is contained in:
parent
85343b1137
commit
aef58154aa
6 changed files with 116 additions and 14 deletions
|
|
@ -163,10 +163,10 @@ class Booking
|
|||
$reversalData['buchungstyp'],
|
||||
$reversalData['kategorie_konto_id'],
|
||||
$reversalData['zahlungskonto_id'],
|
||||
$orig['betrag_netto'],
|
||||
-1 * (float)$orig['betrag_netto'],
|
||||
$orig['ust_satz'],
|
||||
$orig['betrag_ust'],
|
||||
$orig['betrag_brutto'],
|
||||
-1 * (float)$orig['betrag_ust'],
|
||||
-1 * (float)$orig['betrag_brutto'],
|
||||
$reversalData['buchungstext'],
|
||||
$fy['id'],
|
||||
]
|
||||
|
|
|
|||
|
|
@ -35,7 +35,6 @@ class EuerCalculator
|
|||
JOIN accounts a ON bk.kategorie_konto_id = a.id
|
||||
JOIN fiscal_years fy ON bk.fiscal_year_id = fy.id
|
||||
WHERE fy.year = ?
|
||||
AND bk.is_reversal = 0
|
||||
AND a.kontenart IN (\'Betriebseinnahme\', \'Betriebsausgabe\')
|
||||
GROUP BY a.id, a.kontonummer, a.bezeichnung, a.kontenart, a.euer_zeile
|
||||
HAVING buchungsanzahl > 0
|
||||
|
|
@ -81,7 +80,7 @@ class EuerCalculator
|
|||
return Database::fetchAll(
|
||||
'SELECT bk.id, bk.buchungs_datum, bk.buchungstyp, bk.ust_satz,
|
||||
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,
|
||||
z.kontonummer AS zahlungs_nr, z.bezeichnung AS zahlungs_name
|
||||
FROM bookings bk
|
||||
|
|
@ -89,7 +88,7 @@ class EuerCalculator
|
|||
LEFT JOIN belege bl ON bk.beleg_id = bl.id
|
||||
JOIN accounts k ON bk.kategorie_konto_id = k.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',
|
||||
[$year, $kontoId]
|
||||
);
|
||||
|
|
|
|||
|
|
@ -61,7 +61,6 @@ class UstvaCalculator
|
|||
JOIN fiscal_years fy ON bk.fiscal_year_id = fy.id
|
||||
JOIN accounts a ON a.id = bk.kategorie_konto_id
|
||||
WHERE fy.year = ? AND MONTH(bk.buchungs_datum) = ?
|
||||
AND bk.is_reversal = 0
|
||||
AND a.ustva_kz IS NOT NULL AND a.ustva_kz != \'\'
|
||||
GROUP BY a.ustva_kz',
|
||||
[$year, $month]
|
||||
|
|
@ -75,8 +74,8 @@ class UstvaCalculator
|
|||
];
|
||||
}
|
||||
|
||||
$n = fn(string $kz): float => abs($byKz[$kz]['netto'] ?? 0.0);
|
||||
$u = fn(string $kz): float => abs($byKz[$kz]['ust'] ?? 0.0);
|
||||
$n = fn(string $kz): float => (float)($byKz[$kz]['netto'] ?? 0.0);
|
||||
$u = fn(string $kz): float => (float)($byKz[$kz]['ust'] ?? 0.0);
|
||||
|
||||
$kz81 = $n('81'); $kz83 = $u('81');
|
||||
$kz86 = $n('86'); $kz84 = $u('86');
|
||||
|
|
@ -124,7 +123,7 @@ class UstvaCalculator
|
|||
return Database::fetchAll(
|
||||
'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.beleg_id, bl.beleg_nr,
|
||||
bk.is_reversal, bk.beleg_id, bl.beleg_nr,
|
||||
k.kontonummer AS kategorie_nr, k.bezeichnung AS kategorie_name,
|
||||
z.kontonummer AS zahlungs_nr, z.bezeichnung AS zahlungs_name
|
||||
FROM bookings bk
|
||||
|
|
@ -133,7 +132,6 @@ class UstvaCalculator
|
|||
JOIN accounts k ON bk.kategorie_konto_id = k.id
|
||||
JOIN accounts z ON bk.zahlungskonto_id = z.id
|
||||
WHERE fy.year = ? AND MONTH(bk.buchungs_datum) = ?
|
||||
AND bk.is_reversal = 0
|
||||
AND k.ustva_kz IN (' . $placeholders . ')
|
||||
ORDER BY bk.buchungs_datum, bk.id',
|
||||
$params
|
||||
|
|
|
|||
|
|
@ -46,7 +46,7 @@ $fmt = fn($v) => number_format((float)$v, 2, ',', '.') . ' €';
|
|||
$sumUst += $ust;
|
||||
$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>
|
||||
<?php if ($b['beleg_id']): ?>
|
||||
|
|
|
|||
|
|
@ -20,8 +20,9 @@ $ustBeitragKz69 = function(array $b): float {
|
|||
};
|
||||
|
||||
// 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 =>
|
||||
$b['buchungstyp'] === 'Ausgabe' ? -abs((float)$b[$field]) : abs((float)$b[$field]);
|
||||
$b['buchungstyp'] === 'Ausgabe' ? -(float)$b[$field] : (float)$b[$field];
|
||||
?>
|
||||
|
||||
<div class="card">
|
||||
|
|
@ -65,7 +66,7 @@ $signedBetrag = fn(array $b, string $field): float =>
|
|||
$sumUst += $ust;
|
||||
$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>
|
||||
<?php if ($b['beleg_id']): ?>
|
||||
|
|
|
|||
104
tests/18_StornoBuchungTest.php
Normal file
104
tests/18_StornoBuchungTest.php
Normal 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');
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Reference in a new issue