Fix: UStVA-Kennziffern auf offizielle ELSTER-Definitionen korrigiert

This commit is contained in:
fds2610-bot 2026-04-26 17:12:40 +02:00
parent 0866344f51
commit bbfb41232e
11 changed files with 172 additions and 216 deletions

View file

@ -2,6 +2,18 @@
---
## 2026-04-26 – Fix UStVA: Kennziffern auf offizielle ELSTER-Definitionen korrigiert
### Geändert
- `src/Services/UstvaCalculator.php`: Kz 66 ist jetzt die primäre Inlandsvorsteuer (§§ 14, 14a) – alle Ausgabe-Buchungen mit Vorsteuer fließen direkt in Kz 66 (buchungsbasiert, unabhängig vom USt-Satz). Kz 61 ist jetzt Vorsteuer aus ig. Erwerben (§ 3d) = Kz89×0,19 + Kz93×0,07. Kz 62 (Einfuhrumsatzsteuer) und Kz 67 (§ 13b) werden auf 0 gesetzt. Kz 83 ist jetzt das Ergebnis (Verbleibende Vorauszahlung / Überschuss) statt der USt auf 19 %-Umsätze. Kz 35/44 (veraltet seit Migration 017) und Kz 91/94 (wurden fälschlich als USt-Beträge auf ig. Erwerb genutzt) entfernt. Kz 43 (Ausfuhren) und Kz 48 (steuerfreie Umsätze ohne VSt-Abzug) hinzugefügt.
- `src/Services/ElsterXmlService.php`: Betragsfelder auf korrekte Kz aktualisiert (43/48 statt 35/44); Kz39-Export (Sondervorauszahlung) durch Kz83-Export (Vorauszahlung/Überschuss) ersetzt.
- `src/Views/tax/ustva_show.php`: Abschnitte und Beschriftungen auf korrekte ELSTER-Kennziffern angepasst.
- `src/Views/tax/ustva_kz_bookings.php`: Referenz auf Kz 69 → Kz 83 umgestellt.
- `src/Controllers/UstvaController.php`: Whitelist gültiger Kennziffern aktualisiert.
- `docs/berechnungslogik.md`: UStVA-Abschnitt vollständig neu beschrieben.
---
## 2026-04-26 – KI-Diagnose: Erreichbarkeitsprüfung und Fehlerlog für Ollama
### Geändert

View file

