126 lines
5.6 KiB
PHP
126 lines
5.6 KiB
PHP
<?php
|
||
|
||
declare(strict_types=1);
|
||
|
||
/**
|
||
* Test 30: Innergemeinschaftlicher Erwerb 19 % – ReverseCharge-Kindzeilen.
|
||
*
|
||
* Prüft, dass ig_erwerb_19-Buchungen automatisch zwei Kindzeilen erhalten
|
||
* (1574 für Kz 67, 1774 für Kz 91) und die UStVA diese kontobasiert ausweist.
|
||
* Storno muss beide Kindzeilen mitaufheben.
|
||
*/
|
||
class IgErwerbReverseChargeTest extends TestCase
|
||
{
|
||
protected string $testName = 'Test 30: ig. Erwerb 19 % ReverseCharge-Kindzeilen';
|
||
|
||
public function run(): void
|
||
{
|
||
$year = (int)date('Y');
|
||
$month = (int)date('n');
|
||
$datum = date('Y-m-d');
|
||
|
||
$zahlungsId = $this->findAccountId('1200');
|
||
$ausgabeId = $this->findAccountId('4120');
|
||
$konto1574 = \Steuerkram\Database::fetchOne(
|
||
"SELECT id FROM accounts WHERE kontonummer = '1574' AND aktiv = 1"
|
||
);
|
||
$konto1774 = \Steuerkram\Database::fetchOne(
|
||
"SELECT id FROM accounts WHERE kontonummer = '1774' AND aktiv = 1"
|
||
);
|
||
|
||
if (!$zahlungsId || !$ausgabeId || !$konto1574 || !$konto1774) {
|
||
$this->assert(false, 'Benötigte Konten nicht gefunden (1200, 4120, 1574, 1774)');
|
||
return;
|
||
}
|
||
$this->assert(true, 'Konten 1200, 4120, 1574, 1774 vorhanden');
|
||
|
||
$calc = new \Steuerkram\Services\UstvaCalculator();
|
||
$kzPre = $calc->calculate($year, $month);
|
||
|
||
$belegId = \Steuerkram\Models\Beleg::create([
|
||
'beleg_nr' => 'IGE-TEST-' . uniqid(),
|
||
'filename' => 'ige-test.pdf',
|
||
'storage_path' => '',
|
||
'mime_type' => 'application/pdf',
|
||
'file_hash' => hash('sha256', 'ige-test-' . uniqid()),
|
||
'beleg_datum' => $datum,
|
||
'belegtyp' => 'Eingangsrechnung',
|
||
]);
|
||
|
||
// Ausgabe-Buchung 0 % USt, steuerart=ig_erwerb_19, Netto=1000 → IGE-USt=190
|
||
$bookingId = \Steuerkram\Models\Booking::create([
|
||
'beleg_id' => $belegId,
|
||
'buchungs_datum' => $datum,
|
||
'buchungstyp' => 'Ausgabe',
|
||
'kategorie_konto_id' => $ausgabeId,
|
||
'zahlungskonto_id' => $zahlungsId,
|
||
'betrag_netto' => 1000.00,
|
||
'ust_satz' => 0.0,
|
||
'steuerart' => 'ig_erwerb_19',
|
||
'buchungstext' => 'Test ig. Erwerb 19%',
|
||
]);
|
||
|
||
// Zwei Kindzeilen müssen angelegt worden sein
|
||
$kinder = \Steuerkram\Database::fetchAll(
|
||
'SELECT bk.*, a.kontonummer FROM bookings bk
|
||
JOIN accounts a ON bk.kategorie_konto_id = a.id
|
||
WHERE bk.parent_booking_id = ?
|
||
ORDER BY a.kontonummer',
|
||
[$bookingId]
|
||
);
|
||
$this->assertEquals(2, count($kinder), 'Genau zwei Kindzeilen angelegt');
|
||
|
||
$kind1574 = null;
|
||
$kind1774 = null;
|
||
foreach ($kinder as $k) {
|
||
if ($k['kontonummer'] === '1574') $kind1574 = $k;
|
||
if ($k['kontonummer'] === '1774') $kind1774 = $k;
|
||
}
|
||
|
||
$this->assert($kind1574 !== null, 'Kindzeile auf Konto 1574 vorhanden');
|
||
$this->assertEquals(190.00, round((float)($kind1574['betrag_netto'] ?? 0), 2), '1574 betrag_netto = 190');
|
||
$this->assertEquals(0.00, round((float)($kind1574['betrag_ust'] ?? 0), 2), '1574 betrag_ust = 0');
|
||
|
||
$this->assert($kind1774 !== null, 'Kindzeile auf Konto 1774 vorhanden');
|
||
$this->assertEquals(190.00, round((float)($kind1774['betrag_netto'] ?? 0), 2), '1774 betrag_netto = 190');
|
||
$this->assertEquals(0.00, round((float)($kind1774['betrag_ust'] ?? 0), 2), '1774 betrag_ust = 0');
|
||
|
||
// UStVA: Kz 89 (Bemessungsgrundlage), Kz 91 (USt), Kz 67 (VSt)
|
||
$kzMit = $calc->calculate($year, $month);
|
||
|
||
$delta89 = round((float)$kzMit['89']['betrag'] - (float)$kzPre['89']['betrag'], 2);
|
||
$delta91 = round((float)$kzMit['91']['steuer'] - (float)$kzPre['91']['steuer'], 2);
|
||
$delta67 = round((float)$kzMit['67']['steuer'] - (float)$kzPre['67']['steuer'], 2);
|
||
$delta69 = round((float)$kzMit['69']['steuer'] - (float)$kzPre['69']['steuer'], 2);
|
||
|
||
$this->assertEquals(1000.00, $delta89, 'Kz 89 stieg um 1000 €');
|
||
$this->assertEquals(190.00, $delta91, 'Kz 91 stieg um 190 €');
|
||
$this->assertEquals(190.00, $delta67, 'Kz 67 stieg um 190 €');
|
||
$this->assertEquals(0.00, $delta69, 'Kz 69 (Zahllast) unverändert – ReverseCharge neutral');
|
||
|
||
// Drill-down Kz 91 enthält die 1774-Kindzeile
|
||
$drilldown = $calc->fetchBookingsForKz($year, $month, '91');
|
||
$found = false;
|
||
foreach ($drilldown as $b) {
|
||
if ((int)$b['id'] === (int)$kind1774['id']) { $found = true; break; }
|
||
}
|
||
$this->assert($found, 'Drill-down Kz 91 enthält die 1774-Kindzeile');
|
||
|
||
// Storno: beide Kindzeilen werden mitaufgehoben
|
||
$reversalId = \Steuerkram\Models\Booking::reverse($bookingId, 'Test IGE-Storno');
|
||
|
||
$stornoKinder = \Steuerkram\Database::fetchAll(
|
||
'SELECT bk.*, a.kontonummer FROM bookings bk
|
||
JOIN accounts a ON bk.kategorie_konto_id = a.id
|
||
WHERE bk.parent_booking_id = ? AND bk.is_reversal = 1',
|
||
[$reversalId]
|
||
);
|
||
$this->assertEquals(2, count($stornoKinder), 'Storno erzeugt zwei Kindzeilen');
|
||
|
||
$kzNach = $calc->calculate($year, $month);
|
||
$delta91n = round((float)$kzNach['91']['steuer'] - (float)$kzPre['91']['steuer'], 2);
|
||
$delta67n = round((float)$kzNach['67']['steuer'] - (float)$kzPre['67']['steuer'], 2);
|
||
$this->assertEquals(0.00, $delta91n, 'Kz 91 nach Storno wieder auf Ausgangswert');
|
||
$this->assertEquals(0.00, $delta67n, 'Kz 67 nach Storno wieder auf Ausgangswert');
|
||
}
|
||
}
|