Refactor: UStVA auf 5-Kennzahlen-Schema vereinfacht (Kz 81/89/66/61/83)

This commit is contained in:
fds2610-bot 2026-05-14 18:02:04 +02:00
parent 6003bab2a8
commit 88aecd86c2
15 changed files with 249 additions and 347 deletions

View file

@ -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 ## 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 - Migration 047: ustva_kz='91' für Konto 1774; Kindzeilen (1574/1774) für bestehende ig_erwerb_19-Buchungen

View file

@ -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` **Dateien:** `src/Services/UstvaCalculator.php`, `src/Services/ElsterXmlService.php`
Die UStVA muss zwei Anforderungen gleichzeitig erfüllen: Diese Berechnung wird ausschließlich für **monatliche** Voranmeldungen unterstützt.
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)
### Implementierte Kennzahlen ### Implementierte Kennzahlen
| Kz | Bezeichnung (ELSTER) | Materiell-rechtliche Grundlage | Quelle | Berechnung | | Kz | Bezeichnung | Quelle | Berechnung |
|---|---|---|---|---| |---|---|---|---|
| **81** | Steuerpflichtige Umsätze 19 % – Bemessungsgrundlage | § 12 Abs. 1 UStG | Konto | `SUM(betrag_netto)` WHERE `ustva_kz='81'` | | **81** | Steuerpflichtige Umsätze zum Steuersatz von 19 % | Konto | `SUM(betrag_netto)` WHERE `ustva_kz='81'`, EU-Steuerarten ausgeschlossen |
| **83** | USt auf steuerpflichtige Umsätze 19 % | § 12 Abs. 1 UStG | Steuerkonto | `SUM(betrag_netto)` WHERE `ustva_kz='83'` (Konto 1776) | | **89** | Steuerpfl. innergemeinschaftliche Erwerbe 19 % | Steuerart | `SUM(betrag_netto)` WHERE `steuerart='ig_erwerb_19'` |
| **86** | Steuerpflichtige Umsätze 7 % – Bemessungsgrundlage | § 12 Abs. 2 UStG | Konto | `SUM(betrag_netto)` WHERE `ustva_kz='86'` | | **66** | Gesamte Vorsteuerbeträge | Konto | `SUM(betrag_netto)` WHERE `ustva_kz='66'` (Konten 1571, 1576) |
| **84** | USt auf steuerpflichtige Umsätze 7 % | § 12 Abs. 2 UStG | Steuerkonto | `SUM(betrag_netto)` WHERE `ustva_kz='84'` (Konto 1771) | | **61** | Vorsteuerbeträge i.g. Erwerb | Konto | `SUM(betrag_netto)` WHERE `ustva_kz='61'` (Konto 1574) |
| **35** | Umsätze zu anderen Steuersätzen | Restkategorie für nicht-Standard-Sätze | Konto | `SUM(betrag_netto)` WHERE `ustva_kz='35'` | | **83** | Vorauszahlung / Überschuss | abgeleitet | `(Summe ustva_kz='83') − Kz 66 − Kz 61` |
| **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` |
> **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 | | ustva_kz | Konten | Bedeutung |
|---|---|---| |---|---|---|
| **81** | 8400, 8570, 8720, 8801, 8820, 8922 | Inlandsumsätze 19 %, inkl. Eigenverbrauch + Anlageverkäufe | | **81** | 8400, 8510, 8570, 8720, 8801, 8820, 8922 | Inlandsumsätze 19 % |
| **86** | 8300, 8710, 8921 | Inlandsumsätze 7 %, inkl. Eigenverbrauch | | **66** | 1571, 1576 | Vorsteuer 7 % und 19 % (Inland + Buchungszeilen) |
| **35** | (derzeit kein Konto) | Restkategorie | | **61** | 1574 | Vorsteuer aus ig. Erwerb 19 % |
| **41** | 8125 (falls vorhanden) | Steuerfreie ig. Lieferung | | **83** | 1771, 1774, 1776 | USt-Buchungszeilen (7 %, ig. Erwerb, 19 %) |
| **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".
--- ---
@ -147,26 +127,18 @@ Anmeldungssteuern
| intern (UstvaCalculator) | XML-Tag | Format | Bemerkung | | intern (UstvaCalculator) | XML-Tag | Format | Bemerkung |
|---|---|---|---| |---|---|---|---|
| `Kz 81` (Bemessungsgrundlage 19 %) | `<Kz81>` | ganzzahlig | Wert 0 → Tag wird weggelassen | | `Kz 81` (Bemessungsgrundlage 19 %) | `<Kz81>` | ganzzahlig | Wert 0 → Tag wird weggelassen |
| `Kz 83` (USt 19 %) | – | – | **nicht übertragen**, ELSTER leitet serverseitig aus Kz 81 × 19 % ab | | `Kz 89` (ig. Erwerb Bemessungsgrundlage) | `<Kz89>` | ganzzahlig | Wert 0 → Tag wird weggelassen |
| `Kz 86` (Bemessungsgrundlage 7 %) | `<Kz86>` | ganzzahlig | Wert 0 → Tag wird weggelassen | | `Kz 66` (Vorsteuerbeträge gesamt) | `<Kz66>` | 2 Dezimalstellen | – |
| `Kz 84` (USt 7 %) | – | – | **nicht übertragen**, ELSTER leitet serverseitig aus Kz 86 × 7 % ab | | `Kz 61` (Vorsteuerbeträge ig. Erwerb) | `<Kz61>` | 2 Dezimalstellen | – |
| `Kz 35` | `<Kz35>` | ganzzahlig | – | | `Kz 83` (Vorauszahlung / Überschuss) | `<Kz83>` | 2 Dezimalstellen | **nur bei Zahllast > 0**; bei Erstattung kein Tag |
| `Kz 41` | `<Kz41>` | ganzzahlig | – | | `ust81`, `ust89` (Anzeigefelder) | – | – | **nicht übertragen** |
| `Kz 44` | `<Kz44>` | ganzzahlig | – |
| `Kz 89` | `<Kz89>` | ganzzahlig | – |
| `Kz 93` | `<Kz93>` | ganzzahlig | – |
| `Kz 61` | `<Kz61>` | 2 Dezimalstellen | – |
| `Kz 62` | `<Kz62>` | 2 Dezimalstellen | – |
| `Kz 67` | `<Kz67>` | 2 Dezimalstellen | – |
| `Kz 66` | `<Kz66>` | 2 Dezimalstellen | – |
| `Kz 69` (Zahllast/Erstattung) | `<Kz39>` | 2 Dezimalstellen | **nur bei Zahllast > 0** ausgegeben; Erstattung bleibt komplett ohne Tag, da das Schema keine negativen Vorzeichen zulässt |
### Rundungs- und Formatregeln ### Rundungs- und Formatregeln
- **Bemessungsgrundlagen** werden mit `(int)round($v)` gerundet und ohne Dezimalpunkt ausgegeben (`Kz81`, `Kz86`, `Kz35`, `Kz41`, `Kz44`, `Kz89`, `Kz93`). - **Bemessungsgrundlagen** werden mit `(int)round($v)` ganzzahlig ausgegeben (`Kz81`, `Kz89`).
- **Steuerbeträge** werden mit `number_format($v, 2, '.', '')` ausgegeben (`Kz61`, `Kz62`, `Kz67`, `Kz66`, `Kz39`). - **Steuerbeträge** werden mit `number_format($v, 2, '.', '')` ausgegeben (`Kz66`, `Kz61`, `Kz83`).
- **Felder mit Wert 0 werden weggelassen** – ELSTER toleriert fehlende Tags, aber keine `<KzXX>0</KzXX>` ohne Wert. - **Felder mit Wert 0 werden weggelassen** – ELSTER toleriert fehlende Tags.
- **Steuernummer**: alle Nicht-Ziffern werden via `preg_replace('/[^0-9]/', '', …)` entfernt. ELSTER erwartet die 13-stellige bundeseinheitliche Form (Bundesland-Finanzamt-Stammnummer-Prüfziffer). - **Steuernummer**: alle Nicht-Ziffern werden via `preg_replace('/[^0-9]/', '', …)` entfernt.
### Persistenz ### Persistenz
@ -180,15 +152,11 @@ Diese Punkte sind **nicht** abgedeckt und können je nach Sachverhalt zu fehlerh
| Bereich | Fehlt | Folge | | 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. | | **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 |
| **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 | | **Dauerfristverlängerung / Sondervorauszahlung** | Keine Eingabe vorgesehen | Kz 83 enthält den vollen Zahlbetrag ohne Anrechnung einer Sondervorauszahlung |
| **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 | | **Quartals- und Jahres-UStVA** | Nur Monat möglich (`MONTH(buchungs_datum)`) | Quartals- oder Jahresanmelder müssen Werte manuell aggregieren |
| **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 |
| **Berichtigte Anmeldung** (§ 153 AO) | Kein Kennzeichen in `ustva_submissions` | XML enthält keinen Berichtigungs-Hinweis | | **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 `<Kz81>0</Kz81>` sollte gesetzt sein | | **Nullmeldung** | Bei leerer UStVA werden alle Tags weggelassen | Mindestens `<Kz81>0</Kz81>` sollte gesetzt sein, falls ELSTER eine leere Meldung ablehnt |
| **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 |
### Verifikation vor Abgabe ### Verifikation vor Abgabe

View file

@ -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');

View file

@ -104,7 +104,7 @@ class UstvaController
$month = (int)$params['month']; $month = (int)$params['month'];
$kz = $params['kz'] ?? ''; $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)) { if (!in_array($kz, $validKz, true)) {
View::redirect("/tax/ustva/{$year}/{$month}", 'Ungültige Kennzahl.', 'error'); View::redirect("/tax/ustva/{$year}/{$month}", 'Ungültige Kennzahl.', 'error');
return; return;

View file

@ -114,12 +114,11 @@ class ElsterXmlService
$ustva->appendChild($dom->createElement('Steuernummer', $steuernummer)); $ustva->appendChild($dom->createElement('Steuernummer', $steuernummer));
// Kz-Felder: Bemessungsgrundlagen ganzzahlig, Steuerbeträge mit 2 Dezimalstellen. // Kz-Felder: Bemessungsgrundlagen ganzzahlig, Steuerbeträge mit 2 Dezimalstellen.
// Kz83/Kz84 werden nicht übertragen (serverseitig aus Kz81/Kz86 abgeleitet). // ust81 und ust89 (abgeleitete Anzeigewerte) werden nicht übertragen.
// Kz69 (Zahllast) wird als Kz39 übertragen.
$kzBetrag = fn(float $v): string => (string)(int)round($v); $kzBetrag = fn(float $v): string => (string)(int)round($v);
$kzSteuer = fn(float $v): string => number_format($v, 2, '.', ''); $kzSteuer = fn(float $v): string => number_format($v, 2, '.', '');
$betragsfelder = ['81', '86', '35', '41', '44', '89', '93']; $betragsfelder = ['81', '89'];
foreach ($betragsfelder as $nr) { foreach ($betragsfelder as $nr) {
$val = (float)($kz[$nr]['betrag'] ?? 0.0); $val = (float)($kz[$nr]['betrag'] ?? 0.0);
if ($val != 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) { foreach ($steuerfelder as $nr) {
$val = (float)($kz[$nr]['steuer'] ?? 0.0); $val = (float)($kz[$nr]['steuer'] ?? 0.0);
if ($val != 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(); return $dom->saveXML();
} }

View file

@ -7,152 +7,94 @@ namespace Steuerkram\Services;
use Steuerkram\Database; 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): * Steuerart-basiert:
* 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) * 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: * Abgeleitete Kennzahlen:
* Kz 66 = Kz 61 + Kz 62 + Kz 67 (Vorsteuer gesamt) * Kz 83 = (Summe ustva_kz='83') − Kz 66 − Kz 61 (Vorauszahlung / Überschuss)
* Kz 69 = (Kz 83 + Kz 84 + Kz 91) − Kz 66 (Zahllast) *
* 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 class UstvaCalculator
{ {
/** Steuerart-Werte die aus der kontobasierten Berechnung ausgeschlossen werden. */
private const EU_STEUERARTEN = ['ig_lieferung', 'export', 'ig_erwerb_19', 'ig_erwerb_7']; 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 = [ private const KZ_ACCOUNT_MAP = [
'81' => ['81'], '81' => ['81'],
'83' => ['83'], '66' => ['66'],
'86' => ['86'],
'84' => ['84'],
'35' => ['35'],
'41' => ['41'],
'44' => ['44'],
'61' => ['61'], '61' => ['61'],
'62' => ['62'], '83' => ['83'],
'67' => ['67'],
'91' => ['91'],
]; ];
/**
* Welche steuerart-Werte liefern Buchungen für eine Kennzahl.
*/
private const KZ_STEUERART_MAP = [ private const KZ_STEUERART_MAP = [
'41' => ['ig_lieferung'],
'44' => ['export'],
'89' => ['ig_erwerb_19'], '89' => ['ig_erwerb_19'],
'69' => ['ig_lieferung', 'export', 'ig_erwerb_19', 'ig_erwerb_7'],
]; ];
public function calculate(int $year, int $month): array public function calculate(int $year, int $month): array
{ {
// ── 1. Kontobasierte Kennzahlen (EU-Steuerarten ausgeschlossen) ───────── // ── 1. Kontobasierte Kennzahlen (EU-Steuerarten ausgeschlossen) ─────────
// Kz 61 und 62 werden jetzt kontobasiert über 1576/1571 (betrag_netto) ermittelt. $euPh = implode(',', array_fill(0, count(self::EU_STEUERARTEN), '?'));
$euPlaceholders = implode(',', array_fill(0, count(self::EU_STEUERARTEN), '?'));
$accountRows = Database::fetchAll( $accountRows = Database::fetchAll(
'SELECT a.ustva_kz, 'SELECT a.ustva_kz, SUM(bk.betrag_netto) AS summe_netto
SUM(bk.betrag_netto) AS summe_netto,
SUM(bk.betrag_ust) AS summe_ust
FROM bookings bk FROM bookings bk
JOIN fiscal_years fy ON bk.fiscal_year_id = fy.id JOIN fiscal_years fy ON bk.fiscal_year_id = fy.id
JOIN accounts a ON a.id = bk.kategorie_konto_id JOIN accounts a ON a.id = bk.kategorie_konto_id
WHERE fy.year = ? AND MONTH(bk.buchungs_datum) = ? WHERE fy.year = ? AND MONTH(bk.buchungs_datum) = ?
AND a.ustva_kz IS NOT NULL AND a.ustva_kz != \'\' 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', GROUP BY a.ustva_kz',
array_merge([$year, $month], self::EU_STEUERARTEN) array_merge([$year, $month], self::EU_STEUERARTEN)
); );
$byKz = []; $byKz = [];
foreach ($accountRows as $row) { foreach ($accountRows as $row) {
$byKz[$row['ustva_kz']] = [ $byKz[$row['ustva_kz']] = (float)$row['summe_netto'];
'netto' => (float)$row['summe_netto'],
'ust' => (float)$row['summe_ust'],
];
} }
// ── 2. Steuerart-basierte Kennzahlen ───────────────────────────────── // ── 2. Steuerart-basiert: ig_erwerb_19 → Kz 89 ──────────────────────
$stRows = Database::fetchAll( $stRow = Database::fetchOne(
'SELECT bk.steuerart, SUM(bk.betrag_netto) AS summe_netto 'SELECT SUM(bk.betrag_netto) AS summe_netto
FROM bookings bk FROM bookings bk
JOIN fiscal_years fy ON bk.fiscal_year_id = fy.id JOIN fiscal_years fy ON bk.fiscal_year_id = fy.id
WHERE fy.year = ? AND MONTH(bk.buchungs_datum) = ? WHERE fy.year = ? AND MONTH(bk.buchungs_datum) = ?
AND bk.steuerart IN (' . $euPlaceholders . ') AND bk.steuerart = ?',
GROUP BY bk.steuerart', [$year, $month, 'ig_erwerb_19']
array_merge([$year, $month], self::EU_STEUERARTEN)
); );
$kz89 = (float)($stRow['summe_netto'] ?? 0.0);
$bySteuerart = [];
foreach ($stRows as $row) {
$bySteuerart[$row['steuerart']] = (float)$row['summe_netto'];
}
// ── 3. Kennzahlen zusammensetzen ───────────────────────────────────── // ── 3. Kennzahlen zusammensetzen ─────────────────────────────────────
$n = fn(string $kz): float => (float)($byKz[$kz]['netto'] ?? 0.0); $n = fn(string $kz): float => (float)($byKz[$kz] ?? 0.0);
$u = fn(string $kz): float => (float)($byKz[$kz]['ust'] ?? 0.0);
$st = fn(string $art): float => (float)($bySteuerart[$art] ?? 0.0);
// Kz 83/84 kontobasiert: betrag_netto der USt-Buchungszeilen (1776/1771) $kz81 = $n('81');
$kz81 = $n('81'); $kz83 = $n('83'); $kz66 = $n('66');
$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'); $kz61 = $n('61');
$kz62 = $n('62'); $kz83 = $n('83') - $kz66 - $kz61;
$kz67 = $n('67'); $ust81 = round($kz81 * 0.19, 2);
$kz91 = $n('91'); $ust89 = round($kz89 * 0.19, 2);
$kz66 = $kz61 + $kz62 + $kz67;
$kz69 = ($kz83 + $kz84 + $kz91) - $kz66;
return [ return [
'81' => ['name' => 'Steuerpfl. Umsätze 19 %', 'betrag' => round($kz81, 2), 'steuer' => null], '81' => ['name' => 'Steuerpflichtige Umsätze zum Steuersatz von 19 %', 'betrag' => round($kz81, 2), 'steuer' => null, 'xml' => true],
'83' => ['name' => 'USt auf steuerpfl. Umsätze 19 %', 'betrag' => null, 'steuer' => round($kz83, 2)], 'ust81' => ['name' => 'Abzuführende USt aus Kz 81 (19 %)', 'betrag' => null, 'steuer' => $ust81, 'xml' => false],
'86' => ['name' => 'Steuerpfl. Umsätze 7 %', 'betrag' => round($kz86, 2), 'steuer' => null], '89' => ['name' => 'Steuerpfl. innergemeinschaftliche Erwerbe 19 %', 'betrag' => round($kz89, 2), 'steuer' => null, 'xml' => true],
'84' => ['name' => 'USt auf steuerpfl. Umsätze 7 %', 'betrag' => null, 'steuer' => round($kz84, 2)], 'ust89' => ['name' => 'Abzuführende USt aus Kz 89 (19 %)', 'betrag' => null, 'steuer' => $ust89, 'xml' => false],
'35' => ['name' => 'Steuerfreie Umsätze (ohne Vorsteuerabzug)', 'betrag' => round($kz35, 2), 'steuer' => null], '66' => ['name' => 'Gesamte Vorsteuerbeträge', 'betrag' => null, 'steuer' => round($kz66, 2), 'xml' => true],
'41' => ['name' => 'Steuerfreie ig. Lieferungen (§ 4 Nr. 1b)', 'betrag' => round($kz41, 2), 'steuer' => null], '61' => ['name' => 'Vorsteuerbeträge i.g. Erwerb', 'betrag' => null, 'steuer' => round($kz61, 2), 'xml' => true],
'44' => ['name' => 'Steuerfreie Ausfuhren (§ 4 Nr. 1a)', 'betrag' => round($kz44, 2), 'steuer' => null], '83' => ['name' => 'Vorauszahlung / Überschuss', 'betrag' => null, 'steuer' => round($kz83, 2), 'xml' => true],
'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)],
]; ];
} }
/** /**
* Liefert alle Buchungszeilen, die zu einer Kennzahl beitragen (Drill-down). * 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 public function fetchBookingsForKz(int $year, int $month, string $kz): array
{ {
@ -186,11 +128,7 @@ class UstvaCalculator
$parts[] = $select $parts[] = $select
. ' AND k.ustva_kz IN (' . $ph . ')' . ' AND k.ustva_kz IN (' . $ph . ')'
. ' AND (bk.steuerart IS NULL OR bk.steuerart NOT IN (' . $euPh . '))'; . ' AND (bk.steuerart IS NULL OR bk.steuerart NOT IN (' . $euPh . '))';
$params = array_merge($params, $params = array_merge($params, [$year, $month], $accountKzList, self::EU_STEUERARTEN);
[$year, $month],
$accountKzList,
self::EU_STEUERARTEN
);
} }
if (!empty($steuerartList)) { if (!empty($steuerartList)) {
@ -199,8 +137,9 @@ class UstvaCalculator
$params = array_merge($params, [$year, $month], $steuerartList); $params = array_merge($params, [$year, $month], $steuerartList);
} }
$sql = implode(' UNION ALL ', $parts) . ' ORDER BY buchungs_datum, id'; return Database::fetchAll(
implode(' UNION ALL ', $parts) . ' ORDER BY buchungs_datum, id',
return Database::fetchAll($sql, $params); $params
);
} }
} }

View file

@ -30,11 +30,9 @@ $fmtKz = fn($v) => $v !== null ? number_format((float)$v, 2, ',', '.') . ' €'
<tbody> <tbody>
<?php <?php
$sections = [ $sections = [
'Steuerpflichtige Umsätze' => ['81', '86', '35', '41', '44'], 'Umsätze' => ['81', 'ust81', '89', 'ust89'],
'Umsatzsteuer auf Umsätze' => ['83', '84'], 'Vorsteuer' => ['66', '61'],
'Innergemeinschaftlicher Erwerb' => ['89', '91'], 'Ergebnis' => ['83'],
'Vorsteuerbeträge' => ['61', '62', '67', '66'],
'Verbleibende Zahllast' => ['69'],
]; ];
foreach ($sections as $sectionTitle => $kzNrs): foreach ($sections as $sectionTitle => $kzNrs):
?> ?>
@ -46,23 +44,32 @@ $fmtKz = fn($v) => $v !== null ? number_format((float)$v, 2, ',', '.') . ' €'
<?php foreach ($kzNrs as $nr): <?php foreach ($kzNrs as $nr):
if (!isset($kz[$nr])) continue; if (!isset($kz[$nr])) continue;
$field = $kz[$nr]; $field = $kz[$nr];
$isResult = in_array($nr, ['69']); $isResult = ($nr === '83');
$isDisplay = !($field['xml'] ?? true);
$isNegative = $isResult && ($field['steuer'] ?? 0) < 0; $isNegative = $isResult && ($field['steuer'] ?? 0) < 0;
?> ?>
<tr style="<?= $isResult ? 'font-weight:700;background:#f0f4ff' : '' ?>"> <tr style="<?= $isResult ? 'font-weight:700;background:#f0f4ff' : '' ?>">
<td class="td-mono" style="color:var(--color-muted);width:50px"><?= $h($nr) ?></td> <td class="td-mono" style="color:var(--color-muted);width:50px"><?= $isDisplay ? '' : $h($nr) ?></td>
<td><?= $h($field['name']) ?></td> <td><?= $h($field['name']) ?></td>
<td class="td-right td-mono"> <td class="td-right td-mono">
<?php if ($field['betrag'] !== null): ?> <?php if ($field['betrag'] !== null): ?>
<?php if (!$isDisplay): ?>
<a href="/tax/ustva/<?= $year ?>/<?= $month ?>/kz/<?= $nr ?>" style="text-decoration:none;color:inherit"><?= $fmtKz($field['betrag']) ?></a> <a href="/tax/ustva/<?= $year ?>/<?= $month ?>/kz/<?= $nr ?>" style="text-decoration:none;color:inherit"><?= $fmtKz($field['betrag']) ?></a>
<?php else: ?> <?php else: ?>
<?= $fmtKz($field['betrag']) ?>
<?php endif; ?>
<?php else: ?>
<?= $isResult ? '' : '–' ?> <?= $isResult ? '' : '–' ?>
<?php endif; ?> <?php endif; ?>
</td> </td>
<td class="td-right td-mono" style="<?= $isNegative ? 'color:var(--color-success)' : ($isResult ? 'color:var(--color-danger)' : '') ?>"> <td class="td-right td-mono" style="<?= $isNegative ? 'color:var(--color-success)' : ($isResult ? 'color:var(--color-danger)' : '') ?>">
<?php if ($field['steuer'] !== null): ?> <?php if ($field['steuer'] !== null): ?>
<?php if (!$isDisplay): ?>
<a href="/tax/ustva/<?= $year ?>/<?= $month ?>/kz/<?= $nr ?>" style="text-decoration:none;color:inherit"><?= $fmtKz($field['steuer']) ?></a> <a href="/tax/ustva/<?= $year ?>/<?= $month ?>/kz/<?= $nr ?>" style="text-decoration:none;color:inherit"><?= $fmtKz($field['steuer']) ?></a>
<?php else: ?> <?php else: ?>
<?= $fmtKz($field['steuer']) ?>
<?php endif; ?>
<?php else: ?>
– –
<?php endif; ?> <?php endif; ?>
</td> </td>
@ -99,40 +106,45 @@ $fmtKz = fn($v) => $v !== null ? number_format((float)$v, 2, ',', '.') . ' €'
<div class="card-header"><div class="card-title">Zusammenfassung</div></div> <div class="card-header"><div class="card-title">Zusammenfassung</div></div>
<div class="card-body"> <div class="card-body">
<?php <?php
$ust83 = (float)($kz['83']['steuer'] ?? 0); $kz81val = (float)($kz['81']['betrag'] ?? 0);
$ust84 = (float)($kz['84']['steuer'] ?? 0); $kz89val = (float)($kz['89']['betrag'] ?? 0);
$ust91 = (float)($kz['91']['steuer'] ?? 0); $ust81val = (float)($kz['ust81']['steuer'] ?? 0);
$ustGes = $ust83 + $ust84 + $ust91; $ust89val = (float)($kz['ust89']['steuer'] ?? 0);
$vst66 = (float)($kz['66']['steuer'] ?? 0); $vst66val = (float)($kz['66']['steuer'] ?? 0);
$zahllast = (float)($kz['69']['steuer'] ?? 0); $vst61val = (float)($kz['61']['steuer'] ?? 0);
$zahllast = (float)($kz['83']['steuer'] ?? 0);
?> ?>
<table style="width:100%;font-size:.9rem"> <table style="width:100%;font-size:.9rem">
<tr> <tr>
<td style="padding:.35rem 0;color:var(--color-muted)">Umsatz 19 %</td> <td style="padding:.35rem 0;color:var(--color-muted)">Umsatz 19 % (Kz 81)</td>
<td class="td-right td-mono"><?= $fmtKz($kz['81']['betrag']) ?></td> <td class="td-right td-mono"><?= $fmtKz($kz81val) ?></td>
</tr> </tr>
<?php if ($kz89val != 0.0): ?>
<tr> <tr>
<td style="padding:.35rem 0;color:var(--color-muted)">Umsatz 7 %</td> <td style="padding:.35rem 0;color:var(--color-muted)">ig. Erwerb 19 % (Kz 89)</td>
<td class="td-right td-mono"><?= $fmtKz($kz['86']['betrag']) ?></td> <td class="td-right td-mono"><?= $fmtKz($kz89val) ?></td>
</tr> </tr>
<?php endif; ?>
<tr> <tr>
<td style="padding:.35rem 0;color:var(--color-muted)">USt (Kz 83+84)</td> <td style="padding:.35rem 0;color:var(--color-muted)">USt aus Kz 81</td>
<td class="td-right td-mono"><?= $fmtKz($ust83 + $ust84) ?></td> <td class="td-right td-mono"><?= $fmtKz($ust81val) ?></td>
</tr> </tr>
<?php if ($ust91 != 0.0): ?> <?php if ($ust89val != 0.0): ?>
<tr> <tr>
<td style="padding:.35rem 0;color:var(--color-muted)">ig. Erwerb USt (Kz 91)</td> <td style="padding:.35rem 0;color:var(--color-muted)">USt aus Kz 89</td>
<td class="td-right td-mono"><?= $fmtKz($ust91) ?></td> <td class="td-right td-mono"><?= $fmtKz($ust89val) ?></td>
</tr> </tr>
<?php endif; ?> <?php endif; ?>
<tr style="border-top:1px solid #e2e8f0"> <tr style="border-top:1px solid #e2e8f0">
<td style="padding:.35rem 0;font-weight:600">USt gesamt</td>
<td class="td-right td-mono" style="font-weight:600"><?= $fmtKz($ustGes) ?></td>
</tr>
<tr>
<td style="padding:.35rem 0;color:var(--color-muted)">Vorsteuer (Kz 66)</td> <td style="padding:.35rem 0;color:var(--color-muted)">Vorsteuer (Kz 66)</td>
<td class="td-right td-mono" style="color:var(--color-success)">– <?= $fmtKz($vst66) ?></td> <td class="td-right td-mono" style="color:var(--color-success)">– <?= $fmtKz($vst66val) ?></td>
</tr> </tr>
<?php if ($vst61val != 0.0): ?>
<tr>
<td style="padding:.35rem 0;color:var(--color-muted)">i.g. Erwerb VSt (Kz 61)</td>
<td class="td-right td-mono" style="color:var(--color-success)">– <?= $fmtKz($vst61val) ?></td>
</tr>
<?php endif; ?>
<tr style="border-top:2px solid #000;font-weight:700"> <tr style="border-top:2px solid #000;font-weight:700">
<td style="padding:.5rem 0"><?= $zahllast >= 0 ? 'Vorauszahlung' : 'Erstattung' ?></td> <td style="padding:.5rem 0"><?= $zahllast >= 0 ? 'Vorauszahlung' : 'Erstattung' ?></td>
<td class="td-right td-mono" style="color:<?= $zahllast >= 0 ? 'var(--color-danger)' : 'var(--color-success)' ?>"> <td class="td-right td-mono" style="color:<?= $zahllast >= 0 ? 'var(--color-danger)' : 'var(--color-success)' ?>">

View file

@ -1 +1 @@
1-260510 1-260514

View file

@ -111,32 +111,18 @@ class TaxSummenTest extends TestCase
'UStVA Kz 81 (Umsätze 19%): Betrag enthält Einnahme ≥ 500 €' '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( $this->assert(
(float)$kz['83']['steuer'] >= 95.00, (float)$kz['66']['steuer'] >= 38.00,
'UStVA Kz 83 (USt 19%): Steuerbetrag ≥ 95 €' '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( $this->assert(
(float)$kz['61']['steuer'] >= 38.00, isset($kz['83']['steuer']),
'UStVA Kz 61 (Vorsteuer 19%): Steuerbetrag enthält Ausgabe ≥ 38 €' '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 ──────────────────────────────────────────────────── // ── EÜR prüfen ────────────────────────────────────────────────────
$euerCalc = new \Steuerkram\Services\EuerCalculator(); $euerCalc = new \Steuerkram\Services\EuerCalculator();
$euer = $euerCalc->calculate($year); $euer = $euerCalc->calculate($year);

View file

@ -5,8 +5,9 @@ declare(strict_types=1);
/** /**
* Test 7: UStVA-Kennzahlen für Auslandsumsätze und ig. Erwerb. * Test 7: UStVA-Kennzahlen für Auslandsumsätze und ig. Erwerb.
* *
* Prüft, dass steuerart-Buchungen korrekt in die UStVA-Kennzahlen * Prüft, dass steuerart-Buchungen korrekt in die UStVA-Kennzahlen einfließen.
* 41, 44, 89, 91 und 67 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 class UstvaAuslandTest extends TestCase
{ {
@ -18,7 +19,6 @@ class UstvaAuslandTest extends TestCase
$month = (int)date('n'); $month = (int)date('n');
$datum = date('Y-m-d'); $datum = date('Y-m-d');
// Konten aus SKR03
$erloesId = $this->findKonto('Betriebseinnahme'); $erloesId = $this->findKonto('Betriebseinnahme');
$ausgabeId = $this->findKonto('Betriebsausgabe'); $ausgabeId = $this->findKonto('Betriebsausgabe');
$zahlungId = $this->findKonto('Zahlungskonto'); $zahlungId = $this->findKonto('Zahlungskonto');
@ -38,7 +38,8 @@ class UstvaAuslandTest extends TestCase
$calc = new \Steuerkram\Services\UstvaCalculator(); $calc = new \Steuerkram\Services\UstvaCalculator();
$kzPre = $calc->calculate($year, $month); $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([ $belegIgL = \Steuerkram\Models\Beleg::create([
'beleg_nr' => 'TEST-IGL-' . uniqid(), 'beleg_nr' => 'TEST-IGL-' . uniqid(),
'filename' => 'test-igl.pdf', 'filename' => 'test-igl.pdf',
@ -60,7 +61,7 @@ class UstvaAuslandTest extends TestCase
'buchungstext' => 'Test ig. Lieferung EU', 'buchungstext' => 'Test ig. Lieferung EU',
]); ]);
// ── 2. Steuerfreie Ausfuhr → Kz 44 ─────────────────────────────── // ── 2. Steuerfreie Ausfuhr (steuerart=export) ─────────────────────────
$belegExp = \Steuerkram\Models\Beleg::create([ $belegExp = \Steuerkram\Models\Beleg::create([
'beleg_nr' => 'TEST-EXP-' . uniqid(), 'beleg_nr' => 'TEST-EXP-' . uniqid(),
'filename' => 'test-exp.pdf', 'filename' => 'test-exp.pdf',
@ -82,7 +83,7 @@ class UstvaAuslandTest extends TestCase
'buchungstext' => 'Test Ausfuhr Drittland', '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([ $belegIgE19 = \Steuerkram\Models\Beleg::create([
'beleg_nr' => 'TEST-IGE19-' . uniqid(), 'beleg_nr' => 'TEST-IGE19-' . uniqid(),
'filename' => 'test-ige19.pdf', 'filename' => 'test-ige19.pdf',
@ -104,20 +105,12 @@ class UstvaAuslandTest extends TestCase
'buchungstext' => 'Test ig. Erwerb 19%', 'buchungstext' => 'Test ig. Erwerb 19%',
]); ]);
// ── UStVA berechnen und Delta prüfen ────────────────────────────── // ── UStVA berechnen und Delta prüfen ──────────────────────────────────
$kz = $calc->calculate($year, $month); $kz = $calc->calculate($year, $month);
// Kz 41: ig. Lieferung 1000 € // ig. Lieferung/Ausfuhr fließen nicht in Kz 81 ein (EU-Steuerarten-Filter)
$this->assert( $delta81 = round((float)$kz['81']['betrag'] - (float)$kzPre['81']['betrag'], 2);
(float)$kz['41']['betrag'] >= 1000.00, $this->assertEquals(0.00, $delta81, 'ig. Lieferung/Ausfuhr gehen nicht in Kz 81 ein');
'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 €'
);
// Kz 89: ig. Erwerb 19% Bemessungsgrundlage +500 € // Kz 89: ig. Erwerb 19% Bemessungsgrundlage +500 €
$this->assert( $this->assert(
@ -125,32 +118,18 @@ class UstvaAuslandTest extends TestCase
'Kz 89 (ig. Erwerb 19% Bmgl.): Delta ≥ 500 €' '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( $this->assertEquals(
95.00, 95.00,
round((float)$kz['91']['steuer'] - (float)$kzPre['91']['steuer'], 2), round((float)$kz['61']['steuer'] - (float)$kzPre['61']['steuer'], 2),
'Kz 91 (USt ig. Erwerb 19%): Delta = 95,00 €' 'Kz 61 (VSt ig. Erwerb): Delta = 95,00 €'
); );
// Kz 67: VSt ig. Erwerb kontobasiert (1574) +95 € // ReverseCharge ist zahllastneutral: USt auf 1774 (+95) wird durch Kz61-Abzug (-95) aufgehoben
$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
$this->assertEquals( $this->assertEquals(
0.00, 0.00,
round((float)$kz['69']['steuer'] - (float)$kzPre['69']['steuer'], 2), round((float)$kz['83']['steuer'] - (float)$kzPre['83']['steuer'], 2),
'Kz 69 (Zahllast) durch ig. Erwerb nicht verändert' 'Kz 83 (Vorauszahlung) 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'
); );
} }

View file

@ -6,27 +6,28 @@ use Steuerkram\Services\ElsterXmlService;
class ElsterXmlKz39Test extends TestCase 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 public function run(): void
{ {
$service = new ElsterXmlService(); $service = new ElsterXmlService();
// Fall 1: Zahllast positiv → Kz39 muss vorhanden und positiv sein // Fall 1: Zahllast positiv → Kz83 muss vorhanden und positiv sein
$kzZahllast = ['69' => ['steuer' => 150.00]]; $kzZahllast = ['83' => ['steuer' => 150.00]];
$xml = $service->generate(2026, 1, $kzZahllast); $xml = $service->generate(2026, 1, $kzZahllast);
$this->assert(str_contains($xml, '<Kz39>'), 'Kz39 bei Zahllast vorhanden'); $this->assert(str_contains($xml, '<Kz83>'), 'Kz83 bei Zahllast vorhanden');
$this->assert(str_contains($xml, '<Kz39>150.00</Kz39>'), 'Kz39 ohne Vorzeichen bei Zahllast'); $this->assert(str_contains($xml, '<Kz83>150.00</Kz83>'), 'Kz83 ohne Vorzeichen bei Zahllast');
// Fall 2: Erstattung (negativ) → Kz39 darf nicht vorkommen // Fall 2: Erstattung (negativ) → Kz83 muss vorhanden und negativ sein
$kzErstattung = ['69' => ['steuer' => -50.00]]; $kzErstattung = ['83' => ['steuer' => -50.00]];
$xml = $service->generate(2026, 1, $kzErstattung); $xml = $service->generate(2026, 1, $kzErstattung);
$this->assert(!str_contains($xml, '<Kz39>'), 'Kz39 bei Erstattung nicht vorhanden'); $this->assert(str_contains($xml, '<Kz83>'), 'Kz83 bei Erstattung vorhanden');
$this->assert(str_contains($xml, '<Kz83>-50.00</Kz83>'), 'Kz83 mit negativem Vorzeichen bei Erstattung');
// Fall 3: Zahllast = 0 → Kz39 weglassen // Fall 3: Zahllast = 0 → Kz83 weglassen
$kzNull = ['69' => ['steuer' => 0.0]]; $kzNull = ['83' => ['steuer' => 0.0]];
$xml = $service->generate(2026, 1, $kzNull); $xml = $service->generate(2026, 1, $kzNull);
$this->assert(!str_contains($xml, '<Kz39>'), 'Kz39 bei Nullbetrag nicht vorhanden'); $this->assert(!str_contains($xml, '<Kz83>'), 'Kz83 bei Nullbetrag nicht vorhanden');
// Fall 4: XML ist valides XML // Fall 4: XML ist valides XML
$doc = new \DOMDocument(); $doc = new \DOMDocument();

View file

@ -93,9 +93,9 @@ class StornoBuchungTest extends TestCase
); );
// ── UStVA: Vorsteuer-KZ hebt sich auf ───────────────────────── // ── 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). // 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( $vstReversalRow = \Steuerkram\Database::fetchOne(
'SELECT * FROM bookings WHERE parent_booking_id = ? AND is_reversal = 1', 'SELECT * FROM bookings WHERE parent_booking_id = ? AND is_reversal = 1',
[$reversalId] [$reversalId]
@ -106,7 +106,7 @@ class StornoBuchungTest extends TestCase
} }
$ustvaCalc = new \Steuerkram\Services\UstvaCalculator(); $ustvaCalc = new \Steuerkram\Services\UstvaCalculator();
$bookings = $ustvaCalc->fetchBookingsForKz($year, $month, '61'); $bookings = $ustvaCalc->fetchBookingsForKz($year, $month, '66');
$vstReversalFound = false; $vstReversalFound = false;
foreach ($bookings as $b) { foreach ($bookings as $b) {
@ -117,6 +117,6 @@ class StornoBuchungTest extends TestCase
break; 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');
} }
} }

View file

@ -6,7 +6,7 @@ declare(strict_types=1);
* Test 28: Vorsteuerkonten-Buchungszeilen. * Test 28: Vorsteuerkonten-Buchungszeilen.
* *
* Prüft, dass Ausgabe-Buchungen mit USt 7 % / 19 % automatisch eine * 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. * kontobasiert ableitet und ein Storno die VSt-Zeilen mitaufhebt.
*/ */
class VorsteuerkontenTest extends TestCase class VorsteuerkontenTest extends TestCase
@ -95,24 +95,16 @@ class VorsteuerkontenTest extends TestCase
$this->assertEquals('1571', $vstRow7['kontonummer'] ?? '', 'VSt-Buchungszeile auf Konto 1571'); $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'); $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); $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( $this->assert(
round((float)$kzMit['66']['steuer'] - (float)$kzPre['66']['steuer'], 2) >= 45.00, 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 ──────────── // ── Drill-down Kz 66 enthält die VSt-Buchungszeile (1576) ─────
$drilldown = $calc->fetchBookingsForKz($year, $month, '61'); $drilldown = $calc->fetchBookingsForKz($year, $month, '66');
$vstFound = false; $vstFound = false;
foreach ($drilldown as $b) { foreach ($drilldown as $b) {
if ((int)$b['id'] === (int)$vstRow19['id']) { if ((int)$b['id'] === (int)$vstRow19['id']) {
@ -120,7 +112,7 @@ class VorsteuerkontenTest extends TestCase
break; 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 ─────────────── // ── Storno: VSt-Zeile muss mitaufgehoben werden ───────────────
$reversalId = \Steuerkram\Models\Booking::reverse($bookingId19, 'Testfall VSt-Storno'); $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'); $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 // UStVA Kz 66 nach Storno: Netto-Delta = 7 € (nur 7%-Buchung verbleibt)
// (die 19%-Buchung und ihr Storno heben sich auf; 7%-Buchung geht in Kz 62)
$kzNach = $calc->calculate($year, $month); $kzNach = $calc->calculate($year, $month);
$delta61 = round((float)$kzNach['61']['steuer'] - (float)$kzPre['61']['steuer'], 2); $delta66 = round((float)$kzNach['66']['steuer'] - (float)$kzPre['66']['steuer'], 2);
$this->assertEquals(0.00, $delta61, 'Kz 61: Buchung+Storno heben sich gegenüber Ausgangslage auf'); $this->assertEquals(7.00, $delta66, 'Kz 66: Netto-Delta = 7 € nach Storno der 19%-Buchung');
} }
} }

View file

@ -95,19 +95,18 @@ class UmsatzsteuerkontenTest extends TestCase
$this->assertEquals('1771', $ustRow7['kontonummer'] ?? '', 'USt-Buchungszeile auf Konto 1771'); $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'); $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); $kzMit = $calc->calculate($year, $month);
$this->assert( $this->assert(
round((float)$kzMit['83']['steuer'] - (float)$kzPre['83']['steuer'], 2) >= 95.00, round((float)$kzMit['83']['steuer'] - (float)$kzPre['83']['steuer'], 2) >= 95.00,
'UStVA Kz 83 stieg durch 19%-Buchung um ≥ 95 €' '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'); $drilldown = $calc->fetchBookingsForKz($year, $month, '83');
$ustFound = false; $ustFound = false;
foreach ($drilldown as $b) { foreach ($drilldown as $b) {
@ -116,7 +115,7 @@ class UmsatzsteuerkontenTest extends TestCase
break; 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 ─────────────── // ── Storno: USt-Zeile muss mitaufgehoben werden ───────────────
$reversalId = \Steuerkram\Models\Booking::reverse($bookingId19, 'Testfall USt-Storno'); $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'); $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); $kzNach = $calc->calculate($year, $month);
$delta83 = round((float)$kzNach['83']['steuer'] - (float)$kzPre['83']['steuer'], 2); $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');
} }
} }

View file

@ -6,7 +6,8 @@ declare(strict_types=1);
* Test 30: Innergemeinschaftlicher Erwerb 19 % – ReverseCharge-Kindzeilen. * Test 30: Innergemeinschaftlicher Erwerb 19 % – ReverseCharge-Kindzeilen.
* *
* Prüft, dass ig_erwerb_19-Buchungen automatisch zwei Kindzeilen erhalten * 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. * Storno muss beide Kindzeilen mitaufheben.
*/ */
class IgErwerbReverseChargeTest extends TestCase 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(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'); $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); $kzMit = $calc->calculate($year, $month);
$delta89 = round((float)$kzMit['89']['betrag'] - (float)$kzPre['89']['betrag'], 2); $delta89 = round((float)$kzMit['89']['betrag'] - (float)$kzPre['89']['betrag'], 2);
$delta91 = round((float)$kzMit['91']['steuer'] - (float)$kzPre['91']['steuer'], 2); $delta61 = round((float)$kzMit['61']['steuer'] - (float)$kzPre['61']['steuer'], 2);
$delta67 = round((float)$kzMit['67']['steuer'] - (float)$kzPre['67']['steuer'], 2); $delta83 = round((float)$kzMit['83']['steuer'] - (float)$kzPre['83']['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(1000.00, $delta89, 'Kz 89 stieg um 1000 €');
$this->assertEquals(190.00, $delta91, 'Kz 91 stieg um 190 €'); $this->assertEquals(190.00, $delta61, 'Kz 61 (VSt ig. Erwerb, 1574) stieg um 190 €');
$this->assertEquals(190.00, $delta67, 'Kz 67 stieg um 190 €'); // ReverseCharge ist zahllastneutral: USt auf 1774 (+190) minus Kz61-Abzug (-190) = 0
$this->assertEquals(0.00, $delta69, 'Kz 69 (Zahllast) unverändert – ReverseCharge neutral'); $this->assertEquals(0.00, $delta83, 'Kz 83 (Vorauszahlung) unverändert – ReverseCharge neutral');
// Drill-down Kz 91 enthält die 1774-Kindzeile // Drill-down Kz 83 enthält die 1774-Kindzeile
$drilldown = $calc->fetchBookingsForKz($year, $month, '91'); $drilldown = $calc->fetchBookingsForKz($year, $month, '83');
$found = false; $found = false;
foreach ($drilldown as $b) { foreach ($drilldown as $b) {
if ((int)$b['id'] === (int)$kind1774['id']) { $found = true; break; } 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 // Storno: beide Kindzeilen werden mitaufgehoben
$reversalId = \Steuerkram\Models\Booking::reverse($bookingId, 'Test IGE-Storno'); $reversalId = \Steuerkram\Models\Booking::reverse($bookingId, 'Test IGE-Storno');
@ -118,9 +118,9 @@ class IgErwerbReverseChargeTest extends TestCase
$this->assertEquals(2, count($stornoKinder), 'Storno erzeugt zwei Kindzeilen'); $this->assertEquals(2, count($stornoKinder), 'Storno erzeugt zwei Kindzeilen');
$kzNach = $calc->calculate($year, $month); $kzNach = $calc->calculate($year, $month);
$delta91n = round((float)$kzNach['91']['steuer'] - (float)$kzPre['91']['steuer'], 2); $delta61n = round((float)$kzNach['61']['steuer'] - (float)$kzPre['61']['steuer'], 2);
$delta67n = round((float)$kzNach['67']['steuer'] - (float)$kzPre['67']['steuer'], 2); $delta83n = round((float)$kzNach['83']['steuer'] - (float)$kzPre['83']['steuer'], 2);
$this->assertEquals(0.00, $delta91n, 'Kz 91 nach Storno wieder auf Ausgangswert'); $this->assertEquals(0.00, $delta61n, 'Kz 61 nach Storno wieder auf Ausgangswert');
$this->assertEquals(0.00, $delta67n, 'Kz 67 nach Storno wieder auf Ausgangswert'); $this->assertEquals(0.00, $delta83n, 'Kz 83 nach Storno wieder auf Ausgangswert');
} }
} }