Refactor: UStVA auf 5-Kennzahlen-Schema vereinfacht (Kz 81/89/66/61/83)
This commit is contained in:
parent
6003bab2a8
commit
88aecd86c2
15 changed files with 249 additions and 347 deletions
11
Changes.md
11
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
|
||||
|
|
|
|||
|
|
@ -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 %) | `<Kz81>` | ganzzahlig | Wert 0 → Tag wird weggelassen |
|
||||
| `Kz 83` (USt 19 %) | – | – | **nicht übertragen**, ELSTER leitet serverseitig aus Kz 81 × 19 % ab |
|
||||
| `Kz 86` (Bemessungsgrundlage 7 %) | `<Kz86>` | ganzzahlig | Wert 0 → Tag wird weggelassen |
|
||||
| `Kz 84` (USt 7 %) | – | – | **nicht übertragen**, ELSTER leitet serverseitig aus Kz 86 × 7 % ab |
|
||||
| `Kz 35` | `<Kz35>` | ganzzahlig | – |
|
||||
| `Kz 41` | `<Kz41>` | ganzzahlig | – |
|
||||
| `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 |
|
||||
| `Kz 89` (ig. Erwerb Bemessungsgrundlage) | `<Kz89>` | ganzzahlig | Wert 0 → Tag wird weggelassen |
|
||||
| `Kz 66` (Vorsteuerbeträge gesamt) | `<Kz66>` | 2 Dezimalstellen | – |
|
||||
| `Kz 61` (Vorsteuerbeträge ig. Erwerb) | `<Kz61>` | 2 Dezimalstellen | – |
|
||||
| `Kz 83` (Vorauszahlung / Überschuss) | `<Kz83>` | 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 `<KzXX>0</KzXX>` 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 `<Kz81>0</Kz81>` 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 `<Kz81>0</Kz81>` sollte gesetzt sein, falls ELSTER eine leere Meldung ablehnt |
|
||||
|
||||
### Verifikation vor Abgabe
|
||||
|
||||
|
|
|
|||
23
migrations/048_ustva_kz_neustrukturierung.sql
Normal file
23
migrations/048_ustva_kz_neustrukturierung.sql
Normal 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');
|
||||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -30,11 +30,9 @@ $fmtKz = fn($v) => $v !== null ? number_format((float)$v, 2, ',', '.') . ' €'
|
|||
<tbody>
|
||||
<?php
|
||||
$sections = [
|
||||
'Steuerpflichtige Umsätze' => ['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, ',', '.') . ' €'
|
|||
<?php foreach ($kzNrs as $nr):
|
||||
if (!isset($kz[$nr])) continue;
|
||||
$field = $kz[$nr];
|
||||
$isResult = in_array($nr, ['69']);
|
||||
$isResult = ($nr === '83');
|
||||
$isDisplay = !($field['xml'] ?? true);
|
||||
$isNegative = $isResult && ($field['steuer'] ?? 0) < 0;
|
||||
?>
|
||||
<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 class="td-right td-mono">
|
||||
<?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>
|
||||
<?php else: ?>
|
||||
<?= $fmtKz($field['betrag']) ?>
|
||||
<?php endif; ?>
|
||||
<?php else: ?>
|
||||
<?= $isResult ? '' : '–' ?>
|
||||
<?php endif; ?>
|
||||
</td>
|
||||
<td class="td-right td-mono" style="<?= $isNegative ? 'color:var(--color-success)' : ($isResult ? 'color:var(--color-danger)' : '') ?>">
|
||||
<?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>
|
||||
<?php else: ?>
|
||||
<?= $fmtKz($field['steuer']) ?>
|
||||
<?php endif; ?>
|
||||
<?php else: ?>
|
||||
–
|
||||
<?php endif; ?>
|
||||
</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-body">
|
||||
<?php
|
||||
$ust83 = (float)($kz['83']['steuer'] ?? 0);
|
||||
$ust84 = (float)($kz['84']['steuer'] ?? 0);
|
||||
$ust91 = (float)($kz['91']['steuer'] ?? 0);
|
||||
$ustGes = $ust83 + $ust84 + $ust91;
|
||||
$vst66 = (float)($kz['66']['steuer'] ?? 0);
|
||||
$zahllast = (float)($kz['69']['steuer'] ?? 0);
|
||||
$kz81val = (float)($kz['81']['betrag'] ?? 0);
|
||||
$kz89val = (float)($kz['89']['betrag'] ?? 0);
|
||||
$ust81val = (float)($kz['ust81']['steuer'] ?? 0);
|
||||
$ust89val = (float)($kz['ust89']['steuer'] ?? 0);
|
||||
$vst66val = (float)($kz['66']['steuer'] ?? 0);
|
||||
$vst61val = (float)($kz['61']['steuer'] ?? 0);
|
||||
$zahllast = (float)($kz['83']['steuer'] ?? 0);
|
||||
?>
|
||||
<table style="width:100%;font-size:.9rem">
|
||||
<tr>
|
||||
<td style="padding:.35rem 0;color:var(--color-muted)">Umsatz 19 %</td>
|
||||
<td class="td-right td-mono"><?= $fmtKz($kz['81']['betrag']) ?></td>
|
||||
<td style="padding:.35rem 0;color:var(--color-muted)">Umsatz 19 % (Kz 81)</td>
|
||||
<td class="td-right td-mono"><?= $fmtKz($kz81val) ?></td>
|
||||
</tr>
|
||||
<?php if ($kz89val != 0.0): ?>
|
||||
<tr>
|
||||
<td style="padding:.35rem 0;color:var(--color-muted)">Umsatz 7 %</td>
|
||||
<td class="td-right td-mono"><?= $fmtKz($kz['86']['betrag']) ?></td>
|
||||
<td style="padding:.35rem 0;color:var(--color-muted)">ig. Erwerb 19 % (Kz 89)</td>
|
||||
<td class="td-right td-mono"><?= $fmtKz($kz89val) ?></td>
|
||||
</tr>
|
||||
<?php endif; ?>
|
||||
<tr>
|
||||
<td style="padding:.35rem 0;color:var(--color-muted)">USt (Kz 83+84)</td>
|
||||
<td class="td-right td-mono"><?= $fmtKz($ust83 + $ust84) ?></td>
|
||||
<td style="padding:.35rem 0;color:var(--color-muted)">USt aus Kz 81</td>
|
||||
<td class="td-right td-mono"><?= $fmtKz($ust81val) ?></td>
|
||||
</tr>
|
||||
<?php if ($ust91 != 0.0): ?>
|
||||
<?php if ($ust89val != 0.0): ?>
|
||||
<tr>
|
||||
<td style="padding:.35rem 0;color:var(--color-muted)">ig. Erwerb USt (Kz 91)</td>
|
||||
<td class="td-right td-mono"><?= $fmtKz($ust91) ?></td>
|
||||
<td style="padding:.35rem 0;color:var(--color-muted)">USt aus Kz 89</td>
|
||||
<td class="td-right td-mono"><?= $fmtKz($ust89val) ?></td>
|
||||
</tr>
|
||||
<?php endif; ?>
|
||||
<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 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>
|
||||
<?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">
|
||||
<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)' ?>">
|
||||
|
|
|
|||
|
|
@ -1 +1 @@
|
|||
1-260510
|
||||
1-260514
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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'
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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>'), 'Kz39 bei Zahllast vorhanden');
|
||||
$this->assert(str_contains($xml, '<Kz39>150.00</Kz39>'), 'Kz39 ohne Vorzeichen bei Zahllast');
|
||||
$this->assert(str_contains($xml, '<Kz83>'), 'Kz83 bei Zahllast vorhanden');
|
||||
$this->assert(str_contains($xml, '<Kz83>150.00</Kz83>'), '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>'), '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
|
||||
$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>'), 'Kz39 bei Nullbetrag nicht vorhanden');
|
||||
$this->assert(!str_contains($xml, '<Kz83>'), 'Kz83 bei Nullbetrag nicht vorhanden');
|
||||
|
||||
// Fall 4: XML ist valides XML
|
||||
$doc = new \DOMDocument();
|
||||
|
|
|
|||
|
|
@ -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');
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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');
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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');
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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');
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue