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['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'],
|
||||||
]
|
]
|
||||||
|
|
|
||||||
|
|
@ -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]
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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']): ?>
|
||||||
|
|
|
||||||
|
|
@ -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']): ?>
|
||||||
|
|
|
||||||
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