@ -43,44 +43,31 @@ Die EÜR basiert auf dem Feld `accounts.kontenart` sowie `accounts.euer_zeile` d
Die UStVA kombiniert drei Quellen:
**Quelle 1 – kontobasiert** (`accounts.ustva_kz`): reguläre Umsätze. Buchungen mit EU-Steuerarten werden hier ausgeschlossen. Kz 61/62 sind aus dieser Quelle entfernt – `ustva_kz='61'/'62'` wird ignoriert.
**Quelle 1 – kontobasiert** (`accounts.ustva_kz`): Umsatz- und steuerfreie Konten. Buchungen mit EU-Steuerarten werden hier ausgeschlossen.
**Quelle 2 – buchungsbasiert** (`bookings.ust_satz`): Vorsteuer Kz 61/62. Alle Ausgabe-Buchungen mit `betrag_ust != 0` fließen anhand ihres `ust_satz` direkt in Kz 61 (19 %) bzw. Kz 62 (7 %) ein, unabhängig davon welches Konto gebucht wurde. Buchungen mit EU-Steuerarten sind ausgenommen.
**Quelle 2 – buchungsbasiert** (`buchungstyp='Ausgabe'`, `betrag_ust != 0`): Alle inländischen Eingangsrechnungen mit Vorsteuer → **Kz 66** (§§ 14, 14a UStG). Buchungen mit EU-Steuerarten sind ausgenommen. Der Wert `ustva_kz` auf Vorsteuer-Konten wird nicht ausgewertet.
**Quelle 3 – steuerartbasiert** (`bookings.steuerart`): EU-Sonderfälle (ig. Lieferung, Ausfuhr, ig. Erwerb). Diese Buchungen landen unabhängig vom Kategorie-Konto in den richtigen Kennzahlen.
### Kontozuordnung und Kennzahlen
| Kz | Bezeichnung | Quelle | Erläuterung |
| Kz | ELSTER-Bezeichnung | Quelle | Erläuterung |
|---|---|---|---|
| **81** | Steuerpfl. Umsätze 19 % | `SUM(betrag_netto)` WHERE `ustva_kz='81'` | 8400, 8570, 8720, 8801, 8820, 8922 |
| **83** | USt auf steuerpfl. Umsätze 19 % | `SUM(betrag_ust)` WHERE `ustva_kz='81'` | s.o. |
| **86** | Steuerpfl. Umsätze 7 % | `SUM(betrag_netto)` WHERE `ustva_kz='86'` | 8300, 8710, 8921 |
| **84** | USt auf steuerpfl. Umsätze 7 % | `SUM(betrag_ust)` WHERE `ustva_kz='86'` | s.o. |
| **35** | Steuerfreie Umsätze | `SUM(betrag_netto)` WHERE `ustva_kz='35'` | 8100, 8105, 8110 |
| **41** | Steuerfreie ig. Lieferungen | kontobasiert (`ustva_kz='41'`) **+** `steuerart='ig_lieferung'` | 8125; oder Buchung mit steuerart |
| **44** | Steuerfreie Ausfuhren | kontobasiert (`ustva_kz='44'`) **+** `steuerart='export'` | 8120; oder Buchung mit steuerart |
| **61** | Vorsteuer 19 % | `SUM(betrag_ust)` WHERE `buchungstyp='Ausgabe' AND ROUND(ust_satz)=19` | buchungsbasiert – kein spezielles VSt19-Konto nötig |
| **62** | Vorsteuer 7 % | `SUM(betrag_ust)` WHERE `buchungstyp='Ausgabe' AND ROUND(ust_satz)=7` | buchungsbasiert – kein spezielles VSt7-Konto nötig |
| **41** | Steuerfreie ig. Lieferungen (§ 4 Nr. 1b) | kontobasiert (`ustva_kz='41'`) **+** `steuerart='ig_lieferung'` | 8125; oder Buchung mit steuerart |
| **43** | Steuerfreie Ausfuhren (§ 4 Nr. 1a) | kontobasiert (`ustva_kz='43'`) **+** `steuerart='export'` | 8120; oder Buchung mit steuerart |
| **48** | Steuerfreie Umsätze ohne Vorsteuerabzug | `SUM(betrag_netto)` WHERE `ustva_kz='48'` | 8100, 8105, 8110 |
| **89** | Ig. Erwerb 19 % – Bemessungsgrundlage | `SUM(betrag_netto)` WHERE `steuerart='ig_erwerb_19'` | kein spezielles Konto nötig |
| **91** | USt auf ig. Erwerb 19 % | `Kz 89 × 0,19` | berechnet |
| **93** | Ig. Erwerb 7 % – Bemessungsgrundlage | `SUM(betrag_netto)` WHERE `steuerart='ig_erwerb_7'` | kein spezielles Konto nötig |
| **94** | USt auf ig. Erwerb 7 % | `Kz 93 × 0,07` | berechnet |
| **66** | Vorsteuer aus Lieferungen/Leistungen (§§ 14, 14a) | `SUM(betrag_ust)` WHERE `buchungstyp='Ausgabe' AND betrag_ust != 0` | buchungsbasiert – alle inländischen VSt-Beträge unabhängig vom Satz |
| **61** | Vorsteuer aus ig. Erwerben (§ 3d) | `Kz 89 × 0,19 + Kz 93 × 0,07` | berechnet; ig. Erwerb ist USt-neutral (hebt sich in Kz 83 auf) |
| **62** | Vorsteuer aus Einfuhrumsatzsteuer | 0 | in diesem System nicht vorgesehen |
| **67** | Vorsteuer aus Leistungen nach § 13b | 0 | in diesem System nicht vorgesehen |
| **83** | Verbleibende Umsatzsteuer-Vorauszahlung / Überschuss | `ust19 + ust7 − Kz 66 − Kz 61` | Ergebnis; ust19/ust7 = intern aus `betrag_ust` von 81/86-Konten |
### Abgeleitete Kennzahlen
### Kennziffern-Bedeutung (ELSTER)
| Kz | Bezeichnung | Berechnung |
|---|---|---|
| **67** | Vorsteuer aus ig. Erwerb | Kz 91 + Kz 94 |
| **66** | Vorsteuerbeträge gesamt | Kz 61 + Kz 62 + Kz 67 |
| **69** | Verbleibende USt-Vorauszahlung / Erstattung | (Kz 83 + Kz 84 + Kz 91 + Kz 94) − Kz 66 |
Kz 83 ist laut ELSTER-Hilfe das **Ergebnis der Voranmeldung** (Verbleibende Vorauszahlung oder Überschuss), nicht – wie in früheren Systemversionen – die USt auf 19 %-Umsätze. Die ELSTER-Kennziffern 83, 61, 62, 66, 67 wurden mit dieser Änderung (2026-04-26) auf die offiziellen ELSTER-Definitionen korrigiert.
### Migration 022 – Vorsteuer-Kz-Aufteilung (historisch)
Vor Migration 022 trugen alle Vorsteuer-Konten `ustva_kz='66'`. Migration 022 setzte `ustva_kz='61'/'62'` je nach `steuercode`. Diese kontobasierte Zuordnung war erforderlich, weil alle Ausgaben eines Kontos denselben USt-Satz hatten.
### Buchungsbasierte Vorsteuer (ab 2026)
Die kontobasierte Zuordnung über `ustva_kz='61'/'62'` wurde zugunsten einer buchungsbasierten Zuordnung aufgegeben: `UstvaCalculator` wertet jetzt direkt `bookings.ust_satz` aus (`buchungstyp='Ausgabe'`, `betrag_ust != 0`). Damit reicht ein einzelnes Aufwandskonto für 19 % und 7 % USt aus. Der Wert `ustva_kz='61'/'62'` auf Konten wird nicht mehr ausgewertet.
Kz 66 wird weiterhin rechnerisch als `Kz 61 + Kz 62 + Kz 67` aggregiert.
Kz 35 und Kz 44 wurden in Migration 017 auf Kz 48 (steuerfreie Umsätze ohne VSt-Abzug) bzw. Kz 43 (Ausfuhren) umgestellt und werden nicht mehr verwendet.

View file

@ -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','86','41','43','48','89','93','66','61','62','67','83'];
if (!in_array($kz, $validKz, true)) {
View::redirect("/tax/ustva/{$year}/{$month}", 'Ungültige Kennzahl.', 'error');
return;

View file

@ -114,12 +114,12 @@ 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.
// Kz83 (Vorauszahlung/Überschuss) wird aus den Komponenten von ELSTER berechnet,
// muss aber dennoch mit übertragen werden.
$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', '86', '41', '43', '48', '89', '93'];
foreach ($betragsfelder as $nr) {
$val = (float)($kz[$nr]['betrag'] ?? 0.0);
if ($val != 0.0) {
@ -127,7 +127,7 @@ class ElsterXmlService
}
}
$steuerfelder = ['61', '62', '67', '66'];
$steuerfelder = ['66', '61', '62', '67'];
foreach ($steuerfelder as $nr) {
$val = (float)($kz[$nr]['steuer'] ?? 0.0);
if ($val != 0.0) {
@ -135,10 +135,10 @@ 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)));
// Kz83: Verbleibende Umsatzsteuer-Vorauszahlung (positiv) / Überschuss (negativ)
$kz83val = (float)($kz['83']['steuer'] ?? 0.0);
if ($kz83val != 0.0) {
$ustva->appendChild($dom->createElement('Kz83', $kzSteuer($kz83val)));
}
return $dom->saveXML();

View file

@ -9,76 +9,72 @@ use Steuerkram\Database;
/**
* Berechnet alle ELSTER-Kennzahlen (Kz) für die Umsatzsteuervoranmeldung.
*
* Zwei Quellen fließen in die Berechnung ein:
* Drei Quellen fließen in die Berechnung ein:
*
* 1. Kontobasiert (accounts.ustva_kz des Kategorie-Kontos):
* ustva_kz = '81' → Kz 81 (betrag_netto), Kz 83 (betrag_ust)
* ustva_kz = '86' → Kz 86 (betrag_netto), Kz 84 (betrag_ust)
* ustva_kz = '35' → Kz 35 (betrag_netto)
* ustva_kz = '81' → Kz 81 (betrag_netto); intern: ust19 (betrag_ust)
* ustva_kz = '86' → Kz 86 (betrag_netto); intern: ust7 (betrag_ust)
* ustva_kz = '41' → Kz 41 (betrag_netto) [steuerfreie ig. Lieferungen via Konto]
* ustva_kz = '44' → Kz 44 (betrag_netto) [steuerfreie Ausfuhren via Konto]
* (Kz 61/62 werden NICHT mehr über ustva_kz geroutet)
* ustva_kz = '43' → Kz 43 (betrag_netto) [steuerfreie Ausfuhren § 4 Nr. 1a]
* ustva_kz = '48' → Kz 48 (betrag_netto) [steuerfreie Umsätze ohne Vorsteuerabzug]
*
* 2. Buchungsbasiert (bookings.ust_satz, für Vorsteuer):
* buchungstyp = 'Ausgabe', ust_satz = 19 → Kz 61 (betrag_ust)
* buchungstyp = 'Ausgabe', ust_satz = 7 → Kz 62 (betrag_ust)
* Damit entfällt die Notwendigkeit separater VSt19/VSt7-Konten.
* 2. Buchungsbasiert (buchungstyp='Ausgabe', betrag_ust ≠ 0):
* Alle inländischen Eingangsrechnungen → Kz 66 (§§ 14, 14a UStG)
*
* 3. Steuerart-basiert (bookings.steuerart, unabhängig vom Konto):
* steuerart = 'ig_lieferung' → Kz 41 (betrag_netto)
* steuerart = 'export' → Kz 44 (betrag_netto)
* steuerart = 'ig_erwerb_19' → Kz 89 (betrag_netto), Kz 91 (netto × 0,19)
* steuerart = 'ig_erwerb_7' → Kz 93 (betrag_netto), Kz 94 (netto × 0,07)
* steuerart = 'export' → Kz 43 (betrag_netto)
* steuerart = 'ig_erwerb_19' → Kz 89 (betrag_netto)
* steuerart = 'ig_erwerb_7' → Kz 93 (betrag_netto)
* Buchungen mit diesen steuerart-Werten bleiben aus der kontobasierten Berechnung
* ausgeschlossen, um Doppelzählung zu vermeiden.
* Buchungen mit EU-Steuerarten fließen ebenfalls NICHT in Kz 61/62.
*
* Abgeleitete Kennzahlen:
* Kz 67 = Kz 91 + Kz 94 (Vorsteuer aus ig. Erwerb)
* Kz 66 = Kz 61 + Kz 62 + Kz 67 (Vorsteuer gesamt)
* Kz 69 = (Kz 83 + Kz 84 + Kz 91 + Kz 94) − Kz 66 (Zahllast)
* Kz 61 = Kz89 × 0,19 + Kz93 × 0,07 (Vorsteuer aus ig. Erwerben, § 3d UStG)
* Kz 62 = 0 (Einfuhrumsatzsteuer – in diesem System n.v.)
* Kz 67 = 0 (§ 13b-Vorsteuer – in diesem System n.v.)
* Kz 83 = ust19 + ust7 − Kz66 − Kz61 − Kz62 − Kz67
* = ust19 + ust7 − Kz66 (ig. Erwerb ist USt-neutral)
*/
class UstvaCalculator
{
/** Steuerart-Werte die AUSSCHLIESSLICH steuerart-basiert berechnet werden. */
private const EU_STEUERARTEN = ['ig_lieferung', 'export', 'ig_erwerb_19', 'ig_erwerb_7'];
/** Vorsteuer-Kennzahl je USt-Satz (buchungsbasiert). */
private const VST_SATZ_KZ = [19 => '61', 7 => '62'];
/**
* 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.
* Kz 61/62 sind hier nicht mehr enthalten (buchungsbasiert via VST_SATZ_KZ).
* Kontobasiertes Drill-down-Mapping.
* Kz 83 (Zahllast) enthält auch die Einnahmen-Konten 81/86.
*/
private const KZ_ACCOUNT_MAP = [
'81' => ['81'],
'83' => ['81'],
'86' => ['86'],
'84' => ['86'],
'35' => ['35'],
'41' => ['41'],
'44' => ['44'],
'43' => ['43'],
'48' => ['48'],
'83' => ['81', '86'],
];
/**
* Welche steuerart-Werte liefern Buchungen für eine Kennzahl.
* Steuerart-basiertes Drill-down-Mapping.
*/
private const KZ_STEUERART_MAP = [
'41' => ['ig_lieferung'],
'44' => ['export'],
'43' => ['export'],
'89' => ['ig_erwerb_19'],
'91' => ['ig_erwerb_19'],
'93' => ['ig_erwerb_7'],
'94' => ['ig_erwerb_7'],
'67' => ['ig_erwerb_19', 'ig_erwerb_7'],
'66' => ['ig_erwerb_19', 'ig_erwerb_7'],
'69' => ['ig_lieferung', 'export', 'ig_erwerb_19', 'ig_erwerb_7'],
'61' => ['ig_erwerb_19', 'ig_erwerb_7'],
'83' => ['ig_lieferung', 'export', 'ig_erwerb_19', 'ig_erwerb_7'],
];
/**
* Kennzahlen, deren Drill-down alle inländischen Ausgaben mit Vorsteuer enthält.
* (buchungsbasiert: buchungstyp='Ausgabe', betrag_ust != 0, keine EU-Steuerarten)
*/
private const KZ_INCLUDES_VST = ['66', '83'];
public function calculate(int $year, int $month): array
{
// ── 1. Kontobasierte Kennzahlen (EU-Steuerarten und Kz 61/62 ausgeschlossen) ──
// ── 1. Kontobasierte Kennzahlen (EU-Steuerarten ausgeschlossen) ──────────
$euPlaceholders = implode(',', array_fill(0, count(self::EU_STEUERARTEN), '?'));
$accountRows = Database::fetchAll(
'SELECT a.ustva_kz,
@ -89,7 +85,6 @@ class UstvaCalculator
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 a.ustva_kz NOT IN (\'61\', \'62\')
AND (bk.steuerart IS NULL OR bk.steuerart NOT IN (' . $euPlaceholders . '))
GROUP BY a.ustva_kz',
array_merge([$year, $month], self::EU_STEUERARTEN)
@ -103,26 +98,19 @@ class UstvaCalculator
];
}
// ── 2. Buchungsbasierte Vorsteuer Kz 61/62 (via ust_satz) ────────────
$vstRows = Database::fetchAll(
'SELECT bk.ust_satz, SUM(bk.betrag_ust) AS summe_ust
// ── 2. Buchungsbasierte Vorsteuer Kz 66 (alle inländischen Eingangsrechnungen) ──
$vstRow = Database::fetchOne(
'SELECT SUM(bk.betrag_ust) AS summe_ust
FROM bookings bk
JOIN fiscal_years fy ON bk.fiscal_year_id = fy.id
WHERE fy.year = ? AND MONTH(bk.buchungs_datum) = ?
AND bk.buchungstyp = \'Ausgabe\'
AND bk.betrag_ust != 0
AND (bk.steuerart IS NULL OR bk.steuerart NOT IN (' . $euPlaceholders . '))
GROUP BY bk.ust_satz',
AND (bk.steuerart IS NULL OR bk.steuerart NOT IN (' . $euPlaceholders . '))',
array_merge([$year, $month], self::EU_STEUERARTEN)
);
foreach ($vstRows as $row) {
$satz = (int)round((float)$row['ust_satz']);
$kzNr = self::VST_SATZ_KZ[$satz] ?? null;
if ($kzNr !== null) {
$byKz[$kzNr] = ['netto' => 0.0, 'ust' => (float)$row['summe_ust']];
}
}
$byKz['66'] = ['netto' => 0.0, 'ust' => (float)($vstRow['summe_ust'] ?? 0.0)];
// ── 3. Steuerart-basierte Kennzahlen ─────────────────────────────────
$stRows = Database::fetchAll(
@ -141,42 +129,41 @@ class UstvaCalculator
}
// ── 4. 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]['netto'] ?? 0.0);
$u = fn(string $kz): float => (float)($byKz[$kz]['ust'] ?? 0.0);
$st = fn(string $art): float => (float)($bySteuerart[$art] ?? 0.0);
$kz81 = $n('81'); $kz83 = $u('81');
$kz86 = $n('86'); $kz84 = $u('86');
$kz35 = $n('35');
$kz41 = $n('41') + $st('ig_lieferung');
$kz44 = $n('44') + $st('export');
$kz89 = $st('ig_erwerb_19');
$kz91 = round($kz89 * 0.19, 2);
$kz93 = $st('ig_erwerb_7');
$kz94 = round($kz93 * 0.07, 2);
$kz61 = $u('61');
$kz62 = $u('62');
$kz67 = $kz91 + $kz94;
$kz66 = $kz61 + $kz62 + $kz67;
$kz69 = ($kz83 + $kz84 + $kz91 + $kz94) - $kz66;
$kz81 = $n('81');
$ust19 = $u('81'); // interne USt aus 19 %-Umsätzen (kein eigenständiges ELSTER-Kz)
$kz86 = $n('86');
$ust7 = $u('86'); // interne USt aus 7 %-Umsätzen (kein eigenständiges ELSTER-Kz)
$kz41 = $n('41') + $st('ig_lieferung');
$kz43 = $n('43') + $st('export');
$kz48 = $n('48');
$kz89 = $st('ig_erwerb_19');
$kz93 = $st('ig_erwerb_7');
$kz66 = $u('66'); // § 14, 14a
$kz61 = round($kz89 * 0.19, 2) + round($kz93 * 0.07, 2); // § 3d Satz 1
$kz62 = 0.0; // Einfuhrumsatzsteuer n.v.
$kz67 = 0.0; // § 13b-Vorsteuer n.v.
// ig. Erwerb-USt gilt gleichzeitig als Output- und als Vorsteuer (kz61) → neutral
$igErwerbUst = round($kz89 * 0.19, 2) + round($kz93 * 0.07, 2); // = kz61
$kz83 = ($ust19 + $ust7 + $igErwerbUst) - ($kz66 + $kz61 + $kz62 + $kz67);
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)],
'93' => ['name' => 'Ig. Erwerb 7 % – Bemessungsgrundlage', 'betrag' => round($kz93, 2), 'steuer' => null],
'94' => ['name' => 'USt auf ig. Erwerb 7 %', 'betrag' => null, 'steuer' => round($kz94, 2)],
'61' => ['name' => 'Vorsteuer aus Eingangsrechnungen 19 %', 'betrag' => null, 'steuer' => round($kz61, 2)],
'62' => ['name' => 'Vorsteuer aus Eingangsrechnungen 7 %', 'betrag' => null, 'steuer' => round($kz62, 2)],
'67' => ['name' => 'Vorsteuer aus ig. Erwerb', 'betrag' => null, 'steuer' => round($kz67, 2)],
'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' => 'Steuerpfl. Umsätze 19 %', 'betrag' => round($kz81, 2), 'steuer' => null],
'86' => ['name' => 'Steuerpfl. Umsätze 7 %', 'betrag' => round($kz86, 2), 'steuer' => null],
'43' => ['name' => 'Steuerfreie Ausfuhren (§ 4 Nr. 1a)', 'betrag' => round($kz43, 2), 'steuer' => null],
'48' => ['name' => 'Steuerfreie Umsätze ohne Vorsteuerabzug', 'betrag' => round($kz48, 2), 'steuer' => null],
'41' => ['name' => 'Steuerfreie ig. Lieferungen (§ 4 Nr. 1b)', 'betrag' => round($kz41, 2), 'steuer' => null],
'89' => ['name' => 'Ig. Erwerb 19 % – Bemessungsgrundlage', 'betrag' => round($kz89, 2), 'steuer' => null],
'93' => ['name' => 'Ig. Erwerb 7 % – Bemessungsgrundlage', 'betrag' => round($kz93, 2), 'steuer' => null],
'66' => ['name' => 'Vorsteuer aus Lieferungen/Leistungen (§§ 14, 14a)', 'betrag' => null, 'steuer' => round($kz66, 2)],
'61' => ['name' => 'Vorsteuer aus ig. Erwerben (§ 3d UStG)', 'betrag' => null, 'steuer' => round($kz61, 2)],
'62' => ['name' => 'Vorsteuer aus Einfuhrumsatzsteuer', 'betrag' => null, 'steuer' => round($kz62, 2)],
'67' => ['name' => 'Vorsteuer aus Leistungen nach § 13b UStG', 'betrag' => null, 'steuer' => round($kz67, 2)],
'83' => ['name' => 'Verbleibende Umsatzsteuer-Vorauszahlung / Überschuss', 'betrag' => null, 'steuer' => round($kz83, 2)],
'_ust' => ['name' => 'Umsatzsteuer gesamt (intern)', 'betrag' => null, 'steuer' => round($ust19 + $ust7, 2)],
];
}
@ -186,13 +173,11 @@ class UstvaCalculator
*/
public function fetchBookingsForKz(int $year, int $month, string $kz): array
{
$accountKzList = self::KZ_ACCOUNT_MAP[$kz] ?? [];
$steuerartList = self::KZ_STEUERART_MAP[$kz] ?? [];
$accountKzList = self::KZ_ACCOUNT_MAP[$kz] ?? [];
$steuerartList = self::KZ_STEUERART_MAP[$kz] ?? [];
$includesVst = in_array($kz, self::KZ_INCLUDES_VST, true);
// Kz 61/62: buchungsbasiert via ust_satz
$vstSatz = array_search($kz, self::VST_SATZ_KZ, true);
if (empty($accountKzList) && empty($steuerartList) && $vstSatz === false) {
if (empty($accountKzList) && empty($steuerartList) && !$includesVst) {
return [];
}
@ -226,16 +211,14 @@ class UstvaCalculator
);
}
if ($vstSatz !== false) {
if ($includesVst) {
$euPh = implode(',', array_fill(0, count(self::EU_STEUERARTEN), '?'));
$parts[] = $select
. ' AND bk.buchungstyp = \'Ausgabe\''
. ' AND bk.betrag_ust != 0'
. ' AND ROUND(bk.ust_satz) = ?'
. ' AND (bk.steuerart IS NULL OR bk.steuerart NOT IN (' . $euPh . '))';
$params = array_merge($params,
[$year, $month],
[$vstSatz],
self::EU_STEUERARTEN
);
}

View file

@ -3,10 +3,10 @@ use Steuerkram\View;
$h = fn($v) => View::h($v);
$fmtKz = fn($v) => number_format((float)$v, 2, ',', '.') . ' €';
// Für Kz 69: vorzeichenbehafteter USt-Beitrag zur Zahllast.
// Für Kz 83: vorzeichenbehafteter USt-Beitrag zur Zahllast.
// Einnahmen mit USt = positiv, Ausgaben mit Vorsteuer = negativ,
// ig_erwerb heben sich gegenseitig auf → 0.
$ustBeitragKz69 = function(array $b): float {
$ustBeitragKz83 = function(array $b): float {
$steuerart = $b['steuerart'] ?? 'normal';
$ustSatz = (float)$b['ust_satz'];
$betragUst = (float)$b['betrag_ust'];
@ -16,7 +16,7 @@ $ustBeitragKz69 = function(array $b): float {
if ($b['buchungstyp'] === 'Ausgabe' && $steuerart === 'normal' && $betragUst != 0) {
return -$betragUst; // Vorsteuer → Vorzeichen umkehren
}
return 0.0; // steuerfreie Umsätze und ig_erwerb (KZ91-KZ94 = KZ67, Netto-Beitrag 0)
return 0.0; // steuerfreie Umsätze und ig_erwerb (Netto-Beitrag 0, da neutral)
};
// Ausgaben werden als negative Beträge angezeigt (Geld fließt ab).
@ -49,7 +49,7 @@ $signedBetrag = fn(array $b, string $field): float =>
<th>Kategorie</th>
<th>Zahlungskonto</th>
<th class="td-right">Netto</th>
<th class="td-right"><?= $kz === '69' ? 'USt-Beitrag' : 'USt' ?></th>
<th class="td-right"><?= $kz === '83' ? 'USt-Beitrag' : 'USt' ?></th>
<th class="td-right">Brutto</th>
<th>Anlage</th>
</tr>
@ -60,7 +60,7 @@ $signedBetrag = fn(array $b, string $field): float =>
$sumUst = 0.0;
$sumBrutto = 0.0;
foreach ($bookings as $b):
$ust = $kz === '69' ? $ustBeitragKz69($b) : $signedBetrag($b, 'betrag_ust');
$ust = $kz === '83' ? $ustBeitragKz83($b) : $signedBetrag($b, 'betrag_ust');
$netto = $signedBetrag($b, 'betrag_netto');
$brutto = $signedBetrag($b, 'betrag_brutto');
$sumNetto += $netto;

View file

@ -30,11 +30,10 @@ $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', '93', '94'],
'Vorsteuerbeträge' => ['61', '62', '67', '66'],
'Verbleibende Zahllast' => ['69'],
'Lieferungen und sonstige Leistungen' => ['81', '86', '43', '48', '41'],
'Innergemeinschaftlicher Erwerb' => ['89', '93'],
'Abziehbare Vorsteuerbeträge' => ['66', '61', '62', '67'],
'Verbleibende Zahllast' => ['83'],
];
foreach ($sections as $sectionTitle => $kzNrs):
?>
@ -46,7 +45,7 @@ $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 = in_array($nr, ['83']);
$isNegative = $isResult && ($field['steuer'] ?? 0) < 0;
?>
<tr style="<?= $isResult ? 'font-weight:700;background:#f0f4ff' : '' ?>">
@ -109,16 +108,16 @@ $fmtKz = fn($v) => $v !== null ? number_format((float)$v, 2, ',', '.') . ' €'
</tr>
<tr>
<td style="padding:.35rem 0;color:var(--color-muted)">USt gesamt</td>
<td class="td-right td-mono"><?= $fmtKz(($kz['83']['steuer'] ?? 0) + ($kz['84']['steuer'] ?? 0)) ?></td>
<td class="td-right td-mono"><?= $fmtKz($kz['_ust']['steuer'] ?? 0) ?></td>
</tr>
<tr>
<td style="padding:.35rem 0;color:var(--color-muted)">Vorsteuer</td>
<td class="td-right td-mono" style="color:var(--color-success)">– <?= $fmtKz($kz['66']['steuer'] ?? 0) ?></td>
</tr>
<tr style="border-top:2px solid #000;font-weight:700">
<td style="padding:.5rem 0"><?= ($kz['69']['steuer'] ?? 0) >= 0 ? 'Vorauszahlung' : 'Erstattung' ?></td>
<td class="td-right td-mono" style="color:<?= ($kz['69']['steuer'] ?? 0) >= 0 ? 'var(--color-danger)' : 'var(--color-success)' ?>">
<?= $fmtKz($kz['69']['steuer'] ?? 0) ?>
<td style="padding:.5rem 0"><?= ($kz['83']['steuer'] ?? 0) >= 0 ? 'Vorauszahlung' : 'Erstattung' ?></td>
<td class="td-right td-mono" style="color:<?= ($kz['83']['steuer'] ?? 0) >= 0 ? 'var(--color-danger)' : 'var(--color-success)' ?>">
<?= $fmtKz($kz['83']['steuer'] ?? 0) ?>
</td>
</tr>
</table>

View file

@ -111,30 +111,15 @@ 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: reguläre Inlandsvorsteuer (§§ 14, 14a) – 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 §§ 14, 14a): Steuerbetrag enthält Ausgabe ≥ 38 €'
);
// Kz 61: Vorsteuer 19% – muss mindestens 38 € enthalten
$this->assert(
(float)$kz['61']['steuer'] >= 38.00,
'UStVA Kz 61 (Vorsteuer 19%): Steuerbetrag enthält Ausgabe ≥ 38 €'
);
// 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 gesamt - Vorsteuer gesamt
$erwKz69 = round(
((float)$kz['83']['steuer'] + (float)$kz['84']['steuer']) - (float)$kz['66']['steuer'],
2
);
$this->assertEquals($erwKz69, (float)$kz['69']['steuer'], 'UStVA Kz 69 (Zahllast) korrekt berechnet');
// Kz 83: Zahllast = _ust - Kz66 (USt gesamt minus Vorsteuer)
$erwKz83 = round((float)$kz['_ust']['steuer'] - (float)$kz['66']['steuer'], 2);
$this->assertEquals($erwKz83, (float)$kz['83']['steuer'], 'UStVA Kz 83 (Zahllast) korrekt berechnet');
// ── EÜR prüfen ────────────────────────────────────────────────────
$euerCalc = new \Steuerkram\Services\EuerCalculator();

View file

@ -6,7 +6,7 @@ declare(strict_types=1);
* Test 7: UStVA-Kennzahlen für Auslandsumsätze und ig. Erwerb.
*
* Prüft, dass steuerart-Buchungen korrekt in die UStVA-Kennzahlen
* 41, 44, 89, 91, 93, 94 und 67 einfließen.
* 41, 43, 89, 93 und 61 einfließen.
*/
class UstvaAuslandTest extends TestCase
{
@ -36,6 +36,11 @@ class UstvaAuslandTest extends TestCase
);
$bankId = $bankId ? (int)$bankId['id'] : $zahlungId;
// Baseline Kz83 VOR den Test-Buchungen (reale Buchungen im laufenden Monat)
$calcPre = new \Steuerkram\Services\UstvaCalculator();
$kzPre = $calcPre->calculate($year, $month);
$kz83Pre = (float)$kzPre['83']['steuer'];
// ── 1. Steuerfreie ig. Lieferung → Kz 41 ──────────────────────────
$belegIgL = \Steuerkram\Models\Beleg::create([
'beleg_nr' => 'TEST-IGL-' . uniqid(),
@ -58,7 +63,7 @@ class UstvaAuslandTest extends TestCase
'buchungstext' => 'Test ig. Lieferung EU',
]);
// ── 2. Steuerfreie Ausfuhr → Kz 44 ───────────────────────────────
// ── 2. Steuerfreie Ausfuhr → Kz 43 ───────────────────────────────
$belegExp = \Steuerkram\Models\Beleg::create([
'beleg_nr' => 'TEST-EXP-' . uniqid(),
'filename' => 'test-exp.pdf',
@ -80,7 +85,7 @@ class UstvaAuslandTest extends TestCase
'buchungstext' => 'Test Ausfuhr Drittland',
]);
// ── 3. Ig. Erwerb 19 % → Kz 89/91/67 ─────────────────────────────
// ── 3. Ig. Erwerb 19 % → Kz 89 (Bemessungsgrundlage), Kz 61 (Vorsteuer) ──
$belegIgE19 = \Steuerkram\Models\Beleg::create([
'beleg_nr' => 'TEST-IGE19-' . uniqid(),
'filename' => 'test-ige19.pdf',
@ -102,7 +107,7 @@ class UstvaAuslandTest extends TestCase
'buchungstext' => 'Test ig. Erwerb 19%',
]);
// ── 4. Ig. Erwerb 7 % → Kz 93/94/67 ──────────────────────────────
// ── 4. Ig. Erwerb 7 % → Kz 93 (Bemessungsgrundlage), Kz 61 (Vorsteuer) ───
$belegIgE7 = \Steuerkram\Models\Beleg::create([
'beleg_nr' => 'TEST-IGE7-' . uniqid(),
'filename' => 'test-ige7.pdf',
@ -134,10 +139,10 @@ class UstvaAuslandTest extends TestCase
'Kz 41 (ig. Lieferung): Betrag ≥ 1.000 €'
);
// Kz 44: Ausfuhr 2000 €
// Kz 43: Ausfuhr 2000 €
$this->assert(
(float)$kz['44']['betrag'] >= 2000.00,
'Kz 44 (Ausfuhr): Betrag ≥ 2.000 €'
(float)$kz['43']['betrag'] >= 2000.00,
'Kz 43 (Ausfuhr): Betrag ≥ 2.000 €'
);
// Kz 89: ig. Erwerb 19% Bemessungsgrundlage 500 €
@ -146,52 +151,33 @@ class UstvaAuslandTest extends TestCase
'Kz 89 (ig. Erwerb 19% Bmgl.): Betrag ≥ 500 €'
);
// Kz 91: USt auf ig. Erwerb 19% = 500 * 0.19 = 95 €
$this->assertEquals(
round(500.00 * 0.19, 2),
(float)$kz['91']['steuer'],
'Kz 91 (USt ig. Erwerb 19%): Steuer = 95,00 €'
);
// Kz 93: ig. Erwerb 7% Bemessungsgrundlage 300 €
$this->assert(
(float)$kz['93']['betrag'] >= 300.00,
'Kz 93 (ig. Erwerb 7% Bmgl.): Betrag ≥ 300 €'
);
// Kz 94: USt auf ig. Erwerb 7% = 300 * 0.07 = 21 €
// Kz 61: Vorsteuer aus ig. Erwerben (§ 3d) = 500*0.19 + 300*0.07 = 95 + 21 = 116 €
$erwartetKz61 = round(500.00 * 0.19, 2) + round(300.00 * 0.07, 2);
$this->assertEquals(
round(300.00 * 0.07, 2),
(float)$kz['94']['steuer'],
'Kz 94 (USt ig. Erwerb 7%): Steuer = 21,00 €'
$erwartetKz61,
(float)$kz['61']['steuer'],
'Kz 61 (Vorsteuer ig. Erwerb § 3d): Steuer = 116,00 €'
);
// Kz 67: Vorsteuer aus ig. Erwerb = Kz 91 + Kz 94 = 116 €
$erwartetKz67 = round((float)$kz['91']['steuer'] + (float)$kz['94']['steuer'], 2);
// Kz 83 (Zahllast): ig. Erwerb/Lieferung/Ausfuhr sind USt-neutral (betrag_ust=0)
// → Zahllast darf sich gegenüber Baseline nicht verändert haben
$this->assertEquals(
$erwartetKz67,
(float)$kz['67']['steuer'],
'Kz 67 (Vorsteuer ig. Erwerb) = Kz 91 + Kz 94'
round($kz83Pre, 2),
(float)$kz['83']['steuer'],
'Kz 83 (Zahllast) unverändert – alle EU-Buchungen sind USt-neutral'
);
// Kz 69 Plausibilitätsprüfung: ig. Erwerb neutralisiert sich durch Kz 67
// USt auf Umsätze (83+84) + ig. Erwerb (91+94) - Vorsteuer (66) = 69
$ust = (float)$kz['83']['steuer'] + (float)$kz['84']['steuer']
+ (float)$kz['91']['steuer'] + (float)$kz['94']['steuer'];
$erwartetKz69 = round($ust - (float)$kz['66']['steuer'], 2);
$this->assertEquals(
$erwartetKz69,
(float)$kz['69']['steuer'],
'Kz 69 (Zahllast) korrekt berechnet'
);
// ig. Erwerb darf NICHT in Kz 35 oder Kz 61/62 landen
// (kz35 enthält nur normale 0%-Einnahmen, kz61 nur normale Vorsteuer)
// Wir buchten keine normalen 0%-Einnahmen, also muss Kz 35 = 0 sein
// ig. Erwerb darf NICHT in Kz 48 landen (steuerfreie Umsätze ohne VSt-Abzug)
$this->assertEquals(
0.0,
(float)$kz['35']['betrag'],
'Kz 35 (steuerfreie Inlandsumsätze) = 0 (kein normaler 0%-Umsatz gebucht)'
(float)$kz['48']['betrag'],
'Kz 48 (steuerfreie Inlandsumsätze) = 0 (kein steuerfreier Inlandsumsatz gebucht)'
);
}

