diff --git a/Changes.md b/Changes.md index d236a33..be5ce28 100644 --- a/Changes.md +++ b/Changes.md @@ -2,6 +2,16 @@ --- +## 2026-05-10 – Feature: ig. Erwerb 19 % ReverseCharge über Kindzeilen + +- Migration 047: ustva_kz='91' für Konto 1774; Kindzeilen (1574/1774) für bestehende ig_erwerb_19-Buchungen +- `Booking::create()`: steuerart=ig_erwerb_19 erzeugt zwei Kindzeilen – VSt auf 1574 (Kz 67), USt auf 1774 (Kz 91) +- `UstvaCalculator`: Kz 67/91 jetzt kontobasiert via 1574/1774; Kz 93/94 (ig_erwerb_7) entfernt +- Buchungsformular: Option „ig. Erwerb 7 %" entfernt +- Test 7 auf neue Logik angepasst; Test 30 neu + +--- + ## 2026-05-10 – Feature: USt-Buchungszeilen für Ausgangsrechnungen - Migration 046: Konten 1771 (USt 7 %) und 1776 (USt 19 %) erhalten `ustva_kz='84'`/'83'`; bestehende Einnahme-Buchungen mit USt erhalten nachträglich USt-Kindzeilen diff --git a/docs/berechnungslogik.md b/docs/berechnungslogik.md index 4a2d5da..8eedbac 100644 --- a/docs/berechnungslogik.md +++ b/docs/berechnungslogik.md @@ -55,15 +55,23 @@ Diese Berechnung wird ausschließlich für **monatliche** Voranmeldungen unterst **Quelle 1 – kontobasiert** (`accounts.ustva_kz` des Kategorie-Kontos) SKR03-Erlös- und Steuerkonten tragen ihre Kennzahl direkt am Konto. Buchungen mit gefülltem `bookings.steuerart` werden aus dieser Quelle ausgeschlossen, um Doppelzählung mit Quelle 3 zu vermeiden. Konten mit `ustva_kz='61'` oder `'62'` werden **nicht mehr** kontobasiert ausgewertet (siehe Quelle 2). -**Quelle 2 – Steuerkonten-Buchungszeilen** (Kz 61/62 via 1571/1576, Kz 83/84 via 1771/1776) -Jede Ausgabe-Buchung mit 7/19 % und `steuerart=normal` erzeugt automatisch eine VSt-Kindzeile auf Konto 1571 (7 %) bzw. 1576 (19 %); jede Einnahme-Buchung entsprechend eine USt-Kindzeile auf 1771 (7 %) bzw. 1776 (19 %). Kz 61/62 und Kz 83/84 werden aus `betrag_netto` dieser Kindzeilen aggregiert. Maßgeblich ist der tatsächliche USt-Satz der Buchung, nicht das Erlöskonto. EU-Buchungen (`steuerart` ∈ {ig_lieferung, export, ig_erwerb_19, ig_erwerb_7}) erhalten keine Steuerkonten-Kindzeilen. +**Quelle 2 – Steuerkonten-Buchungszeilen** (Kz 61/62/67/83/84/91 via Steuerkonten) +Normale Buchungen mit 7/19 % erzeugen automatisch Kindzeilen auf den zugehörigen Steuerkonten. Buchungen mit `steuerart=ig_erwerb_19` erzeugen zwei Kindzeilen auf 1574 (Kz 67) und 1774 (Kz 91) – jeweils `netto × 0,19`. Alle anderen EU-Steuerarten erhalten keine Kindzeilen. + +| Buchungstyp | Steuersatz | Kindzeile Konto | Kennzahl | +|---|---|---|---| +| Ausgabe, steuerart=normal | 19 % | 1576 | Kz 61 | +| Ausgabe, steuerart=normal | 7 % | 1571 | Kz 62 | +| Einnahme, steuerart=normal | 19 % | 1776 | Kz 83 | +| Einnahme, steuerart=normal | 7 % | 1771 | Kz 84 | +| Ausgabe, steuerart=ig_erwerb_19 | – | 1574 (netto×0,19) | Kz 67 | +| Ausgabe, steuerart=ig_erwerb_19 | – | 1774 (netto×0,19) | Kz 91 | **Quelle 3 – steuerartbasiert** (`bookings.steuerart`) Auslandssachverhalte unabhängig vom gebuchten Konto: - `ig_lieferung` → Kz 41 (steuerfreie ig. Lieferung, § 4 Nr. 1b UStG) - `export` → Kz 44 (steuerfreie Ausfuhr in Drittland, § 4 Nr. 1a UStG) -- `ig_erwerb_19` → Kz 89 (Bemessungsgrundlage) + Kz 91 (USt = 19 %) -- `ig_erwerb_7` → Kz 93 (Bemessungsgrundlage) + Kz 94 (USt = 7 %) +- `ig_erwerb_19` → Kz 89 (Bemessungsgrundlage) – Kz 91/67 laufen über Kindzeilen (Quelle 2) ### Implementierte Kennzahlen @@ -77,14 +85,12 @@ Auslandssachverhalte unabhängig vom gebuchten Konto: | **41** | Steuerfreie ig. Lieferungen mit USt-IdNr. | § 4 Nr. 1b i.V.m. § 6a UStG | Konto + Steuerart | `ustva_kz='41'` ODER `steuerart='ig_lieferung'` | | **44** | Steuerfreie Ausfuhrlieferungen Drittland | § 4 Nr. 1a i.V.m. § 6 UStG | Konto + Steuerart | `ustva_kz='44'` ODER `steuerart='export'` | | **89** | Ig. Erwerb 19 % – Bemessungsgrundlage | § 1a UStG, § 12 Abs. 1 UStG | Steuerart | `SUM(betrag_netto)` WHERE `steuerart='ig_erwerb_19'` | -| **91** | USt auf ig. Erwerb 19 % | § 1a UStG | berechnet | `Kz 89 × 0,19` | -| **93** | Ig. Erwerb 7 % – Bemessungsgrundlage | § 1a UStG, § 12 Abs. 2 UStG | Steuerart | `SUM(betrag_netto)` WHERE `steuerart='ig_erwerb_7'` | -| **94** | USt auf ig. Erwerb 7 % | § 1a UStG | berechnet | `Kz 93 × 0,07` | +| **91** | USt auf ig. Erwerb 19 % | § 1a UStG | Steuerkonto | `SUM(betrag_netto)` WHERE `ustva_kz='91'` (Konto 1774) | | **61** | Vorsteuerbeträge aus Eingangsrechnungen 19 % | § 15 Abs. 1 Nr. 1 UStG | Steuerkonto | `SUM(betrag_netto)` WHERE `ustva_kz='61'` (Konto 1576) | | **62** | Vorsteuerbeträge aus Eingangsrechnungen 7 % | § 15 Abs. 1 Nr. 1 UStG | Steuerkonto | `SUM(betrag_netto)` WHERE `ustva_kz='62'` (Konto 1571) | -| **67** | Vorsteuer aus innergemeinschaftlichem Erwerb | § 15 Abs. 1 Nr. 3 UStG | abgeleitet | `Kz 91 + Kz 94` | +| **67** | Vorsteuer aus innergemeinschaftlichem Erwerb | § 15 Abs. 1 Nr. 3 UStG | Steuerkonto | `SUM(betrag_netto)` WHERE `ustva_kz='67'` (Konto 1574) | | **66** | Vorsteuerbeträge gesamt | § 15 UStG | abgeleitet | `Kz 61 + Kz 62 + Kz 67` | -| **69** | Verbleibende USt-Vorauszahlung / Erstattung | § 18 Abs. 1 UStG | abgeleitet | `(Kz 83 + Kz 84 + Kz 91 + Kz 94) − Kz 66` | +| **69** | Verbleibende USt-Vorauszahlung / Erstattung | § 18 Abs. 1 UStG | abgeleitet | `(Kz 83 + Kz 84 + Kz 91) − Kz 66` | > **Hinweis zur Bezeichnung Kz 61/62:** Die offizielle ELSTER-Bezeichnung „Kz 61 = Vorsteuer aus innergemeinschaftlichem Erwerb" (§ 15 Abs. 1 Nr. 3 UStG) und „Kz 62 = Einfuhrumsatzsteuer" (§ 15 Abs. 1 Nr. 2 UStG) wird hier abweichend für Inlands-VSt 19 % bzw. 7 % verwendet. Diese Konvention stammt aus den frühen Migrationen und wurde nie auf die offiziellen Kz 66 (Inlandsvorsteuer 19 %) und Kz 66 (Inlandsvorsteuer 7 %) zurückgeführt. Für die ELSTER-Übertragung ist das unkritisch, weil das XML alle Vorsteuerwerte rechnerisch in Kz 66 zusammenfasst – materiell-rechtlich sollte dieser Punkt aber bei der nächsten Refaktorierung geprüft werden (siehe „Offene Punkte"). Migration 017 dokumentiert die Diskrepanz und fixt sie für VSt-Konten teilweise; Migration 022 hat sie für die buchungsbasierte Auswertung bewusst wieder eingeführt. diff --git a/migrations/047_ige_rc_buchungszeilen.sql b/migrations/047_ige_rc_buchungszeilen.sql new file mode 100644 index 0000000..9c4f024 --- /dev/null +++ b/migrations/047_ige_rc_buchungszeilen.sql @@ -0,0 +1,82 @@ +-- Migration 047: ig. Erwerb 19 % ReverseCharge über Kindzeilen (1574/1774) +-- +-- Hintergrund: Bisher wurden Kz 91 (USt ig.E.) und Kz 67 (VSt ig.E.) durch den +-- UstvaCalculator aus betrag_netto × 0,19 der Hauptbuchung berechnet. +-- Ab jetzt erzeugt Booking::create() bei steuerart='ig_erwerb_19' zwei Kindzeilen: +-- - 1574 (Vorsteuer ig.E. 19 %, ustva_kz='67') für Kz 67 +-- - 1774 (USt ig.E. 19 %, ustva_kz='91') für Kz 91 + +-- ── 1. ustva_kz für 1774 auf '91' setzen ───────────────────────────────────── + +UPDATE accounts SET ustva_kz = '91' WHERE kontonummer = '1774'; + +-- ── 2. Kindzeilen für bestehende ig_erwerb_19 Hauptbuchungen anlegen ───────── +-- Idempotenz: WHERE NOT EXISTS verhindert Doppel-Einfügung + +-- 2a. VSt-Zeile auf 1574 (Kz 67) +INSERT INTO bookings ( + buchungs_nr, beleg_id, parent_booking_id, buchungs_datum, buchungstyp, + kategorie_konto_id, zahlungskonto_id, + betrag_netto, ust_satz, steuerart, betrag_ust, betrag_brutto, + buchungstext, is_reversal, fiscal_year_id +) +SELECT + CONCAT('IGE-VST-', LPAD(b.id, 8, '0')), + b.beleg_id, + b.id, + b.buchungs_datum, + b.buchungstyp, + a_vst.id, + b.zahlungskonto_id, + ROUND(b.betrag_netto * 0.19, 2), + 0.00, + 'normal', + 0.00, + ROUND(b.betrag_netto * 0.19, 2), + CONCAT('VSt ig.E. zu ', b.buchungs_nr), + b.is_reversal, + b.fiscal_year_id +FROM bookings b +JOIN accounts a_vst ON a_vst.kontonummer = '1574' +WHERE b.steuerart = 'ig_erwerb_19' + AND b.parent_booking_id IS NULL + AND b.betrag_netto != 0 + AND NOT EXISTS ( + SELECT 1 FROM bookings k + JOIN accounts ak ON k.kategorie_konto_id = ak.id + WHERE k.parent_booking_id = b.id AND ak.kontonummer = '1574' + ); + +-- 2b. USt-Zeile auf 1774 (Kz 91) +INSERT INTO bookings ( + buchungs_nr, beleg_id, parent_booking_id, buchungs_datum, buchungstyp, + kategorie_konto_id, zahlungskonto_id, + betrag_netto, ust_satz, steuerart, betrag_ust, betrag_brutto, + buchungstext, is_reversal, fiscal_year_id +) +SELECT + CONCAT('IGE-UST-', LPAD(b.id, 8, '0')), + b.beleg_id, + b.id, + b.buchungs_datum, + b.buchungstyp, + a_ust.id, + b.zahlungskonto_id, + ROUND(b.betrag_netto * 0.19, 2), + 0.00, + 'normal', + 0.00, + ROUND(b.betrag_netto * 0.19, 2), + CONCAT('USt ig.E. zu ', b.buchungs_nr), + b.is_reversal, + b.fiscal_year_id +FROM bookings b +JOIN accounts a_ust ON a_ust.kontonummer = '1774' +WHERE b.steuerart = 'ig_erwerb_19' + AND b.parent_booking_id IS NULL + AND b.betrag_netto != 0 + AND NOT EXISTS ( + SELECT 1 FROM bookings k + JOIN accounts ak ON k.kategorie_konto_id = ak.id + WHERE k.parent_booking_id = b.id AND ak.kontonummer = '1774' + ); diff --git a/src/Models/Booking.php b/src/Models/Booking.php index 3623654..b4b6ddd 100644 --- a/src/Models/Booking.php +++ b/src/Models/Booking.php @@ -177,6 +177,45 @@ class Booking } } + // ig. Erwerb 19 % ReverseCharge: Kindzeilen auf 1574 (VSt, Kz 67) und 1774 (USt, Kz 91) + if ($data['buchungstyp'] === 'Ausgabe' + && $steuerart === 'ig_erwerb_19' + && abs($netto) > 0.0 + ) { + $igeUst = round($netto * 0.19, 2); + foreach ([ + ['1574', 'VSt ig.E. zu '], + ['1774', 'USt ig.E. zu '], + ] as [$kontonr, $prefix]) { + $igeAcc = Account::findByKontonummer($kontonr); + if ($igeAcc) { + Database::execute( + 'INSERT INTO bookings (buchungs_nr, beleg_id, parent_booking_id, buchungs_datum, buchungstyp, + kategorie_konto_id, zahlungskonto_id, + betrag_netto, ust_satz, steuerart, betrag_ust, betrag_brutto, + buchungstext, fiscal_year_id) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)', + [ + self::generateBuchungsNr(), + (int)$data['beleg_id'], + $mainId, + $data['buchungs_datum'], + 'Ausgabe', + (int)$igeAcc['id'], + (int)$data['zahlungskonto_id'], + $igeUst, + 0.0, + 'normal', + 0.0, + $igeUst, + $prefix . $nr, + $fy['id'], + ] + ); + } + } + } + return $mainId; } diff --git a/src/Services/UstvaCalculator.php b/src/Services/UstvaCalculator.php index c433f19..d1640ae 100644 --- a/src/Services/UstvaCalculator.php +++ b/src/Services/UstvaCalculator.php @@ -21,23 +21,23 @@ use Steuerkram\Database; * ustva_kz = '44' → Kz 44 (betrag_netto) [steuerfreie Ausfuhren via Konto] * ustva_kz = '61' → Kz 61 (betrag_netto) [Vorsteuer 19 % via Konto 1576] * ustva_kz = '62' → Kz 62 (betrag_netto) [Vorsteuer 7 % via Konto 1571] + * ustva_kz = '67' → Kz 67 (betrag_netto) [VSt ig. Erwerb 19 % via Konto 1574] + * ustva_kz = '91' → Kz 91 (betrag_netto) [USt ig. Erwerb 19 % via Konto 1774] * * 2. Steuerart-basiert (bookings.steuerart, unabhängig vom Konto): * steuerart = 'ig_lieferung' → Kz 41 (betrag_netto) * steuerart = 'export' → Kz 44 (betrag_netto) - * steuerart = 'ig_erwerb_19' → Kz 89 (betrag_netto), Kz 91 (netto × 0,19) - * steuerart = 'ig_erwerb_7' → Kz 93 (betrag_netto), Kz 94 (netto × 0,07) + * steuerart = 'ig_erwerb_19' → Kz 89 (betrag_netto, Bemessungsgrundlage) * Buchungen mit diesen steuerart-Werten bleiben aus der kontobasierten Berechnung * ausgeschlossen, um Doppelzählung zu vermeiden. * * Abgeleitete Kennzahlen: - * Kz 67 = Kz 91 + Kz 94 (Vorsteuer aus ig. Erwerb) * Kz 66 = Kz 61 + Kz 62 + Kz 67 (Vorsteuer gesamt) - * Kz 69 = (Kz 83 + Kz 84 + Kz 91 + Kz 94) − Kz 66 (Zahllast) + * Kz 69 = (Kz 83 + Kz 84 + Kz 91) − Kz 66 (Zahllast) */ class UstvaCalculator { - /** Steuerart-Werte die AUSSCHLIESSLICH steuerart-basiert berechnet werden. */ + /** Steuerart-Werte die aus der kontobasierten Berechnung ausgeschlossen werden. */ private const EU_STEUERARTEN = ['ig_lieferung', 'export', 'ig_erwerb_19', 'ig_erwerb_7']; /** @@ -54,6 +54,8 @@ class UstvaCalculator '44' => ['44'], '61' => ['61'], '62' => ['62'], + '67' => ['67'], + '91' => ['91'], ]; /** @@ -63,11 +65,6 @@ class UstvaCalculator '41' => ['ig_lieferung'], '44' => ['export'], '89' => ['ig_erwerb_19'], - '91' => ['ig_erwerb_19'], - '93' => ['ig_erwerb_7'], - '94' => ['ig_erwerb_7'], - '67' => ['ig_erwerb_19', 'ig_erwerb_7'], - '66' => ['ig_erwerb_19', 'ig_erwerb_7'], '69' => ['ig_lieferung', 'export', 'ig_erwerb_19', 'ig_erwerb_7'], ]; @@ -126,15 +123,13 @@ class UstvaCalculator $kz41 = $n('41') + $st('ig_lieferung'); $kz44 = $n('44') + $st('export'); $kz89 = $st('ig_erwerb_19'); - $kz91 = round($kz89 * 0.19, 2); - $kz93 = $st('ig_erwerb_7'); - $kz94 = round($kz93 * 0.07, 2); - // Kz 61/62 kontobasiert: betrag_netto der VSt-Buchungszeilen (1576/1571) + // Kz 61/62/67/91 kontobasiert via Steuerkonten (1576/1571/1574/1774) $kz61 = $n('61'); $kz62 = $n('62'); - $kz67 = $kz91 + $kz94; + $kz67 = $n('67'); + $kz91 = $n('91'); $kz66 = $kz61 + $kz62 + $kz67; - $kz69 = ($kz83 + $kz84 + $kz91 + $kz94) - $kz66; + $kz69 = ($kz83 + $kz84 + $kz91) - $kz66; return [ '81' => ['name' => 'Steuerpfl. Umsätze 19 %', 'betrag' => round($kz81, 2), 'steuer' => null], @@ -146,11 +141,9 @@ class UstvaCalculator '44' => ['name' => 'Steuerfreie Ausfuhren (§ 4 Nr. 1a)', 'betrag' => round($kz44, 2), 'steuer' => null], '89' => ['name' => 'Ig. Erwerb 19 % – Bemessungsgrundlage', 'betrag' => round($kz89, 2), 'steuer' => null], '91' => ['name' => 'USt auf ig. Erwerb 19 %', 'betrag' => null, 'steuer' => round($kz91, 2)], - '93' => ['name' => 'Ig. Erwerb 7 % – Bemessungsgrundlage', 'betrag' => round($kz93, 2), 'steuer' => null], - '94' => ['name' => 'USt auf ig. Erwerb 7 %', 'betrag' => null, 'steuer' => round($kz94, 2)], '61' => ['name' => 'Vorsteuer aus Eingangsrechnungen 19 %', 'betrag' => null, 'steuer' => round($kz61, 2)], '62' => ['name' => 'Vorsteuer aus Eingangsrechnungen 7 %', 'betrag' => null, 'steuer' => round($kz62, 2)], - '67' => ['name' => 'Vorsteuer aus ig. Erwerb', 'betrag' => null, 'steuer' => round($kz67, 2)], + '67' => ['name' => 'Vorsteuer aus ig. Erwerb 19 %', 'betrag' => null, 'steuer' => round($kz67, 2)], '66' => ['name' => 'Vorsteuerbeträge gesamt', 'betrag' => null, 'steuer' => round($kz66, 2)], '69' => ['name' => 'Verbleibende USt-Vorauszahlung/Erstattung', 'betrag' => null, 'steuer' => round($kz69, 2)], ]; diff --git a/src/Views/belege/detail.php b/src/Views/belege/detail.php index b8265ec..489a66e 100644 --- a/src/Views/belege/detail.php +++ b/src/Views/belege/detail.php @@ -383,8 +383,7 @@ $suppliers = $suppliers ?? []; 'normal' => 'Normal (Inland)', 'ig_lieferung' => 'Steuerfreie ig. Lieferung § 4 Nr. 1b (Kz 41)', 'export' => 'Steuerfreie Ausfuhr § 4 Nr. 1a (Kz 44)', - 'ig_erwerb_19' => 'Innergemeinschaftlicher Erwerb 19 % (Kz 89/91)', - 'ig_erwerb_7' => 'Innergemeinschaftlicher Erwerb 7 % (Kz 93/94)', + 'ig_erwerb_19' => 'Innergemeinschaftlicher Erwerb 19 % – ReverseCharge (Kz 89/91/67)', ]; $currentSteuerart = $formData['steuerart'] ?? 'normal'; foreach ($steuerartOptionen as $val => $label): ?> diff --git a/tests/07_UstvaAuslandTest.php b/tests/07_UstvaAuslandTest.php index 92e6ef3..6adcc32 100644 --- a/tests/07_UstvaAuslandTest.php +++ b/tests/07_UstvaAuslandTest.php @@ -6,7 +6,7 @@ declare(strict_types=1); * Test 7: UStVA-Kennzahlen für Auslandsumsätze und ig. Erwerb. * * Prüft, dass steuerart-Buchungen korrekt in die UStVA-Kennzahlen - * 41, 44, 89, 91, 93, 94 und 67 einfließen. + * 41, 44, 89, 91 und 67 einfließen. */ class UstvaAuslandTest extends TestCase { @@ -29,22 +29,24 @@ class UstvaAuslandTest extends TestCase } $this->assert(true, 'Konten vorhanden'); - // Zweites Zahlungskonto für Ausgabe-Buchungen $bankId = \Steuerkram\Database::fetchOne( "SELECT id FROM accounts WHERE kontenart = 'Zahlungskonto' AND aktiv = 1 AND id != ? LIMIT 1", [$zahlungId] ); $bankId = $bankId ? (int)$bankId['id'] : $zahlungId; + $calc = new \Steuerkram\Services\UstvaCalculator(); + $kzPre = $calc->calculate($year, $month); + // ── 1. Steuerfreie ig. Lieferung → Kz 41 ────────────────────────── $belegIgL = \Steuerkram\Models\Beleg::create([ - 'beleg_nr' => 'TEST-IGL-' . uniqid(), - 'filename' => 'test-igl.pdf', - 'storage_path' => '', - 'mime_type' => 'application/pdf', - 'file_hash' => hash('sha256', 'test-igl'), - 'beleg_datum' => $datum, - 'belegtyp' => 'Ausgangsrechnung', + 'beleg_nr' => 'TEST-IGL-' . uniqid(), + 'filename' => 'test-igl.pdf', + 'storage_path' => '', + 'mime_type' => 'application/pdf', + 'file_hash' => hash('sha256', 'test-igl'), + 'beleg_datum' => $datum, + 'belegtyp' => 'Ausgangsrechnung', ]); \Steuerkram\Models\Booking::create([ 'beleg_id' => $belegIgL, @@ -60,13 +62,13 @@ class UstvaAuslandTest extends TestCase // ── 2. Steuerfreie Ausfuhr → Kz 44 ─────────────────────────────── $belegExp = \Steuerkram\Models\Beleg::create([ - 'beleg_nr' => 'TEST-EXP-' . uniqid(), - 'filename' => 'test-exp.pdf', - 'storage_path' => '', - 'mime_type' => 'application/pdf', - 'file_hash' => hash('sha256', 'test-exp'), - 'beleg_datum' => $datum, - 'belegtyp' => 'Ausgangsrechnung', + 'beleg_nr' => 'TEST-EXP-' . uniqid(), + 'filename' => 'test-exp.pdf', + 'storage_path' => '', + 'mime_type' => 'application/pdf', + 'file_hash' => hash('sha256', 'test-exp'), + 'beleg_datum' => $datum, + 'belegtyp' => 'Ausgangsrechnung', ]); \Steuerkram\Models\Booking::create([ 'beleg_id' => $belegExp, @@ -82,13 +84,13 @@ class UstvaAuslandTest extends TestCase // ── 3. Ig. Erwerb 19 % → Kz 89/91/67 ───────────────────────────── $belegIgE19 = \Steuerkram\Models\Beleg::create([ - 'beleg_nr' => 'TEST-IGE19-' . uniqid(), - 'filename' => 'test-ige19.pdf', - 'storage_path' => '', - 'mime_type' => 'application/pdf', - 'file_hash' => hash('sha256', 'test-ige19'), - 'beleg_datum' => $datum, - 'belegtyp' => 'Eingangsrechnung', + 'beleg_nr' => 'TEST-IGE19-' . uniqid(), + 'filename' => 'test-ige19.pdf', + 'storage_path' => '', + 'mime_type' => 'application/pdf', + 'file_hash' => hash('sha256', 'test-ige19'), + 'beleg_datum' => $datum, + 'belegtyp' => 'Eingangsrechnung', ]); \Steuerkram\Models\Booking::create([ 'beleg_id' => $belegIgE19, @@ -102,31 +104,8 @@ class UstvaAuslandTest extends TestCase 'buchungstext' => 'Test ig. Erwerb 19%', ]); - // ── 4. Ig. Erwerb 7 % → Kz 93/94/67 ────────────────────────────── - $belegIgE7 = \Steuerkram\Models\Beleg::create([ - 'beleg_nr' => 'TEST-IGE7-' . uniqid(), - 'filename' => 'test-ige7.pdf', - 'storage_path' => '', - 'mime_type' => 'application/pdf', - 'file_hash' => hash('sha256', 'test-ige7'), - 'beleg_datum' => $datum, - 'belegtyp' => 'Eingangsrechnung', - ]); - \Steuerkram\Models\Booking::create([ - 'beleg_id' => $belegIgE7, - 'buchungs_datum' => $datum, - 'buchungstyp' => 'Ausgabe', - 'steuerart' => 'ig_erwerb_7', - 'kategorie_konto_id' => $ausgabeId, - 'zahlungskonto_id' => $bankId, - 'betrag_netto' => 300.00, - 'ust_satz' => 0.0, - 'buchungstext' => 'Test ig. Erwerb 7%', - ]); - - // ── UStVA berechnen und Kennzahlen prüfen ───────────────────────── - $calc = new \Steuerkram\Services\UstvaCalculator(); - $kz = $calc->calculate($year, $month); + // ── UStVA berechnen und Delta prüfen ────────────────────────────── + $kz = $calc->calculate($year, $month); // Kz 41: ig. Lieferung 1000 € $this->assert( @@ -140,58 +119,38 @@ class UstvaAuslandTest extends TestCase 'Kz 44 (Ausfuhr): Betrag ≥ 2.000 €' ); - // Kz 89: ig. Erwerb 19% Bemessungsgrundlage 500 € + // Kz 89: ig. Erwerb 19% Bemessungsgrundlage +500 € $this->assert( - (float)$kz['89']['betrag'] >= 500.00, - 'Kz 89 (ig. Erwerb 19% Bmgl.): Betrag ≥ 500 €' + round((float)$kz['89']['betrag'] - (float)$kzPre['89']['betrag'], 2) >= 500.00, + 'Kz 89 (ig. Erwerb 19% Bmgl.): Delta ≥ 500 €' ); - // Kz 91: USt auf ig. Erwerb 19% = 500 * 0.19 = 95 € + // Kz 91: USt ig. Erwerb kontobasiert (1774) +95 € $this->assertEquals( - round(500.00 * 0.19, 2), - (float)$kz['91']['steuer'], - 'Kz 91 (USt ig. Erwerb 19%): Steuer = 95,00 €' + 95.00, + round((float)$kz['91']['steuer'] - (float)$kzPre['91']['steuer'], 2), + 'Kz 91 (USt ig. Erwerb 19%): Delta = 95,00 €' ); - // Kz 93: ig. Erwerb 7% Bemessungsgrundlage 300 € - $this->assert( - (float)$kz['93']['betrag'] >= 300.00, - 'Kz 93 (ig. Erwerb 7% Bmgl.): Betrag ≥ 300 €' - ); - - // Kz 94: USt auf ig. Erwerb 7% = 300 * 0.07 = 21 € + // Kz 67: VSt ig. Erwerb kontobasiert (1574) +95 € $this->assertEquals( - round(300.00 * 0.07, 2), - (float)$kz['94']['steuer'], - 'Kz 94 (USt ig. Erwerb 7%): Steuer = 21,00 €' + 95.00, + round((float)$kz['67']['steuer'] - (float)$kzPre['67']['steuer'], 2), + 'Kz 67 (VSt ig. Erwerb): Delta = 95,00 €' ); - // Kz 67: Vorsteuer aus ig. Erwerb = Kz 91 + Kz 94 = 116 € - $erwartetKz67 = round((float)$kz['91']['steuer'] + (float)$kz['94']['steuer'], 2); + // ReverseCharge ist zahllastneutral: Kz 69 unverändert $this->assertEquals( - $erwartetKz67, - (float)$kz['67']['steuer'], - 'Kz 67 (Vorsteuer ig. Erwerb) = Kz 91 + Kz 94' + 0.00, + round((float)$kz['69']['steuer'] - (float)$kzPre['69']['steuer'], 2), + 'Kz 69 (Zahllast) durch ig. Erwerb nicht verändert' ); - // Kz 69 Plausibilitätsprüfung: ig. Erwerb neutralisiert sich durch Kz 67 - // USt auf Umsätze (83+84) + ig. Erwerb (91+94) - Vorsteuer (66) = 69 - $ust = (float)$kz['83']['steuer'] + (float)$kz['84']['steuer'] - + (float)$kz['91']['steuer'] + (float)$kz['94']['steuer']; - $erwartetKz69 = round($ust - (float)$kz['66']['steuer'], 2); - $this->assertEquals( - $erwartetKz69, - (float)$kz['69']['steuer'], - 'Kz 69 (Zahllast) korrekt berechnet' - ); - - // ig. Erwerb darf NICHT in Kz 35 oder Kz 61/62 landen - // (kz35 enthält nur normale 0%-Einnahmen, kz61 nur normale Vorsteuer) - // Wir buchten keine normalen 0%-Einnahmen, also muss Kz 35 = 0 sein + // ig. Erwerb darf nicht in Kz 35 landen $this->assertEquals( 0.0, (float)$kz['35']['betrag'], - 'Kz 35 (steuerfreie Inlandsumsätze) = 0 (kein normaler 0%-Umsatz gebucht)' + 'Kz 35 (steuerfreie Inlandsumsätze) = 0' ); } diff --git a/tests/30_IgErwerbReverseChargeTest.php b/tests/30_IgErwerbReverseChargeTest.php new file mode 100644 index 0000000..d5b69e0 --- /dev/null +++ b/tests/30_IgErwerbReverseChargeTest.php @@ -0,0 +1,126 @@ +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'); + } +}