Test: Stornorechnung End-to-End-Verifikation für UStVA und EÜR
This commit is contained in:
parent
ccaee6190b
commit
99cf3dd25b
1 changed files with 133 additions and 28 deletions
|
|
@ -4,13 +4,31 @@ declare(strict_types=1);
|
|||
|
||||
/**
|
||||
* Test 27: Stornorechnung erstellen via Invoice::createCancellation().
|
||||
*
|
||||
* Prüft End-to-End:
|
||||
* - DB-Struktur (storno_of_id, negierte Beträge, Status)
|
||||
* - Stornobuchungen werden angelegt
|
||||
* - UStVA: Kz 81/81-USt heben sich nach Storno auf
|
||||
* - EÜR: Erlös-Konto-Saldo nach Storno = vorheriger Stand
|
||||
*/
|
||||
class StornorechnungTest extends TestCase
|
||||
{
|
||||
protected string $testName = 'Test 27: Stornorechnung erstellen';
|
||||
protected string $testName = 'Test 27: Stornorechnung – DB, Buchungen, UStVA, EÜR';
|
||||
|
||||
public function run(): void
|
||||
{
|
||||
$year = (int)date('Y');
|
||||
$month = (int)date('n');
|
||||
$datum = date('Y-m-d');
|
||||
|
||||
$zahlungsId = $this->findAccountId('1200'); // Bank
|
||||
$kategorieId = $this->findAccountId('8400'); // Erlöse 19% (ustva_kz=81)
|
||||
|
||||
if (!$zahlungsId || !$kategorieId) {
|
||||
$this->assert(false, 'Konten 1200/8400 nicht gefunden – SKR03-Seed vorhanden?');
|
||||
return;
|
||||
}
|
||||
|
||||
// Testkunden anlegen
|
||||
$customerId = \Steuerkram\Models\Customer::create([
|
||||
'nachname' => 'StornoTester',
|
||||
|
|
@ -23,11 +41,11 @@ class StornorechnungTest extends TestCase
|
|||
'payment_days' => 14,
|
||||
]);
|
||||
|
||||
// Originalrechnung erstellen und finalisieren
|
||||
// ── 1. Originalrechnung erstellen + finalisieren ──────────────
|
||||
$originalId = \Steuerkram\Models\Invoice::create([
|
||||
'customer_id' => $customerId,
|
||||
'betreff' => 'Testrechnung für Storno',
|
||||
'rechnungs_datum' => date('Y-m-d'),
|
||||
'rechnungs_datum' => $datum,
|
||||
'lines' => [
|
||||
[
|
||||
'bezeichnung' => 'Beratung',
|
||||
|
|
@ -48,53 +66,140 @@ class StornorechnungTest extends TestCase
|
|||
|
||||
\Steuerkram\Models\Invoice::finalize($originalId);
|
||||
$original = \Steuerkram\Models\Invoice::find($originalId);
|
||||
|
||||
$this->assertNotEmpty($original['rechnungs_nr'], 'Original hat Rechnungsnummer');
|
||||
$this->assertEquals('Finalisiert', $original['status'], 'Original ist finalisiert');
|
||||
$this->assertEquals(250.00, round((float)$original['betrag_netto'], 2), 'Original Nettobetrag korrekt');
|
||||
|
||||
// Stornorechnung erstellen
|
||||
// ── 2. Beleg + Buchung manuell anlegen (sonst keine Stornowirkung) ──
|
||||
$belegId = \Steuerkram\Models\Beleg::create([
|
||||
'beleg_nr' => \Steuerkram\Models\Beleg::generateBelegNr(),
|
||||
'filename' => $original['rechnungs_nr'] . '.pdf',
|
||||
'storage_path' => '',
|
||||
'mime_type' => 'application/pdf',
|
||||
'file_hash' => hash('sha256', $original['rechnungs_nr'] . uniqid()),
|
||||
'beleg_datum' => $datum,
|
||||
'rechnungs_nr' => $original['rechnungs_nr'],
|
||||
'betrag_brutto' => $original['betrag_brutto'],
|
||||
'belegtyp' => 'Ausgangsrechnung',
|
||||
]);
|
||||
\Steuerkram\Database::execute(
|
||||
'UPDATE invoices SET beleg_id=? WHERE id=?', [$belegId, $originalId]
|
||||
);
|
||||
|
||||
$origBookingId = \Steuerkram\Models\Booking::create([
|
||||
'beleg_id' => $belegId,
|
||||
'buchungs_datum' => $datum,
|
||||
'buchungstyp' => 'Einnahme',
|
||||
'kategorie_konto_id' => $kategorieId,
|
||||
'zahlungskonto_id' => $zahlungsId,
|
||||
'betrag_netto' => 250.00,
|
||||
'ust_satz' => 19.0,
|
||||
'buchungstext' => 'Test Originalrechnung',
|
||||
]);
|
||||
$origBooking = \Steuerkram\Models\Booking::find($origBookingId);
|
||||
$this->assertEquals(250.00, round((float)$origBooking['betrag_netto'], 2), 'Original-Buchung Netto = 250');
|
||||
$this->assertEquals(47.50, round((float)$origBooking['betrag_ust'], 2), 'Original-Buchung USt = 47,50');
|
||||
|
||||
// ── 3. UStVA + EÜR Stand VOR Stornorechnung erfassen ──────────
|
||||
$ustvaCalc = new \Steuerkram\Services\UstvaCalculator();
|
||||
$kzPre = $ustvaCalc->calculate($year, $month);
|
||||
$kz81NettoPre = (float)($kzPre['81']['betrag'] ?? 0);
|
||||
$kz83UstPre = (float)($kzPre['83']['steuer'] ?? 0);
|
||||
|
||||
$euerCalc = new \Steuerkram\Services\EuerCalculator();
|
||||
$euerPre = $euerCalc->calculate($year);
|
||||
$kontoNettoPre = 0.0;
|
||||
foreach ($euerPre['einnahmen'] as $row) {
|
||||
if ((int)$row['id'] === (int)$kategorieId) {
|
||||
$kontoNettoPre = (float)$row['netto'];
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Sanity-Check: Originalbuchung ist in den Pre-Werten enthalten
|
||||
$this->assert($kz81NettoPre >= 250.0, 'UStVA Kz 81 Pre enthält Original-Netto (≥ 250)');
|
||||
$this->assert($kz83UstPre >= 47.50, 'UStVA Kz 83 Pre enthält Original-USt (≥ 47,50)');
|
||||
$this->assert($kontoNettoPre >= 250.0, 'EÜR Pre: Erlös-Konto enthält Originalbuchung (≥ 250)');
|
||||
|
||||
// ── 4. Stornorechnung erstellen ───────────────────────────────
|
||||
$stornoId = \Steuerkram\Models\Invoice::createCancellation($originalId);
|
||||
$this->assertGreaterThan(0, $stornoId, 'createCancellation gibt gültige ID zurück');
|
||||
|
||||
// Stornorechnung prüfen
|
||||
$storno = \Steuerkram\Models\Invoice::find($stornoId);
|
||||
$this->assertNotEmpty($storno, 'Stornorechnung gefunden');
|
||||
$this->assertEquals($originalId, (int)$storno['storno_of_id'], 'storno_of_id zeigt auf Original');
|
||||
$this->assertEquals('Finalisiert', $storno['status'], 'Stornorechnung ist sofort finalisiert');
|
||||
$this->assertNotEmpty($storno['rechnungs_nr'], 'Stornorechnung hat Rechnungsnummer');
|
||||
$this->assert($original['rechnungs_nr'] !== $storno['rechnungs_nr'], 'Rechnungsnummern sind verschieden');
|
||||
|
||||
// Beträge sind negiert
|
||||
$this->assertEquals(-250.00, round((float)$storno['betrag_netto'], 2), 'Nettobetrag negiert');
|
||||
$this->assertEquals(-47.50, round((float)$storno['betrag_ust'], 2), 'USt-Betrag negiert');
|
||||
$this->assertEquals(-297.50, round((float)$storno['betrag_brutto'], 2), 'Bruttobetrag negiert');
|
||||
$this->assertEquals('Finalisiert', $storno['status'], 'Stornorechnung ist Finalisiert');
|
||||
$this->assert($original['rechnungs_nr'] !== $storno['rechnungs_nr'], 'Rechnungsnummern verschieden');
|
||||
$this->assertEquals(-250.00, round((float)$storno['betrag_netto'], 2), 'Storno Netto negiert');
|
||||
$this->assertEquals(-47.50, round((float)$storno['betrag_ust'], 2), 'Storno USt negiert');
|
||||
$this->assertEquals(-297.50, round((float)$storno['betrag_brutto'], 2), 'Storno Brutto negiert');
|
||||
|
||||
// Originalrechnung ist storniert
|
||||
$originalNach = \Steuerkram\Models\Invoice::find($originalId);
|
||||
$this->assertEquals('Storniert', $originalNach['status'], 'Originalrechnung hat Status Storniert');
|
||||
|
||||
// Positionen der Stornorechnung sind negiert
|
||||
// Positionen negiert
|
||||
$stornoLines = \Steuerkram\Models\Invoice::lines($stornoId);
|
||||
$this->assertEquals(2, count($stornoLines), 'Stornorechnung hat 2 Positionen');
|
||||
foreach ($stornoLines as $line) {
|
||||
if (($line['type'] ?? 'position') === 'position') {
|
||||
$this->assert((float)$line['menge'] > 0, 'Menge bleibt positiv');
|
||||
$this->assert((float)$line['einzelpreis'] < 0, 'Einzelpreis ist negativ');
|
||||
$this->assert((float)$line['betrag_netto'] < 0, 'Position Netto < 0');
|
||||
$this->assert((float)$line['betrag_ust'] < 0, 'Position USt < 0');
|
||||
}
|
||||
}
|
||||
|
||||
// Stornobuchungen müssen angelegt worden sein (Original hatte beleg_id nach finalize)
|
||||
$originalMitBeleg = \Steuerkram\Models\Invoice::find($originalId);
|
||||
if (!empty($originalMitBeleg['beleg_id'])) {
|
||||
$bookings = \Steuerkram\Database::fetchAll(
|
||||
'SELECT id FROM bookings WHERE beleg_id = ? AND is_reversal = 1',
|
||||
[(int)$originalMitBeleg['beleg_id']]
|
||||
// ── 5. Stornobuchung muss am Originalbeleg hängen ─────────────
|
||||
$reversalRows = \Steuerkram\Database::fetchAll(
|
||||
'SELECT * FROM bookings WHERE beleg_id = ? AND is_reversal = 1',
|
||||
[$belegId]
|
||||
);
|
||||
$this->assertGreaterThan(0, count($bookings), 'Stornobuchungen wurden angelegt');
|
||||
}
|
||||
$this->assertEquals(1, count($reversalRows), 'Genau eine Stornobuchung am Originalbeleg');
|
||||
$reversal = $reversalRows[0];
|
||||
$this->assertEquals(-250.00, round((float)$reversal['betrag_netto'], 2), 'Stornobuchung Netto = -250');
|
||||
$this->assertEquals(-47.50, round((float)$reversal['betrag_ust'], 2), 'Stornobuchung USt = -47,50');
|
||||
$this->assertEquals(-297.50, round((float)$reversal['betrag_brutto'], 2), 'Stornobuchung Brutto = -297,50');
|
||||
|
||||
// Erneutes Stornieren der Stornorechnung muss fehlschlagen
|
||||
// Original-Buchung verweist auf Stornobuchung (reversed_by)
|
||||
$origNach = \Steuerkram\Models\Booking::find($origBookingId);
|
||||
$this->assertEquals(
|
||||
(int)$reversal['id'],
|
||||
(int)$origNach['reversed_by'],
|
||||
'Originalbuchung hat reversed_by = Stornobuchungs-ID'
|
||||
);
|
||||
|
||||
// ── 6. UStVA Kz 81 + 83 müssen sich aufheben ──────────────────
|
||||
$kzPost = $ustvaCalc->calculate($year, $month);
|
||||
$kz81NettoPost = (float)($kzPost['81']['betrag'] ?? 0);
|
||||
$kz83UstPost = (float)($kzPost['83']['steuer'] ?? 0);
|
||||
|
||||
$this->assertEquals(
|
||||
round($kz81NettoPre - 250.00, 2),
|
||||
round($kz81NettoPost, 2),
|
||||
'UStVA Kz 81 (Netto): Original + Storno heben sich auf'
|
||||
);
|
||||
$this->assertEquals(
|
||||
round($kz83UstPre - 47.50, 2),
|
||||
round($kz83UstPost, 2),
|
||||
'UStVA Kz 83 (USt): Original + Storno heben sich auf'
|
||||
);
|
||||
|
||||
// ── 7. EÜR Erlös-Konto muss sich aufheben ─────────────────────
|
||||
$euerPost = $euerCalc->calculate($year);
|
||||
$kontoNettoPost = 0.0;
|
||||
foreach ($euerPost['einnahmen'] as $row) {
|
||||
if ((int)$row['id'] === (int)$kategorieId) {
|
||||
$kontoNettoPost = (float)$row['netto'];
|
||||
break;
|
||||
}
|
||||
}
|
||||
$this->assertEquals(
|
||||
round($kontoNettoPre - 250.00, 2),
|
||||
round($kontoNettoPost, 2),
|
||||
'EÜR: Original + Storno heben sich auf Erlös-Konto auf'
|
||||
);
|
||||
|
||||
// ── 8. Erneutes Stornieren der Stornorechnung scheitert ───────
|
||||
$exception = false;
|
||||
try {
|
||||
\Steuerkram\Models\Invoice::createCancellation($stornoId);
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue