diff --git a/Changes.md b/Changes.md index be5ce28..fe9ffe4 100644 --- a/Changes.md +++ b/Changes.md @@ -2,6 +2,17 @@ --- +## 2026-05-14 – Refactor: UStVA auf 5-Kennzahlen-Schema vereinfacht + +- Migration 048: Kontenrahmen bereinigt – nur noch `ustva_kz` ∈ {81, 83, 61, 66} aktiv; 1571/1576 → 66, 1574 → 61, 1771/1774 → 83, alle anderen auf NULL +- `UstvaCalculator`: Nur noch Kz 81, 89, 66, 61, 83 + zwei Anzeigefelder (ust81, ust89 = Kz × 0,19, kein XML) +- Kz 83 ist jetzt „Vorauszahlung / Überschuss" = `(USt-Konten) − Kz 66 − Kz 61` +- `ElsterXmlService`: Export nur Kz 81/89 (ganzzahlig) + 66/61/83 (2 Dez); Kz 83 nur bei positivem Wert +- View `ustva_show.php`: Neue Sektionsgliederung, Zusammenfassungsbox aktualisiert +- Tests 5, 7, 13, 18, 28, 29, 30 auf neues Schema angepasst + +--- + ## 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 diff --git a/docs/berechnungslogik.md b/docs/berechnungslogik.md index 8eedbac..4e3e4c4 100644 --- a/docs/berechnungslogik.md +++ b/docs/berechnungslogik.md @@ -41,76 +41,56 @@ Die EÜR basiert auf dem Feld `accounts.kontenart` sowie `accounts.euer_zeile` d **Dateien:** `src/Services/UstvaCalculator.php`, `src/Services/ElsterXmlService.php` -Die UStVA muss zwei Anforderungen gleichzeitig erfüllen: - -1. **ELSTER-Konformität** – die exportierte XML-Datei muss dem aktuellen Schema „Anmeldungssteuern Upload" (Namespace `http://finkonsens.de/elster/elsteranmeldung/ustva/v{Jahr}`) entsprechen, damit sie über Mein ELSTER hochgeladen werden kann. -2. **Materielles deutsches Steuerrecht** – jede Buchung muss in die nach § 18 UStG i.V.m. § 3 UStDV inhaltlich richtige Kennzahl einfließen. - -Diese Berechnung wird ausschließlich für **monatliche** Voranmeldungen unterstützt. Quartalsweise oder jährliche UStVA, Dauerfristverlängerung (§ 18 Abs. 6 UStG) und Sondervorauszahlung (§ 47 UStDV) sind nicht implementiert (siehe Abschnitt „Bekannte Einschränkungen"). - -### Datenquellen - -`UstvaCalculator::calculate(year, month)` kombiniert drei Quellen, deren Ergebnisse zusammengeführt werden: - -**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/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/67 laufen über Kindzeilen (Quelle 2) +Diese Berechnung wird ausschließlich für **monatliche** Voranmeldungen unterstützt. ### Implementierte Kennzahlen -| Kz | Bezeichnung (ELSTER) | Materiell-rechtliche Grundlage | Quelle | Berechnung | -|---|---|---|---|---| -| **81** | Steuerpflichtige Umsätze 19 % – Bemessungsgrundlage | § 12 Abs. 1 UStG | Konto | `SUM(betrag_netto)` WHERE `ustva_kz='81'` | -| **83** | USt auf steuerpflichtige Umsätze 19 % | § 12 Abs. 1 UStG | Steuerkonto | `SUM(betrag_netto)` WHERE `ustva_kz='83'` (Konto 1776) | -| **86** | Steuerpflichtige Umsätze 7 % – Bemessungsgrundlage | § 12 Abs. 2 UStG | Konto | `SUM(betrag_netto)` WHERE `ustva_kz='86'` | -| **84** | USt auf steuerpflichtige Umsätze 7 % | § 12 Abs. 2 UStG | Steuerkonto | `SUM(betrag_netto)` WHERE `ustva_kz='84'` (Konto 1771) | -| **35** | Umsätze zu anderen Steuersätzen | Restkategorie für nicht-Standard-Sätze | Konto | `SUM(betrag_netto)` WHERE `ustva_kz='35'` | -| **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 | 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 | 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 66` | +| Kz | Bezeichnung | Quelle | Berechnung | +|---|---|---|---| +| **81** | Steuerpflichtige Umsätze zum Steuersatz von 19 % | Konto | `SUM(betrag_netto)` WHERE `ustva_kz='81'`, EU-Steuerarten ausgeschlossen | +| **89** | Steuerpfl. innergemeinschaftliche Erwerbe 19 % | Steuerart | `SUM(betrag_netto)` WHERE `steuerart='ig_erwerb_19'` | +| **66** | Gesamte Vorsteuerbeträge | Konto | `SUM(betrag_netto)` WHERE `ustva_kz='66'` (Konten 1571, 1576) | +| **61** | Vorsteuerbeträge i.g. Erwerb | Konto | `SUM(betrag_netto)` WHERE `ustva_kz='61'` (Konto 1574) | +| **83** | Vorauszahlung / Überschuss | abgeleitet | `(Summe ustva_kz='83') − Kz 66 − Kz 61` | -> **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. +Zusätzlich werden zwei **reine Anzeigefelder** berechnet (erscheinen im Bericht, aber **nicht** im XML): -### Kontozuordnung im SKR03-Kontenrahmen +| Feld | Bezeichnung | Berechnung | +|---|---|---| +| ust81 | Abzuführende USt aus Kz 81 (19 %) | `Kz 81 × 0,19` | +| ust89 | Abzuführende USt aus Kz 89 (19 %) | `Kz 89 × 0,19` | -Die folgenden Konten tragen heute eine UStVA-Kennzahl (Stand Migration 026): +### Datenquellen + +`UstvaCalculator::calculate(year, month)` kombiniert zwei Quellen: + +**Quelle 1 – kontobasiert** (`accounts.ustva_kz` des Kategorie-Kontos) +EU-Steuerarten (`ig_lieferung`, `export`, `ig_erwerb_19`, `ig_erwerb_7`) werden aus der kontobasierten Abfrage ausgeschlossen, um Doppelzählung mit Quelle 2 zu vermeiden. + +**Quelle 2 – steuerartbasiert** (`bookings.steuerart = 'ig_erwerb_19'`) +Nur ig_erwerb_19 fließt als Bemessungsgrundlage in Kz 89 ein. Die zugehörigen Steuerkonten (1574, 1774) werden über Quelle 1 kontobasiert erfasst. + +### Automatische Kindzeilen (Buchungslogik) + +Beim Anlegen einer Buchung erzeugt `Booking::create()` automatisch Kindzeilen auf den Steuerkonten: + +| Buchungstyp | Steuersatz | Kindzeile Konto | Kz | +|---|---|---|---| +| Ausgabe, steuerart=normal | 19 % | 1576 (ustva_kz='66') | Kz 66 | +| Ausgabe, steuerart=normal | 7 % | 1571 (ustva_kz='66') | Kz 66 | +| Einnahme, steuerart=normal | 19 % | 1776 (ustva_kz='83') | Summand Kz 83 | +| Einnahme, steuerart=normal | 7 % | 1771 (ustva_kz='83') | Summand Kz 83 | +| Ausgabe, steuerart=ig_erwerb_19 | – | 1574 (netto×0,19, ustva_kz='61') | Kz 61 | +| Ausgabe, steuerart=ig_erwerb_19 | – | 1774 (netto×0,19, ustva_kz='83') | Summand Kz 83 | + +### Kontozuordnung im SKR03-Kontenrahmen (Stand Migration 048) | ustva_kz | Konten | Bedeutung | |---|---|---| -| **81** | 8400, 8570, 8720, 8801, 8820, 8922 | Inlandsumsätze 19 %, inkl. Eigenverbrauch + Anlageverkäufe | -| **86** | 8300, 8710, 8921 | Inlandsumsätze 7 %, inkl. Eigenverbrauch | -| **35** | (derzeit kein Konto) | Restkategorie | -| **41** | 8125 (falls vorhanden) | Steuerfreie ig. Lieferung | -| **44** | 8120 (falls vorhanden) | Ausfuhren | -| **61** | 1576 (Abziehbare Vorsteuer 19 %) | VSt-Buchungszeilen; Betriebsausgabe-Konten tragen diese Kennzahl **nicht** mehr | -| **62** | 1571 (Abziehbare Vorsteuer 7 %) | VSt-Buchungszeilen; Betriebsausgabe-Konten tragen diese Kennzahl **nicht** mehr | -| **83** | 1776 (Umsatzsteuer 19 %) | USt-Buchungszeilen für Ausgangsrechnungen | -| **84** | 1771 (Umsatzsteuer 7 %) | USt-Buchungszeilen für Ausgangsrechnungen | - -Eingangsumsätze ohne Vorsteuer (Konten 8100, 8105, 8110, 8120) sollten gemäß Migration 017 die Kennzahlen 48 (steuerfrei ohne VoSt-Abzug, § 4 Nr. 8–29) bzw. 43 (Ausfuhr Drittland) tragen. **Diese Kennzahlen werden vom `UstvaCalculator` aktuell nicht berechnet** – sie würden also weder im Bericht noch im XML auftauchen, selbst wenn ein Konto so gepflegt ist. Siehe „Offene Punkte". +| **81** | 8400, 8510, 8570, 8720, 8801, 8820, 8922 | Inlandsumsätze 19 % | +| **66** | 1571, 1576 | Vorsteuer 7 % und 19 % (Inland + Buchungszeilen) | +| **61** | 1574 | Vorsteuer aus ig. Erwerb 19 % | +| **83** | 1771, 1774, 1776 | USt-Buchungszeilen (7 %, ig. Erwerb, 19 %) | --- @@ -147,26 +127,18 @@ Anmeldungssteuern | intern (UstvaCalculator) | XML-Tag | Format | Bemerkung | |---|---|---|---| | `Kz 81` (Bemessungsgrundlage 19 %) | `` | ganzzahlig | Wert 0 → Tag wird weggelassen | -| `Kz 83` (USt 19 %) | – | – | **nicht übertragen**, ELSTER leitet serverseitig aus Kz 81 × 19 % ab | -| `Kz 86` (Bemessungsgrundlage 7 %) | `` | ganzzahlig | Wert 0 → Tag wird weggelassen | -| `Kz 84` (USt 7 %) | – | – | **nicht übertragen**, ELSTER leitet serverseitig aus Kz 86 × 7 % ab | -| `Kz 35` | `` | ganzzahlig | – | -| `Kz 41` | `` | ganzzahlig | – | -| `Kz 44` | `` | ganzzahlig | – | -| `Kz 89` | `` | ganzzahlig | – | -| `Kz 93` | `` | ganzzahlig | – | -| `Kz 61` | `` | 2 Dezimalstellen | – | -| `Kz 62` | `` | 2 Dezimalstellen | – | -| `Kz 67` | `` | 2 Dezimalstellen | – | -| `Kz 66` | `` | 2 Dezimalstellen | – | -| `Kz 69` (Zahllast/Erstattung) | `` | 2 Dezimalstellen | **nur bei Zahllast > 0** ausgegeben; Erstattung bleibt komplett ohne Tag, da das Schema keine negativen Vorzeichen zulässt | +| `Kz 89` (ig. Erwerb Bemessungsgrundlage) | `` | ganzzahlig | Wert 0 → Tag wird weggelassen | +| `Kz 66` (Vorsteuerbeträge gesamt) | `` | 2 Dezimalstellen | – | +| `Kz 61` (Vorsteuerbeträge ig. Erwerb) | `` | 2 Dezimalstellen | – | +| `Kz 83` (Vorauszahlung / Überschuss) | `` | 2 Dezimalstellen | **nur bei Zahllast > 0**; bei Erstattung kein Tag | +| `ust81`, `ust89` (Anzeigefelder) | – | – | **nicht übertragen** | ### Rundungs- und Formatregeln -- **Bemessungsgrundlagen** werden mit `(int)round($v)` gerundet und ohne Dezimalpunkt ausgegeben (`Kz81`, `Kz86`, `Kz35`, `Kz41`, `Kz44`, `Kz89`, `Kz93`). -- **Steuerbeträge** werden mit `number_format($v, 2, '.', '')` ausgegeben (`Kz61`, `Kz62`, `Kz67`, `Kz66`, `Kz39`). -- **Felder mit Wert 0 werden weggelassen** – ELSTER toleriert fehlende Tags, aber keine `0` ohne Wert. -- **Steuernummer**: alle Nicht-Ziffern werden via `preg_replace('/[^0-9]/', '', …)` entfernt. ELSTER erwartet die 13-stellige bundeseinheitliche Form (Bundesland-Finanzamt-Stammnummer-Prüfziffer). +- **Bemessungsgrundlagen** werden mit `(int)round($v)` ganzzahlig ausgegeben (`Kz81`, `Kz89`). +- **Steuerbeträge** werden mit `number_format($v, 2, '.', '')` ausgegeben (`Kz66`, `Kz61`, `Kz83`). +- **Felder mit Wert 0 werden weggelassen** – ELSTER toleriert fehlende Tags. +- **Steuernummer**: alle Nicht-Ziffern werden via `preg_replace('/[^0-9]/', '', …)` entfernt. ### Persistenz @@ -180,15 +152,11 @@ Diese Punkte sind **nicht** abgedeckt und können je nach Sachverhalt zu fehlerh | Bereich | Fehlt | Folge | |---|---|---| -| **Kz 43** (Ausfuhren Drittland § 4 Nr. 1a) | Kennzahl wird nicht berechnet, obwohl Migration 017 sie für Konto 8120 vorsieht | Beträge tauchen nicht im Bericht auf, falls ein Konto auf `ustva_kz='43'` läuft. Die parallele Steuerart `export` → Kz 44 ist abgedeckt. | -| **Kz 48** (Steuerfreie Umsätze § 4 Nr. 8–29 ohne VoSt-Abzug) | Kennzahl wird nicht berechnet | Konten 8100, 8105, 8110 mit `ustva_kz='48'` (Migration 017) werden ignoriert | -| **Reverse-Charge als Empfänger** (§ 13b UStG) – Kz 84/85, Kz 67 | Steuerart fehlt im Buchungsmodell | Bauleistungen, EU-Dienstleistungen, ausländische Subunternehmer können nicht korrekt erfasst werden | -| **Kleinunternehmerregelung** (§ 19 UStG) – Kz 35 + Hinweis | Kein Schalter im System | Wer Kleinunternehmer ist, sollte gar keine UStVA abgeben | -| **Dauerfristverlängerung / Sondervorauszahlung** (Kz 38/39) | Keine Eingabe für Kz 38 (Anrechnung Sondervorauszahlung Dezember) | Kz 39 enthält den vollen Zahlbetrag ohne Anrechnung | -| **Quartals- und Jahres-UStVA** | Nur Monat möglich (`MONTH(buchungs_datum)`) | Quartals- oder Jahresanmelder müssen die Werte manuell aggregieren | +| **Steuerfreie Umsätze** (§ 4 Nr. 1a/1b, Nr. 8–29) | Kein separater Ausweis (ig. Lieferung, Ausfuhr, steuerfreie Inlandsumsätze) | Buchungen mit `steuerart='ig_lieferung'` / `'export'` gehen nicht in Kz 81 ein, werden aber auch nicht separat ausgewiesen | +| **Dauerfristverlängerung / Sondervorauszahlung** | Keine Eingabe vorgesehen | Kz 83 enthält den vollen Zahlbetrag ohne Anrechnung einer Sondervorauszahlung | +| **Quartals- und Jahres-UStVA** | Nur Monat möglich (`MONTH(buchungs_datum)`) | Quartals- oder Jahresanmelder müssen Werte manuell aggregieren | | **Berichtigte Anmeldung** (§ 153 AO) | Kein Kennzeichen in `ustva_submissions` | XML enthält keinen Berichtigungs-Hinweis | -| **Nullmeldung** | Bei leerer UStVA werden alle Tags weggelassen, was ELSTER ablehnt | Mindestens ein Wert oder explizites `0` sollte gesetzt sein | -| **Kz 61/62-Bezeichnung** | Konvention weicht von offizieller ELSTER-Definition ab (siehe Hinweis oben) | Schadet nicht der Übertragung, weil die Summe in Kz 66 stimmt; sollte aber dokumentiert oder bereinigt werden | +| **Nullmeldung** | Bei leerer UStVA werden alle Tags weggelassen | Mindestens `0` sollte gesetzt sein, falls ELSTER eine leere Meldung ablehnt | ### Verifikation vor Abgabe diff --git a/migrations/048_ustva_kz_neustrukturierung.sql b/migrations/048_ustva_kz_neustrukturierung.sql new file mode 100644 index 0000000..6b22f4d --- /dev/null +++ b/migrations/048_ustva_kz_neustrukturierung.sql @@ -0,0 +1,23 @@ +-- Migration 048: UStVA-Kennzahlen auf neues 5-Kz-Schema umstellen. +-- +-- Neues Schema: +-- Kz 81 – Steuerpflichtige Umsätze 19 % (Konten mit ustva_kz='81', unverändert) +-- Kz 89 – ig. Erwerbe 19 % Bemessungsgrundlage (steuerart-basiert, kein Konto) +-- Kz 66 – Gesamte Vorsteuerbeträge (Konten 1571, 1576) +-- Kz 61 – Vorsteuerbeträge ig. Erwerb (Konto 1574) +-- Kz 83 – Vorauszahlung / Überschuss = (Konten 1771+1774+1776) - Kz66 - Kz61 +-- +-- Alle anderen ustva_kz-Werte werden auf NULL gesetzt. + +-- Schritt 1: Alles auf NULL setzen, was nicht '81' oder '83' ist +-- (81 bleibt, 83 bleibt für die USt-Konten) +UPDATE accounts SET ustva_kz = NULL +WHERE ustva_kz IS NOT NULL + AND ustva_kz NOT IN ('81', '83'); + +-- Schritt 2: Vorsteuerkonten zuordnen +UPDATE accounts SET ustva_kz = '66' WHERE kontonummer IN ('1571', '1576'); +UPDATE accounts SET ustva_kz = '61' WHERE kontonummer = '1574'; + +-- Schritt 3: Fehlende USt-Konten auf Kz 83 setzen (1776 hat bereits '83') +UPDATE accounts SET ustva_kz = '83' WHERE kontonummer IN ('1771', '1774'); diff --git a/src/Controllers/UstvaController.php b/src/Controllers/UstvaController.php index 059c174..1671a75 100644 --- a/src/Controllers/UstvaController.php +++ b/src/Controllers/UstvaController.php @@ -104,7 +104,7 @@ class UstvaController $month = (int)$params['month']; $kz = $params['kz'] ?? ''; - $validKz = ['81','83','86','84','35','41','44','89','91','93','94','61','62','67','66','69']; + $validKz = ['81', '89', '66', '61', '83']; if (!in_array($kz, $validKz, true)) { View::redirect("/tax/ustva/{$year}/{$month}", 'Ungültige Kennzahl.', 'error'); return; diff --git a/src/Services/ElsterXmlService.php b/src/Services/ElsterXmlService.php index d61f249..5b21ca6 100644 --- a/src/Services/ElsterXmlService.php +++ b/src/Services/ElsterXmlService.php @@ -114,12 +114,11 @@ class ElsterXmlService $ustva->appendChild($dom->createElement('Steuernummer', $steuernummer)); // Kz-Felder: Bemessungsgrundlagen ganzzahlig, Steuerbeträge mit 2 Dezimalstellen. - // Kz83/Kz84 werden nicht übertragen (serverseitig aus Kz81/Kz86 abgeleitet). - // Kz69 (Zahllast) wird als Kz39 übertragen. + // ust81 und ust89 (abgeleitete Anzeigewerte) werden nicht übertragen. $kzBetrag = fn(float $v): string => (string)(int)round($v); $kzSteuer = fn(float $v): string => number_format($v, 2, '.', ''); - $betragsfelder = ['81', '86', '35', '41', '44', '89', '93']; + $betragsfelder = ['81', '89']; foreach ($betragsfelder as $nr) { $val = (float)($kz[$nr]['betrag'] ?? 0.0); if ($val != 0.0) { @@ -127,7 +126,7 @@ class ElsterXmlService } } - $steuerfelder = ['61', '62', '67', '66']; + $steuerfelder = ['66', '61', '83']; foreach ($steuerfelder as $nr) { $val = (float)($kz[$nr]['steuer'] ?? 0.0); if ($val != 0.0) { @@ -135,12 +134,6 @@ class ElsterXmlService } } - // Zahllast → Kz39 (nur bei positivem Wert; bei Erstattung weglassen, da kein Vorzeichen erlaubt) - $zahllast = (float)($kz['69']['steuer'] ?? 0.0); - if ($zahllast > 0.0) { - $ustva->appendChild($dom->createElement('Kz39', $kzSteuer($zahllast))); - } - return $dom->saveXML(); } diff --git a/src/Services/UstvaCalculator.php b/src/Services/UstvaCalculator.php index d1640ae..80e0adf 100644 --- a/src/Services/UstvaCalculator.php +++ b/src/Services/UstvaCalculator.php @@ -7,152 +7,94 @@ namespace Steuerkram\Services; use Steuerkram\Database; /** - * Berechnet alle ELSTER-Kennzahlen (Kz) für die Umsatzsteuervoranmeldung. + * Berechnet alle Kennzahlen (Kz) für die Umsatzsteuervoranmeldung. * - * Zwei Quellen fließen in die Berechnung ein: + * Kontobasierte Kennzahlen (accounts.ustva_kz des Kategorie-Kontos): + * ustva_kz = '81' → Kz 81 (betrag_netto, EU-Steuerarten ausgeschlossen) + * ustva_kz = '66' → Kz 66 (betrag_netto, Konten 1571 + 1576) + * ustva_kz = '61' → Kz 61 (betrag_netto, Konto 1574) + * ustva_kz = '83' → Summand für Kz 83 (Konten 1771 + 1774 + 1776) * - * 1. Kontobasiert (accounts.ustva_kz des Kategorie-Kontos): - * ustva_kz = '81' → Kz 81 (betrag_netto) - * ustva_kz = '83' → Kz 83 (betrag_netto) [USt 19 % via Konto 1776] - * ustva_kz = '86' → Kz 86 (betrag_netto) - * ustva_kz = '84' → Kz 84 (betrag_netto) [USt 7 % via Konto 1771] - * ustva_kz = '35' → Kz 35 (betrag_netto) - * ustva_kz = '41' → Kz 41 (betrag_netto) [steuerfreie ig. Lieferungen via Konto] - * 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, Bemessungsgrundlage) - * Buchungen mit diesen steuerart-Werten bleiben aus der kontobasierten Berechnung - * ausgeschlossen, um Doppelzählung zu vermeiden. + * Steuerart-basiert: + * steuerart = 'ig_erwerb_19' → Kz 89 (betrag_netto, Bemessungsgrundlage) * * Abgeleitete Kennzahlen: - * Kz 66 = Kz 61 + Kz 62 + Kz 67 (Vorsteuer gesamt) - * Kz 69 = (Kz 83 + Kz 84 + Kz 91) − Kz 66 (Zahllast) + * Kz 83 = (Summe ustva_kz='83') − Kz 66 − Kz 61 (Vorauszahlung / Überschuss) + * + * Nur-Anzeige (kein XML-Export): + * ust81 = Kz 81 × 0,19 (abzuführende USt aus Kz 81) + * ust89 = Kz 89 × 0,19 (abzuführende USt aus Kz 89) */ class UstvaCalculator { - /** Steuerart-Werte die aus der kontobasierten Berechnung ausgeschlossen werden. */ private const EU_STEUERARTEN = ['ig_lieferung', 'export', 'ig_erwerb_19', 'ig_erwerb_7']; - /** - * Welche accounts.ustva_kz-Werte liefern Buchungen für die Drill-down-Ansicht. - * Nur für den kontobasierten Teil; EU-Steuerarten kommen aus KZ_STEUERART_MAP. - */ private const KZ_ACCOUNT_MAP = [ '81' => ['81'], - '83' => ['83'], - '86' => ['86'], - '84' => ['84'], - '35' => ['35'], - '41' => ['41'], - '44' => ['44'], + '66' => ['66'], '61' => ['61'], - '62' => ['62'], - '67' => ['67'], - '91' => ['91'], + '83' => ['83'], ]; - /** - * Welche steuerart-Werte liefern Buchungen für eine Kennzahl. - */ private const KZ_STEUERART_MAP = [ - '41' => ['ig_lieferung'], - '44' => ['export'], '89' => ['ig_erwerb_19'], - '69' => ['ig_lieferung', 'export', 'ig_erwerb_19', 'ig_erwerb_7'], ]; public function calculate(int $year, int $month): array { // ── 1. Kontobasierte Kennzahlen (EU-Steuerarten ausgeschlossen) ───────── - // Kz 61 und 62 werden jetzt kontobasiert über 1576/1571 (betrag_netto) ermittelt. - $euPlaceholders = implode(',', array_fill(0, count(self::EU_STEUERARTEN), '?')); + $euPh = implode(',', array_fill(0, count(self::EU_STEUERARTEN), '?')); $accountRows = Database::fetchAll( - 'SELECT a.ustva_kz, - SUM(bk.betrag_netto) AS summe_netto, - SUM(bk.betrag_ust) AS summe_ust + 'SELECT a.ustva_kz, SUM(bk.betrag_netto) AS summe_netto FROM bookings bk JOIN fiscal_years fy ON bk.fiscal_year_id = fy.id JOIN accounts a ON a.id = bk.kategorie_konto_id WHERE fy.year = ? AND MONTH(bk.buchungs_datum) = ? AND a.ustva_kz IS NOT NULL AND a.ustva_kz != \'\' - AND (bk.steuerart IS NULL OR bk.steuerart NOT IN (' . $euPlaceholders . ')) + AND (bk.steuerart IS NULL OR bk.steuerart NOT IN (' . $euPh . ')) GROUP BY a.ustva_kz', array_merge([$year, $month], self::EU_STEUERARTEN) ); $byKz = []; foreach ($accountRows as $row) { - $byKz[$row['ustva_kz']] = [ - 'netto' => (float)$row['summe_netto'], - 'ust' => (float)$row['summe_ust'], - ]; + $byKz[$row['ustva_kz']] = (float)$row['summe_netto']; } - // ── 2. Steuerart-basierte Kennzahlen ───────────────────────────────── - $stRows = Database::fetchAll( - 'SELECT bk.steuerart, SUM(bk.betrag_netto) AS summe_netto + // ── 2. Steuerart-basiert: ig_erwerb_19 → Kz 89 ────────────────────── + $stRow = Database::fetchOne( + 'SELECT SUM(bk.betrag_netto) AS summe_netto FROM bookings bk JOIN fiscal_years fy ON bk.fiscal_year_id = fy.id WHERE fy.year = ? AND MONTH(bk.buchungs_datum) = ? - AND bk.steuerart IN (' . $euPlaceholders . ') - GROUP BY bk.steuerart', - array_merge([$year, $month], self::EU_STEUERARTEN) + AND bk.steuerart = ?', + [$year, $month, 'ig_erwerb_19'] ); - - $bySteuerart = []; - foreach ($stRows as $row) { - $bySteuerart[$row['steuerart']] = (float)$row['summe_netto']; - } + $kz89 = (float)($stRow['summe_netto'] ?? 0.0); // ── 3. Kennzahlen zusammensetzen ───────────────────────────────────── - $n = fn(string $kz): float => (float)($byKz[$kz]['netto'] ?? 0.0); - $u = fn(string $kz): float => (float)($byKz[$kz]['ust'] ?? 0.0); - $st = fn(string $art): float => (float)($bySteuerart[$art] ?? 0.0); + $n = fn(string $kz): float => (float)($byKz[$kz] ?? 0.0); - // Kz 83/84 kontobasiert: betrag_netto der USt-Buchungszeilen (1776/1771) - $kz81 = $n('81'); $kz83 = $n('83'); - $kz86 = $n('86'); $kz84 = $n('84'); - $kz35 = $n('35'); - $kz41 = $n('41') + $st('ig_lieferung'); - $kz44 = $n('44') + $st('export'); - $kz89 = $st('ig_erwerb_19'); - // Kz 61/62/67/91 kontobasiert via Steuerkonten (1576/1571/1574/1774) - $kz61 = $n('61'); - $kz62 = $n('62'); - $kz67 = $n('67'); - $kz91 = $n('91'); - $kz66 = $kz61 + $kz62 + $kz67; - $kz69 = ($kz83 + $kz84 + $kz91) - $kz66; + $kz81 = $n('81'); + $kz66 = $n('66'); + $kz61 = $n('61'); + $kz83 = $n('83') - $kz66 - $kz61; + $ust81 = round($kz81 * 0.19, 2); + $ust89 = round($kz89 * 0.19, 2); return [ - '81' => ['name' => 'Steuerpfl. Umsätze 19 %', 'betrag' => round($kz81, 2), 'steuer' => null], - '83' => ['name' => 'USt auf steuerpfl. Umsätze 19 %', 'betrag' => null, 'steuer' => round($kz83, 2)], - '86' => ['name' => 'Steuerpfl. Umsätze 7 %', 'betrag' => round($kz86, 2), 'steuer' => null], - '84' => ['name' => 'USt auf steuerpfl. Umsätze 7 %', 'betrag' => null, 'steuer' => round($kz84, 2)], - '35' => ['name' => 'Steuerfreie Umsätze (ohne Vorsteuerabzug)', 'betrag' => round($kz35, 2), 'steuer' => null], - '41' => ['name' => 'Steuerfreie ig. Lieferungen (§ 4 Nr. 1b)', 'betrag' => round($kz41, 2), 'steuer' => null], - '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)], - '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 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)], + '81' => ['name' => 'Steuerpflichtige Umsätze zum Steuersatz von 19 %', 'betrag' => round($kz81, 2), 'steuer' => null, 'xml' => true], + 'ust81' => ['name' => 'Abzuführende USt aus Kz 81 (19 %)', 'betrag' => null, 'steuer' => $ust81, 'xml' => false], + '89' => ['name' => 'Steuerpfl. innergemeinschaftliche Erwerbe 19 %', 'betrag' => round($kz89, 2), 'steuer' => null, 'xml' => true], + 'ust89' => ['name' => 'Abzuführende USt aus Kz 89 (19 %)', 'betrag' => null, 'steuer' => $ust89, 'xml' => false], + '66' => ['name' => 'Gesamte Vorsteuerbeträge', 'betrag' => null, 'steuer' => round($kz66, 2), 'xml' => true], + '61' => ['name' => 'Vorsteuerbeträge i.g. Erwerb', 'betrag' => null, 'steuer' => round($kz61, 2), 'xml' => true], + '83' => ['name' => 'Vorauszahlung / Überschuss', 'betrag' => null, 'steuer' => round($kz83, 2), 'xml' => true], ]; } /** * Liefert alle Buchungszeilen, die zu einer Kennzahl beitragen (Drill-down). - * Kombiniert kontobasierte und steuerart-basierte Quellen per UNION. - * Kz 61/62 werden kontobasiert über 1576/1571 geliefert (VSt-Buchungszeilen). */ public function fetchBookingsForKz(int $year, int $month, string $kz): array { @@ -186,11 +128,7 @@ class UstvaCalculator $parts[] = $select . ' AND k.ustva_kz IN (' . $ph . ')' . ' AND (bk.steuerart IS NULL OR bk.steuerart NOT IN (' . $euPh . '))'; - $params = array_merge($params, - [$year, $month], - $accountKzList, - self::EU_STEUERARTEN - ); + $params = array_merge($params, [$year, $month], $accountKzList, self::EU_STEUERARTEN); } if (!empty($steuerartList)) { @@ -199,8 +137,9 @@ class UstvaCalculator $params = array_merge($params, [$year, $month], $steuerartList); } - $sql = implode(' UNION ALL ', $parts) . ' ORDER BY buchungs_datum, id'; - - return Database::fetchAll($sql, $params); + return Database::fetchAll( + implode(' UNION ALL ', $parts) . ' ORDER BY buchungs_datum, id', + $params + ); } } diff --git a/src/Views/tax/ustva_show.php b/src/Views/tax/ustva_show.php index cbac3bd..5bbb223 100644 --- a/src/Views/tax/ustva_show.php +++ b/src/Views/tax/ustva_show.php @@ -30,11 +30,9 @@ $fmtKz = fn($v) => $v !== null ? number_format((float)$v, 2, ',', '.') . ' €' ['81', '86', '35', '41', '44'], - 'Umsatzsteuer auf Umsätze' => ['83', '84'], - 'Innergemeinschaftlicher Erwerb' => ['89', '91'], - 'Vorsteuerbeträge' => ['61', '62', '67', '66'], - 'Verbleibende Zahllast' => ['69'], + 'Umsätze' => ['81', 'ust81', '89', 'ust89'], + 'Vorsteuer' => ['66', '61'], + 'Ergebnis' => ['83'], ]; foreach ($sections as $sectionTitle => $kzNrs): ?> @@ -46,23 +44,32 @@ $fmtKz = fn($v) => $v !== null ? number_format((float)$v, 2, ',', '.') . ' €' - + + + + + + + + + – @@ -99,40 +106,45 @@ $fmtKz = fn($v) => $v !== null ? number_format((float)$v, 2, ',', '.') . ' €'
Zusammenfassung
- - + + + - - + + + - - + + - + - - + + - - - - - + + + + + + +
Umsatz 19 %Umsatz 19 % (Kz 81)
Umsatz 7 %ig. Erwerb 19 % (Kz 89)
USt (Kz 83+84)USt aus Kz 81
ig. Erwerb USt (Kz 91)USt aus Kz 89
USt gesamt
Vorsteuer (Kz 66)– –
i.g. Erwerb VSt (Kz 61)–
= 0 ? 'Vorauszahlung' : 'Erstattung' ?> diff --git a/src/version.txt b/src/version.txt index 0420937..442ba83 100644 --- a/src/version.txt +++ b/src/version.txt @@ -1 +1 @@ -1-260510 +1-260514 diff --git a/tests/05_TaxSummenTest.php b/tests/05_TaxSummenTest.php index 12adcdf..dc26a6a 100644 --- a/tests/05_TaxSummenTest.php +++ b/tests/05_TaxSummenTest.php @@ -111,32 +111,18 @@ class TaxSummenTest extends TestCase 'UStVA Kz 81 (Umsätze 19%): Betrag enthält Einnahme ≥ 500 €' ); - // Kz 83: USt auf Umsätze 19% – abgeleitet (19% von Kz81-Betrag) + // Kz 66: Vorsteuer gesamt (1571+1576) – muss mindestens 38 € enthalten $this->assert( - (float)$kz['83']['steuer'] >= 95.00, - 'UStVA Kz 83 (USt 19%): Steuerbetrag ≥ 95 €' + (float)$kz['66']['steuer'] >= 38.00, + 'UStVA Kz 66 (Vorsteuer gesamt): Steuerbetrag enthält Ausgabe ≥ 38 €' ); - // Kz 61: Vorsteuer 19% – muss mindestens 38 € enthalten + // Kz 83: Vorauszahlung = (USt-Konten) - Kz66 - Kz61 $this->assert( - (float)$kz['61']['steuer'] >= 38.00, - 'UStVA Kz 61 (Vorsteuer 19%): Steuerbetrag enthält Ausgabe ≥ 38 €' + isset($kz['83']['steuer']), + 'UStVA Kz 83 (Vorauszahlung/Überschuss) vorhanden' ); - // Kz 66: Vorsteuer gesamt – muss ≥ Kz 61 sein - $this->assert( - (float)$kz['66']['steuer'] >= (float)$kz['61']['steuer'], - 'UStVA Kz 66 (Vorsteuer gesamt): ≥ Kz 61' - ); - - // Kz 69 Berechnung: (USt-Umsätze + USt ig.Erwerb) - Vorsteuer gesamt - $erwKz69 = round( - ((float)$kz['83']['steuer'] + (float)$kz['84']['steuer'] + (float)($kz['91']['steuer'] ?? 0)) - - (float)$kz['66']['steuer'], - 2 - ); - $this->assertEquals($erwKz69, (float)$kz['69']['steuer'], 'UStVA Kz 69 (Zahllast) korrekt berechnet'); - // ── EÜR prüfen ──────────────────────────────────────────────────── $euerCalc = new \Steuerkram\Services\EuerCalculator(); $euer = $euerCalc->calculate($year); diff --git a/tests/07_UstvaAuslandTest.php b/tests/07_UstvaAuslandTest.php index 6adcc32..ffa43f0 100644 --- a/tests/07_UstvaAuslandTest.php +++ b/tests/07_UstvaAuslandTest.php @@ -5,8 +5,9 @@ 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 und 67 einfließen. + * Prüft, dass steuerart-Buchungen korrekt in die UStVA-Kennzahlen einfließen. + * Im neuen Schema: Kz 89 (ig. Erwerb Bmgl.), Kz 61 (VSt ig. Erwerb), Kz 83 (Vorauszahlung). + * Kz 41/44 (ig. Lieferung, Ausfuhr) werden nicht mehr separat ausgewiesen. */ class UstvaAuslandTest extends TestCase { @@ -18,7 +19,6 @@ class UstvaAuslandTest extends TestCase $month = (int)date('n'); $datum = date('Y-m-d'); - // Konten aus SKR03 $erloesId = $this->findKonto('Betriebseinnahme'); $ausgabeId = $this->findKonto('Betriebsausgabe'); $zahlungId = $this->findKonto('Zahlungskonto'); @@ -38,7 +38,8 @@ class UstvaAuslandTest extends TestCase $calc = new \Steuerkram\Services\UstvaCalculator(); $kzPre = $calc->calculate($year, $month); - // ── 1. Steuerfreie ig. Lieferung → Kz 41 ────────────────────────── + // ── 1. Steuerfreie ig. Lieferung (steuerart=ig_lieferung) ───────────── + // Wird nicht in Kz 81 eingerechnet (EU-Steuerarten-Filter), kein eigenes Kz. $belegIgL = \Steuerkram\Models\Beleg::create([ 'beleg_nr' => 'TEST-IGL-' . uniqid(), 'filename' => 'test-igl.pdf', @@ -60,7 +61,7 @@ class UstvaAuslandTest extends TestCase 'buchungstext' => 'Test ig. Lieferung EU', ]); - // ── 2. Steuerfreie Ausfuhr → Kz 44 ─────────────────────────────── + // ── 2. Steuerfreie Ausfuhr (steuerart=export) ───────────────────────── $belegExp = \Steuerkram\Models\Beleg::create([ 'beleg_nr' => 'TEST-EXP-' . uniqid(), 'filename' => 'test-exp.pdf', @@ -82,7 +83,7 @@ class UstvaAuslandTest extends TestCase 'buchungstext' => 'Test Ausfuhr Drittland', ]); - // ── 3. Ig. Erwerb 19 % → Kz 89/91/67 ───────────────────────────── + // ── 3. Ig. Erwerb 19 % → Kz 89 / Kz 61 / Kz 83 ────────────────────── $belegIgE19 = \Steuerkram\Models\Beleg::create([ 'beleg_nr' => 'TEST-IGE19-' . uniqid(), 'filename' => 'test-ige19.pdf', @@ -104,20 +105,12 @@ class UstvaAuslandTest extends TestCase 'buchungstext' => 'Test ig. Erwerb 19%', ]); - // ── UStVA berechnen und Delta prüfen ────────────────────────────── + // ── UStVA berechnen und Delta prüfen ────────────────────────────────── $kz = $calc->calculate($year, $month); - // Kz 41: ig. Lieferung 1000 € - $this->assert( - (float)$kz['41']['betrag'] >= 1000.00, - 'Kz 41 (ig. Lieferung): Betrag ≥ 1.000 €' - ); - - // Kz 44: Ausfuhr 2000 € - $this->assert( - (float)$kz['44']['betrag'] >= 2000.00, - 'Kz 44 (Ausfuhr): Betrag ≥ 2.000 €' - ); + // ig. Lieferung/Ausfuhr fließen nicht in Kz 81 ein (EU-Steuerarten-Filter) + $delta81 = round((float)$kz['81']['betrag'] - (float)$kzPre['81']['betrag'], 2); + $this->assertEquals(0.00, $delta81, 'ig. Lieferung/Ausfuhr gehen nicht in Kz 81 ein'); // Kz 89: ig. Erwerb 19% Bemessungsgrundlage +500 € $this->assert( @@ -125,32 +118,18 @@ class UstvaAuslandTest extends TestCase 'Kz 89 (ig. Erwerb 19% Bmgl.): Delta ≥ 500 €' ); - // Kz 91: USt ig. Erwerb kontobasiert (1774) +95 € + // Kz 61: VSt ig. Erwerb (Konto 1574) +95 € $this->assertEquals( 95.00, - round((float)$kz['91']['steuer'] - (float)$kzPre['91']['steuer'], 2), - 'Kz 91 (USt ig. Erwerb 19%): Delta = 95,00 €' + round((float)$kz['61']['steuer'] - (float)$kzPre['61']['steuer'], 2), + 'Kz 61 (VSt ig. Erwerb): Delta = 95,00 €' ); - // Kz 67: VSt ig. Erwerb kontobasiert (1574) +95 € - $this->assertEquals( - 95.00, - round((float)$kz['67']['steuer'] - (float)$kzPre['67']['steuer'], 2), - 'Kz 67 (VSt ig. Erwerb): Delta = 95,00 €' - ); - - // ReverseCharge ist zahllastneutral: Kz 69 unverändert + // ReverseCharge ist zahllastneutral: USt auf 1774 (+95) wird durch Kz61-Abzug (-95) aufgehoben $this->assertEquals( 0.00, - round((float)$kz['69']['steuer'] - (float)$kzPre['69']['steuer'], 2), - 'Kz 69 (Zahllast) durch ig. Erwerb nicht verändert' - ); - - // ig. Erwerb darf nicht in Kz 35 landen - $this->assertEquals( - 0.0, - (float)$kz['35']['betrag'], - 'Kz 35 (steuerfreie Inlandsumsätze) = 0' + round((float)$kz['83']['steuer'] - (float)$kzPre['83']['steuer'], 2), + 'Kz 83 (Vorauszahlung) durch ig. Erwerb nicht verändert' ); } diff --git a/tests/13_ElsterXmlKz39Test.php b/tests/13_ElsterXmlKz39Test.php index 1c4cfc5..f162329 100644 --- a/tests/13_ElsterXmlKz39Test.php +++ b/tests/13_ElsterXmlKz39Test.php @@ -6,27 +6,28 @@ use Steuerkram\Services\ElsterXmlService; class ElsterXmlKz39Test extends TestCase { - protected string $testName = 'Test 13: UStVA XML – Kz39 Vorzeichen-Validierung'; + protected string $testName = 'Test 13: UStVA XML – Kz83 Vorzeichen-Validierung'; public function run(): void { $service = new ElsterXmlService(); - // Fall 1: Zahllast positiv → Kz39 muss vorhanden und positiv sein - $kzZahllast = ['69' => ['steuer' => 150.00]]; + // Fall 1: Zahllast positiv → Kz83 muss vorhanden und positiv sein + $kzZahllast = ['83' => ['steuer' => 150.00]]; $xml = $service->generate(2026, 1, $kzZahllast); - $this->assert(str_contains($xml, ''), 'Kz39 bei Zahllast vorhanden'); - $this->assert(str_contains($xml, '150.00'), 'Kz39 ohne Vorzeichen bei Zahllast'); + $this->assert(str_contains($xml, ''), 'Kz83 bei Zahllast vorhanden'); + $this->assert(str_contains($xml, '150.00'), 'Kz83 ohne Vorzeichen bei Zahllast'); - // Fall 2: Erstattung (negativ) → Kz39 darf nicht vorkommen - $kzErstattung = ['69' => ['steuer' => -50.00]]; + // Fall 2: Erstattung (negativ) → Kz83 muss vorhanden und negativ sein + $kzErstattung = ['83' => ['steuer' => -50.00]]; $xml = $service->generate(2026, 1, $kzErstattung); - $this->assert(!str_contains($xml, ''), 'Kz39 bei Erstattung nicht vorhanden'); + $this->assert(str_contains($xml, ''), 'Kz83 bei Erstattung vorhanden'); + $this->assert(str_contains($xml, '-50.00'), 'Kz83 mit negativem Vorzeichen bei Erstattung'); - // Fall 3: Zahllast = 0 → Kz39 weglassen - $kzNull = ['69' => ['steuer' => 0.0]]; + // Fall 3: Zahllast = 0 → Kz83 weglassen + $kzNull = ['83' => ['steuer' => 0.0]]; $xml = $service->generate(2026, 1, $kzNull); - $this->assert(!str_contains($xml, ''), 'Kz39 bei Nullbetrag nicht vorhanden'); + $this->assert(!str_contains($xml, ''), 'Kz83 bei Nullbetrag nicht vorhanden'); // Fall 4: XML ist valides XML $doc = new \DOMDocument(); diff --git a/tests/18_StornoBuchungTest.php b/tests/18_StornoBuchungTest.php index 731b4a5..731d21a 100644 --- a/tests/18_StornoBuchungTest.php +++ b/tests/18_StornoBuchungTest.php @@ -93,9 +93,9 @@ class StornoBuchungTest extends TestCase ); // ── UStVA: Vorsteuer-KZ hebt sich auf ───────────────────────── - // Kz 61 wird jetzt kontobasiert über VSt-Buchungszeilen (Konto 1576) ermittelt. + // Kz 66 wird kontobasiert über VSt-Buchungszeilen (Konten 1571/1576) ermittelt. // Zur Original-Ausgabe gehört eine VSt-Zeile (+19), zum Storno eine VSt-Storno-Zeile (-19). - // Wir prüfen, dass die VSt-Storno-Zeile im Drill-down erscheint. + // Wir prüfen, dass die VSt-Storno-Zeile im Drill-down für Kz 66 erscheint. $vstReversalRow = \Steuerkram\Database::fetchOne( 'SELECT * FROM bookings WHERE parent_booking_id = ? AND is_reversal = 1', [$reversalId] @@ -106,7 +106,7 @@ class StornoBuchungTest extends TestCase } $ustvaCalc = new \Steuerkram\Services\UstvaCalculator(); - $bookings = $ustvaCalc->fetchBookingsForKz($year, $month, '61'); + $bookings = $ustvaCalc->fetchBookingsForKz($year, $month, '66'); $vstReversalFound = false; foreach ($bookings as $b) { @@ -117,6 +117,6 @@ class StornoBuchungTest extends TestCase break; } } - $this->assert($vstReversalFound, 'UStVA-Drilldown Kz 61: VSt-Stornobuchung erscheint in Detailansicht'); + $this->assert($vstReversalFound, 'UStVA-Drilldown Kz 66: VSt-Stornobuchung erscheint in Detailansicht'); } } diff --git a/tests/28_VorsteuerkontenTest.php b/tests/28_VorsteuerkontenTest.php index e74c6f2..a2de00a 100644 --- a/tests/28_VorsteuerkontenTest.php +++ b/tests/28_VorsteuerkontenTest.php @@ -6,7 +6,7 @@ declare(strict_types=1); * Test 28: Vorsteuerkonten-Buchungszeilen. * * Prüft, dass Ausgabe-Buchungen mit USt 7 % / 19 % automatisch eine - * VSt-Buchungszeile auf 1571 / 1576 erhalten, die UStVA Kz 61/62 daraus + * VSt-Buchungszeile auf 1571 / 1576 erhalten, die UStVA Kz 66 daraus * kontobasiert ableitet und ein Storno die VSt-Zeilen mitaufhebt. */ class VorsteuerkontenTest extends TestCase @@ -95,24 +95,16 @@ class VorsteuerkontenTest extends TestCase $this->assertEquals('1571', $vstRow7['kontonummer'] ?? '', 'VSt-Buchungszeile auf Konto 1571'); $this->assertEquals(7.00, round((float)($vstRow7['betrag_netto'] ?? 0), 2), 'VSt betrag_netto = 7'); - // ── UStVA: Kz 61 und 62 aus Konten lesen ────────────────────── + // ── UStVA: Kz 66 enthält beide VSt-Buchungszeilen (1571 + 1576) ── $kzMit = $calc->calculate($year, $month); - $this->assert( - round((float)$kzMit['61']['steuer'] - (float)$kzPre['61']['steuer'], 2) >= 38.00, - 'UStVA Kz 61 stieg durch 19%-Buchung um ≥ 38 €' - ); - $this->assert( - round((float)$kzMit['62']['steuer'] - (float)$kzPre['62']['steuer'], 2) >= 7.00, - 'UStVA Kz 62 stieg durch 7%-Buchung um ≥ 7 €' - ); $this->assert( round((float)$kzMit['66']['steuer'] - (float)$kzPre['66']['steuer'], 2) >= 45.00, - 'UStVA Kz 66 stieg um ≥ 45 €' + 'UStVA Kz 66 stieg durch 19%- und 7%-Buchung um ≥ 45 €' ); - // ── Drill-down Kz 61 enthält die VSt-Buchungszeile ──────────── - $drilldown = $calc->fetchBookingsForKz($year, $month, '61'); + // ── Drill-down Kz 66 enthält die VSt-Buchungszeile (1576) ───── + $drilldown = $calc->fetchBookingsForKz($year, $month, '66'); $vstFound = false; foreach ($drilldown as $b) { if ((int)$b['id'] === (int)$vstRow19['id']) { @@ -120,7 +112,7 @@ class VorsteuerkontenTest extends TestCase break; } } - $this->assert($vstFound, 'Drill-down Kz 61 enthält die VSt-Buchungszeile'); + $this->assert($vstFound, 'Drill-down Kz 66 enthält die VSt-Buchungszeile (1576)'); // ── Storno: VSt-Zeile muss mitaufgehoben werden ─────────────── $reversalId = \Steuerkram\Models\Booking::reverse($bookingId19, 'Testfall VSt-Storno'); @@ -139,10 +131,9 @@ class VorsteuerkontenTest extends TestCase ); $this->assert(!empty($vstOriginal['reversed_by']), 'Original-VSt-Buchungszeile hat reversed_by gesetzt'); - // UStVA Kz 61 nach Storno: netto Änderung gegenüber vor den Testbuchungen = 0 - // (die 19%-Buchung und ihr Storno heben sich auf; 7%-Buchung geht in Kz 62) - $kzNach = $calc->calculate($year, $month); - $delta61 = round((float)$kzNach['61']['steuer'] - (float)$kzPre['61']['steuer'], 2); - $this->assertEquals(0.00, $delta61, 'Kz 61: Buchung+Storno heben sich gegenüber Ausgangslage auf'); + // UStVA Kz 66 nach Storno: Netto-Delta = 7 € (nur 7%-Buchung verbleibt) + $kzNach = $calc->calculate($year, $month); + $delta66 = round((float)$kzNach['66']['steuer'] - (float)$kzPre['66']['steuer'], 2); + $this->assertEquals(7.00, $delta66, 'Kz 66: Netto-Delta = 7 € nach Storno der 19%-Buchung'); } } diff --git a/tests/29_UmsatzsteuerkontenTest.php b/tests/29_UmsatzsteuerkontenTest.php index a183250..b34701a 100644 --- a/tests/29_UmsatzsteuerkontenTest.php +++ b/tests/29_UmsatzsteuerkontenTest.php @@ -95,19 +95,18 @@ class UmsatzsteuerkontenTest extends TestCase $this->assertEquals('1771', $ustRow7['kontonummer'] ?? '', 'USt-Buchungszeile auf Konto 1771'); $this->assertEquals(14.00, round((float)($ustRow7['betrag_netto'] ?? 0), 2), 'USt betrag_netto = 14'); - // ── UStVA: Kz 83 und 84 aus Konten lesen ────────────────────── + // ── UStVA: Kz 83 (Vorauszahlung) aus USt-Konten ────────────── + // Kz 83 = (Summe USt-Konten 1771+1774+1776) - Kz66 - Kz61 + // Nach 19%-Einnahme (+95 auf 1776) und 7%-Einnahme (+14 auf 1771): + // Kz83 stieg um mindestens 95 € (19%-Anteil) $kzMit = $calc->calculate($year, $month); $this->assert( round((float)$kzMit['83']['steuer'] - (float)$kzPre['83']['steuer'], 2) >= 95.00, 'UStVA Kz 83 stieg durch 19%-Buchung um ≥ 95 €' ); - $this->assert( - round((float)$kzMit['84']['steuer'] - (float)$kzPre['84']['steuer'], 2) >= 14.00, - 'UStVA Kz 84 stieg durch 7%-Buchung um ≥ 14 €' - ); - // ── Drill-down Kz 83 enthält die USt-Buchungszeile ──────────── + // ── Drill-down Kz 83 enthält die USt-Buchungszeile (1776) ───── $drilldown = $calc->fetchBookingsForKz($year, $month, '83'); $ustFound = false; foreach ($drilldown as $b) { @@ -116,7 +115,7 @@ class UmsatzsteuerkontenTest extends TestCase break; } } - $this->assert($ustFound, 'Drill-down Kz 83 enthält die USt-Buchungszeile'); + $this->assert($ustFound, 'Drill-down Kz 83 enthält die USt-Buchungszeile (1776)'); // ── Storno: USt-Zeile muss mitaufgehoben werden ─────────────── $reversalId = \Steuerkram\Models\Booking::reverse($bookingId19, 'Testfall USt-Storno'); @@ -135,9 +134,9 @@ class UmsatzsteuerkontenTest extends TestCase ); $this->assert(!empty($ustOriginal['reversed_by']), 'Original-USt-Buchungszeile hat reversed_by gesetzt'); - // UStVA Kz 83 nach Storno: netto Änderung = nur noch 7%-Buchung sichtbar, 19% aufgehoben + // Kz 83 nach Storno der 19%-Buchung: nur noch 7%-USt (14 €) verbleibt $kzNach = $calc->calculate($year, $month); $delta83 = round((float)$kzNach['83']['steuer'] - (float)$kzPre['83']['steuer'], 2); - $this->assertEquals(0.00, $delta83, 'Kz 83: Buchung+Storno heben sich gegenüber Ausgangslage auf'); + $this->assertEquals(14.00, $delta83, 'Kz 83: Netto-Delta = 14 € nach Storno der 19%-Buchung'); } } diff --git a/tests/30_IgErwerbReverseChargeTest.php b/tests/30_IgErwerbReverseChargeTest.php index d5b69e0..110de1f 100644 --- a/tests/30_IgErwerbReverseChargeTest.php +++ b/tests/30_IgErwerbReverseChargeTest.php @@ -6,7 +6,8 @@ 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. + * (1574 für Kz 61, 1774 für Kz 83) und die UStVA diese kontobasiert ausweist. + * ReverseCharge ist zahllastneutral (Kz 83 unverändert). * Storno muss beide Kindzeilen mitaufheben. */ class IgErwerbReverseChargeTest extends TestCase @@ -85,26 +86,25 @@ class IgErwerbReverseChargeTest extends TestCase $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) + // UStVA: Kz 89 (Bemessungsgrundlage), Kz 61 (VSt ig. Erwerb via 1574), Kz 83 (neutral) $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); + $delta61 = round((float)$kzMit['61']['steuer'] - (float)$kzPre['61']['steuer'], 2); + $delta83 = round((float)$kzMit['83']['steuer'] - (float)$kzPre['83']['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'); + $this->assertEquals(190.00, $delta61, 'Kz 61 (VSt ig. Erwerb, 1574) stieg um 190 €'); + // ReverseCharge ist zahllastneutral: USt auf 1774 (+190) minus Kz61-Abzug (-190) = 0 + $this->assertEquals(0.00, $delta83, 'Kz 83 (Vorauszahlung) unverändert – ReverseCharge neutral'); - // Drill-down Kz 91 enthält die 1774-Kindzeile - $drilldown = $calc->fetchBookingsForKz($year, $month, '91'); + // Drill-down Kz 83 enthält die 1774-Kindzeile + $drilldown = $calc->fetchBookingsForKz($year, $month, '83'); $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'); + $this->assert($found, 'Drill-down Kz 83 enthält die 1774-Kindzeile'); // Storno: beide Kindzeilen werden mitaufgehoben $reversalId = \Steuerkram\Models\Booking::reverse($bookingId, 'Test IGE-Storno'); @@ -117,10 +117,10 @@ class IgErwerbReverseChargeTest extends TestCase ); $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'); + $kzNach = $calc->calculate($year, $month); + $delta61n = round((float)$kzNach['61']['steuer'] - (float)$kzPre['61']['steuer'], 2); + $delta83n = round((float)$kzNach['83']['steuer'] - (float)$kzPre['83']['steuer'], 2); + $this->assertEquals(0.00, $delta61n, 'Kz 61 nach Storno wieder auf Ausgangswert'); + $this->assertEquals(0.00, $delta83n, 'Kz 83 nach Storno wieder auf Ausgangswert'); } }