View file

@ -6,29 +6,33 @@ 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 Vorauszahlung/Überschuss';
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 enthält korrekten Betrag bei Zahllast');
// Fall 2: Erstattung (negativ) → Kz39 darf nicht vorkommen
$kzErstattung = ['69' => ['steuer' => -50.00]];
// Fall 2: Erstattung (negativ) → Kz83 muss vorhanden sein (Überschuss)
$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 enthält negativen Betrag 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
// Fall 4: Kz39 (Sondervorauszahlung) darf NICHT mehr als Zahllast exportiert werden
$this->assert(!str_contains($xml, '<Kz39>'), 'Kz39 (Sondervorauszahlung) wird nicht exportiert');
// Fall 5: XML ist valides XML
$doc = new \DOMDocument();
$loaded = @$doc->loadXML($xml);
$this->assert($loaded !== false, 'Generiertes XML ist wohlgeformt');

View file

@ -93,12 +93,12 @@ class StornoBuchungTest extends TestCase
);
// ── UStVA: Vorsteuer-KZ hebt sich auf ─────────────────────────
// Kz 61 enthält Vorsteuer aus dem Ausgabe-Konto (ustva_kz='61').
// Kz 66 enthält alle inländischen Ausgaben mit Vorsteuer (buchungsbasiert).
// Original: +19, Storno: -19 → Summe = 0 (oder weniger als andere Tests einbringen).
// Da weitere Tests parallel laufen können, prüfen wir nur, dass die Storno-Buchung
// als eigenständige Zeile in fetchBookingsForKz erscheint.
$ustvaCalc = new \Steuerkram\Services\UstvaCalculator();
$bookings = $ustvaCalc->fetchBookingsForKz($year, $month, '61');
$bookings = $ustvaCalc->fetchBookingsForKz($year, $month, '66');
$reversalFound = false;
foreach ($bookings as $b) {
@ -109,6 +109,6 @@ class StornoBuchungTest extends TestCase
break;
}
}
$this->assert($reversalFound, 'UStVA-Drilldown Kz 61: Stornobuchung erscheint in Detailansicht');
$this->assert($reversalFound, 'UStVA-Drilldown Kz 66: Stornobuchung erscheint in Detailansicht');
}
}