From 7609d1e4e39921675c27c231c36159a232fc0e90 Mon Sep 17 00:00:00 2001 From: fds2610 Date: Thu, 26 Mar 2026 20:07:28 +0100 Subject: [PATCH] =?UTF-8?q?Fix=20#6:=20Auslandsums=C3=A4tze=20und=20ig.=20?= =?UTF-8?q?Erwerb=20korrekt=20in=20UStVA=20abbilden?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- migrations/006_steuerart.sql | 12 ++ src/Models/Booking.php | 10 +- src/Services/ElsterXmlService.php | 35 +++++ src/Services/UstvaCalculator.php | 120 ++++++++++++----- src/Views/belege/detail.php | 39 +++++- src/Views/tax/ustva_show.php | 9 +- tests/07_UstvaAuslandTest.php | 210 ++++++++++++++++++++++++++++++ 7 files changed, 393 insertions(+), 42 deletions(-) create mode 100644 migrations/006_steuerart.sql create mode 100644 tests/07_UstvaAuslandTest.php diff --git a/migrations/006_steuerart.sql b/migrations/006_steuerart.sql new file mode 100644 index 0000000..0409c1f --- /dev/null +++ b/migrations/006_steuerart.sql @@ -0,0 +1,12 @@ +-- Migration 006: steuerart für Auslandsbuchungen +-- Unterscheidet Buchungsarten für USt-freie Auslandsumsätze (UStVA-Pflicht) + +ALTER TABLE bookings + ADD COLUMN steuerart ENUM( + 'normal', -- Standard-Inlandsgeschäft + 'ig_lieferung', -- Steuerfreie ig. Lieferung § 4 Nr. 1b (Kz 41) + 'export', -- Ausfuhr § 4 Nr. 1a (Kz 44) + 'ig_erwerb_19', -- Innergemeinschaftlicher Erwerb 19 % (Kz 89/91) + 'ig_erwerb_7' -- Innergemeinschaftlicher Erwerb 7 % (Kz 93/94) + ) NOT NULL DEFAULT 'normal' + AFTER ust_satz; diff --git a/src/Models/Booking.php b/src/Models/Booking.php index f336305..4ab52fd 100644 --- a/src/Models/Booking.php +++ b/src/Models/Booking.php @@ -96,11 +96,16 @@ class Booking $nr = self::generateBuchungsNr(); + $steuerart = $data['steuerart'] ?? 'normal'; + if (!in_array($steuerart, ['normal','ig_lieferung','export','ig_erwerb_19','ig_erwerb_7'], true)) { + $steuerart = 'normal'; + } + Database::execute( 'INSERT INTO bookings (buchungs_nr, beleg_id, buchungs_datum, buchungstyp, kategorie_konto_id, zahlungskonto_id, - betrag_netto, ust_satz, betrag_ust, betrag_brutto, buchungstext, fiscal_year_id) - VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)', + betrag_netto, ust_satz, steuerart, betrag_ust, betrag_brutto, buchungstext, fiscal_year_id) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)', [ $nr, (int)$data['beleg_id'], @@ -110,6 +115,7 @@ class Booking (int)$data['zahlungskonto_id'], $netto, (float)($data['ust_satz'] ?? 0), + $steuerart, $ust, $brutto, $data['buchungstext'], diff --git a/src/Services/ElsterXmlService.php b/src/Services/ElsterXmlService.php index eaf1c26..989ef61 100644 --- a/src/Services/ElsterXmlService.php +++ b/src/Services/ElsterXmlService.php @@ -153,17 +153,52 @@ class ElsterXmlService $kzEl = $dom->createElement('Kz35', (string)round($kz['35']['betrag'])); $ustva->appendChild($kzEl); } + // Kz41 (steuerfreie ig. Lieferungen) + if (!empty($kz['41']['betrag'])) { + $kzEl = $dom->createElement('Kz41', (string)round($kz['41']['betrag'])); + $ustva->appendChild($kzEl); + } + // Kz44 (steuerfreie Ausfuhren) + if (!empty($kz['44']['betrag'])) { + $kzEl = $dom->createElement('Kz44', (string)round($kz['44']['betrag'])); + $ustva->appendChild($kzEl); + } // Kz83 (USt 19%) $kzEl = $dom->createElement('Kz83', (string)(round($kz['83']['steuer'] * 100))); $ustva->appendChild($kzEl); // Kz84 (USt 7%) $kzEl = $dom->createElement('Kz84', (string)(round($kz['84']['steuer'] * 100))); $ustva->appendChild($kzEl); + // Kz89 (ig. Erwerb 19% Bemessungsgrundlage) + if (!empty($kz['89']['betrag'])) { + $kzEl = $dom->createElement('Kz89', (string)round($kz['89']['betrag'])); + $ustva->appendChild($kzEl); + } + // Kz91 (USt auf ig. Erwerb 19%) + if (!empty($kz['91']['steuer'])) { + $kzEl = $dom->createElement('Kz91', (string)(round($kz['91']['steuer'] * 100))); + $ustva->appendChild($kzEl); + } + // Kz93 (ig. Erwerb 7% Bemessungsgrundlage) + if (!empty($kz['93']['betrag'])) { + $kzEl = $dom->createElement('Kz93', (string)round($kz['93']['betrag'])); + $ustva->appendChild($kzEl); + } + // Kz94 (USt auf ig. Erwerb 7%) + if (!empty($kz['94']['steuer'])) { + $kzEl = $dom->createElement('Kz94', (string)(round($kz['94']['steuer'] * 100))); + $ustva->appendChild($kzEl); + } // Kz66 (Vorsteuer gesamt) if (!empty($kz['66']['steuer'])) { $kzEl = $dom->createElement('Kz66', (string)(round($kz['66']['steuer'] * 100))); $ustva->appendChild($kzEl); } + // Kz67 (Vorsteuer aus ig. Erwerb) + if (!empty($kz['67']['steuer'])) { + $kzEl = $dom->createElement('Kz67', (string)(round($kz['67']['steuer'] * 100))); + $ustva->appendChild($kzEl); + } // Kz69 (Vorauszahlung / Überschuss) $kzEl = $dom->createElement('Kz69', (string)(round($kz['69']['steuer'] * 100))); $ustva->appendChild($kzEl); diff --git a/src/Services/UstvaCalculator.php b/src/Services/UstvaCalculator.php index 478be4b..2149451 100644 --- a/src/Services/UstvaCalculator.php +++ b/src/Services/UstvaCalculator.php @@ -9,70 +9,120 @@ use Steuerkram\Database; /** * Berechnet alle ELSTER-Kennzahlen (Kz) für die Umsatzsteuervoranmeldung. * - * Maßgeblich sind buchungstyp, ust_satz und betrag_ust der Buchung selbst. + * Maßgeblich sind buchungstyp, steuerart, ust_satz und betrag_ust der Buchung. * * Buchungslogik: - * Umsatzsteuer (Kz 81/86): buchungstyp = 'Einnahme' mit ust_satz > 0 - * Vorsteuer (Kz 61/62): buchungstyp = 'Ausgabe' mit ust_satz > 0 - * Steuerfreie Umsätze (Kz 35): buchungstyp = 'Einnahme' mit ust_satz = 0 + * Umsatzsteuer (Kz 81/86): buchungstyp = 'Einnahme', steuerart = 'normal', ust_satz > 0 + * Steuerfreie Umsätze (Kz 35): buchungstyp = 'Einnahme', steuerart = 'normal', ust_satz = 0 + * Steuerfreie ig. Lieferungen (Kz 41): buchungstyp = 'Einnahme', steuerart = 'ig_lieferung' + * Ausfuhren (Kz 44): buchungstyp = 'Einnahme', steuerart = 'export' + * Vorsteuer (Kz 61/62): buchungstyp = 'Ausgabe', steuerart = 'normal', ust_satz > 0 + * Ig. Erwerb 19 % (Kz 89/91): steuerart = 'ig_erwerb_19' + * Ig. Erwerb 7 % (Kz 93/94): steuerart = 'ig_erwerb_7' + * Vorsteuer aus ig. Erwerb (Kz 67): abgeleitet aus Kz 91 + Kz 94 */ class UstvaCalculator { public function calculate(int $year, int $month): array { $kz = [ - '81' => ['name' => 'Steuerpfl. Umsätze 19 %', 'betrag' => 0.0, 'steuer' => 0.0], - '86' => ['name' => 'Steuerpfl. Umsätze 7 %', 'betrag' => 0.0, 'steuer' => 0.0], - '35' => ['name' => 'Steuerfreie Umsätze (ohne Vorsteuerabzug)', 'betrag' => 0.0, 'steuer' => null], - '41' => ['name' => 'Steuerfreie EU-Lieferungen (§ 4 Nr. 1b)', 'betrag' => 0.0, 'steuer' => null], - '83' => ['name' => 'USt auf steuerpfl. Umsätze 19 %', 'betrag' => null, 'steuer' => 0.0], - '84' => ['name' => 'USt auf steuerpfl. Umsätze 7 %', 'betrag' => null, 'steuer' => 0.0], - '61' => ['name' => 'Vorsteuer aus Eingangsrechnungen 19 %', 'betrag' => null, 'steuer' => 0.0], - '62' => ['name' => 'Vorsteuer aus Eingangsrechnungen 7 %', 'betrag' => null, 'steuer' => 0.0], - '66' => ['name' => 'Vorsteuerbeträge gesamt', 'betrag' => null, 'steuer' => 0.0], - '69' => ['name' => 'Verbleibende USt-Vorauszahlung/Erstattung', 'betrag' => null, 'steuer' => 0.0], + // Steuerpflichtige und steuerfreie Umsätze + '81' => ['name' => 'Steuerpfl. Umsätze 19 %', 'betrag' => 0.0, 'steuer' => 0.0], + '86' => ['name' => 'Steuerpfl. Umsätze 7 %', 'betrag' => 0.0, 'steuer' => 0.0], + '35' => ['name' => 'Steuerfreie Umsätze (ohne Vorsteuerabzug)', 'betrag' => 0.0, 'steuer' => null], + '41' => ['name' => 'Steuerfreie ig. Lieferungen (§ 4 Nr. 1b)', 'betrag' => 0.0, 'steuer' => null], + '44' => ['name' => 'Steuerfreie Ausfuhren (§ 4 Nr. 1a)', 'betrag' => 0.0, 'steuer' => null], + // USt auf Umsätze (abgeleitet) + '83' => ['name' => 'USt auf steuerpfl. Umsätze 19 %', 'betrag' => null, 'steuer' => 0.0], + '84' => ['name' => 'USt auf steuerpfl. Umsätze 7 %', 'betrag' => null, 'steuer' => 0.0], + // Innergemeinschaftlicher Erwerb + '89' => ['name' => 'Ig. Erwerb 19 % – Bemessungsgrundlage', 'betrag' => 0.0, 'steuer' => null], + '91' => ['name' => 'USt auf ig. Erwerb 19 %', 'betrag' => null, 'steuer' => 0.0], + '93' => ['name' => 'Ig. Erwerb 7 % – Bemessungsgrundlage', 'betrag' => 0.0, 'steuer' => null], + '94' => ['name' => 'USt auf ig. Erwerb 7 %', 'betrag' => null, 'steuer' => 0.0], + // Vorsteuer + '61' => ['name' => 'Vorsteuer aus Eingangsrechnungen 19 %', 'betrag' => null, 'steuer' => 0.0], + '62' => ['name' => 'Vorsteuer aus Eingangsrechnungen 7 %', 'betrag' => null, 'steuer' => 0.0], + '67' => ['name' => 'Vorsteuer aus ig. Erwerb', 'betrag' => null, 'steuer' => 0.0], + '66' => ['name' => 'Vorsteuerbeträge gesamt', 'betrag' => null, 'steuer' => 0.0], + // Ergebnis + '69' => ['name' => 'Verbleibende USt-Vorauszahlung/Erstattung', 'betrag' => null, 'steuer' => 0.0], ]; $bookings = $this->fetchBookings($year, $month); foreach ($bookings as $b) { + $steuerart = $b['steuerart'] ?? 'normal'; $ustSatz = (float)$b['ust_satz']; $betragUst = abs((float)$b['betrag_ust']); $betragNetto = abs((float)$b['betrag_netto']); $buchungstyp = $b['buchungstyp']; - // ── Umsatzsteuer (Einnahmen) ───────────────────────────── - if ($buchungstyp === 'Einnahme' && $ustSatz > 0) { - if (abs($ustSatz - 19) < 0.01) { - $kz['81']['betrag'] += $betragNetto; - } elseif (abs($ustSatz - 7) < 0.01) { - $kz['86']['betrag'] += $betragNetto; + // ── Einnahmen ─────────────────────────────────────────────── + if ($buchungstyp === 'Einnahme') { + switch ($steuerart) { + case 'ig_lieferung': + $kz['41']['betrag'] += $betragNetto; + break; + case 'export': + $kz['44']['betrag'] += $betragNetto; + break; + default: // normal + if ($ustSatz > 0) { + if (abs($ustSatz - 19) < 0.01) { + $kz['81']['betrag'] += $betragNetto; + } elseif (abs($ustSatz - 7) < 0.01) { + $kz['86']['betrag'] += $betragNetto; + } + } else { + $kz['35']['betrag'] += $betragNetto; + } } } - // Steuerfreie Umsätze: Einnahme mit ust_satz = 0 - if ($buchungstyp === 'Einnahme' && $ustSatz == 0) { - $kz['35']['betrag'] += $betragNetto; - } - - // ── Vorsteuer (Ausgaben) ───────────────────────────────── - if ($buchungstyp === 'Ausgabe' && $ustSatz > 0 && $betragUst > 0) { - if (abs($ustSatz - 19) < 0.01) { - $kz['61']['steuer'] += $betragUst; - } elseif (abs($ustSatz - 7) < 0.01) { - $kz['62']['steuer'] += $betragUst; + // ── Ausgaben ──────────────────────────────────────────────── + if ($buchungstyp === 'Ausgabe') { + switch ($steuerart) { + case 'ig_erwerb_19': + $kz['89']['betrag'] += $betragNetto; + break; + case 'ig_erwerb_7': + $kz['93']['betrag'] += $betragNetto; + break; + default: // normal + if ($ustSatz > 0 && $betragUst > 0) { + if (abs($ustSatz - 19) < 0.01) { + $kz['61']['steuer'] += $betragUst; + } elseif (abs($ustSatz - 7) < 0.01) { + $kz['62']['steuer'] += $betragUst; + } + } } - $kz['66']['steuer'] += $betragUst; } } - // Abgeleitete Kennzahlen + // ── Abgeleitete Kennzahlen ─────────────────────────────────────── $kz['83']['steuer'] = round($kz['81']['betrag'] * 0.19, 2); $kz['84']['steuer'] = round($kz['86']['betrag'] * 0.07, 2); - $ustGesamt = $kz['83']['steuer'] + $kz['84']['steuer']; + $kz['91']['steuer'] = round($kz['89']['betrag'] * 0.19, 2); + $kz['94']['steuer'] = round($kz['93']['betrag'] * 0.07, 2); + + // Vorsteuer aus ig. Erwerb = Steuer auf ig. Erwerb (vollständig abziehbar) + $kz['67']['steuer'] = round($kz['91']['steuer'] + $kz['94']['steuer'], 2); + + // Vorsteuer gesamt + $kz['66']['steuer'] = round( + $kz['61']['steuer'] + $kz['62']['steuer'] + $kz['67']['steuer'], + 2 + ); + + // Verbleibende Zahllast + $ustGesamt = $kz['83']['steuer'] + $kz['84']['steuer'] + + $kz['91']['steuer'] + $kz['94']['steuer']; $kz['69']['steuer'] = round($ustGesamt - $kz['66']['steuer'], 2); + // Runden foreach ($kz as &$field) { if ($field['betrag'] !== null) $field['betrag'] = round($field['betrag'], 2); if ($field['steuer'] !== null) $field['steuer'] = round($field['steuer'], 2); @@ -84,7 +134,7 @@ class UstvaCalculator private function fetchBookings(int $year, int $month): array { return Database::fetchAll( - 'SELECT bk.buchungstyp, bk.ust_satz, bk.betrag_netto, bk.betrag_ust + 'SELECT bk.buchungstyp, bk.steuerart, bk.ust_satz, bk.betrag_netto, bk.betrag_ust FROM bookings bk JOIN fiscal_years fy ON bk.fiscal_year_id = fy.id WHERE fy.year = ? AND MONTH(bk.buchungs_datum) = ? diff --git a/src/Views/belege/detail.php b/src/Views/belege/detail.php index d2522a5..c529759 100644 --- a/src/Views/belege/detail.php +++ b/src/Views/belege/detail.php @@ -290,7 +290,7 @@ $testMode = Setting::isTestMode();
- @@ -298,6 +298,28 @@ $testMode = Setting::isTestMode();
+ +
+ + +

+ Nur relevant bei USt-Satz 0 % für Auslandsumsätze und EU-Erwerbe. +

+
+