Fix: Vorsteuer Kz 61/62 buchungsbasiert statt kontobasiert ermitteln

This commit is contained in:
fds2610-bot 2026-04-11 17:14:00 +02:00
parent 7bd2d51181
commit 4653c082be
3 changed files with 78 additions and 19 deletions

View file

@ -41,11 +41,13 @@ Die EÜR basiert auf dem Feld `accounts.kontenart` sowie `accounts.euer_zeile` d
**Datei:** `src/Services/UstvaCalculator.php`
Die UStVA kombiniert zwei Quellen (Migration 022):
Die UStVA kombiniert drei Quellen:
**Quelle 1 – kontobasiert** (`accounts.ustva_kz`): reguläre Umsätze und Vorsteuer. Buchungen mit EU-Steuerarten (s.u.) werden hier ausgeschlossen.
**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 2 – steuerartbasiert** (`bookings.steuerart`): EU-Sonderfälle (ig. Lieferung, Ausfuhr, ig. Erwerb). Diese Buchungen landen unabhängig vom Kategorie-Konto in den richtigen Kennzahlen.
**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 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
@ -58,8 +60,8 @@ Die UStVA kombiniert zwei Quellen (Migration 022):
| **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 `ustva_kz='61'` | alle Betriebsausgabe-Konten mit `steuercode='VSt19'` (Migration 022: früher kz=66) |
| **62** | Vorsteuer 7 % | `SUM(betrag_ust)` WHERE `ustva_kz='62'` | alle Betriebsausgabe-Konten mit `steuercode='VSt7'` (Migration 022: früher kz=66) |
| **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 |
| **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 |
@ -73,10 +75,12 @@ Die UStVA kombiniert zwei Quellen (Migration 022):
| **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 |
### Migration 022 – Vorsteuer-Kz-Aufteilung
### Migration 022 – Vorsteuer-Kz-Aufteilung (historisch)
Vor Migration 022 trugen alle Vorsteuer-Konten `ustva_kz='66'`, was dazu führte, dass Kz 61, 62 und 66 stets 0 ergaben. Migration 022 setzt:
- `steuercode='VSt19'` → `ustva_kz='61'`
- `steuercode='VSt7'` → `ustva_kz='62'`
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.

View file

@ -2,6 +2,15 @@
---
## 2026-04-11 – Buchungsbasierte Vorsteuer Kz 61/62 in UStVA
### Geändert
- `UstvaCalculator`: Kz 61 und 62 werden nicht mehr über `accounts.ustva_kz` geroutet, sondern direkt aus `bookings.ust_satz` ermittelt (`buchungstyp='Ausgabe'`, `betrag_ust != 0`). Damit reicht ein einziges Aufwandskonto für beide USt-Sätze – separate VSt19-/VSt7-Konten sind nicht mehr erforderlich
- `fetchBookingsForKz`: Drill-down für Kz 61/62 nutzt jetzt ebenfalls buchungsbasierte Filterung
- `BERICHTE.md`: Routing-Logik für Kz 61/62 dokumentiert
---
## 2026-04-10 – Nextcloud-Belegimport via WebDAV
### Neu

View file

@ -17,16 +17,21 @@ use Steuerkram\Database;
* 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_ust) [Vorsteuer 19 %, VSt19-Konten]
* ustva_kz = '62' → Kz 62 (betrag_ust) [Vorsteuer 7 %, VSt7-Konten]
* (Kz 61/62 werden NICHT mehr über ustva_kz geroutet)
*
* 2. Steuerart-basiert (bookings.steuerart, unabhängig vom Konto):
* 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.
*
* 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)
* 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)
@ -38,9 +43,13 @@ 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).
*/
private const KZ_ACCOUNT_MAP = [
'81' => ['81'],
@ -50,8 +59,6 @@ class UstvaCalculator
'35' => ['35'],
'41' => ['41'],
'44' => ['44'],
'61' => ['61'],
'62' => ['62'],
];
/**
@ -71,7 +78,7 @@ class UstvaCalculator
public function calculate(int $year, int $month): array
{
// ── 1. Kontobasierte Kennzahlen (EU-Steuerarten ausgeschlossen) ──────
// ── 1. Kontobasierte Kennzahlen (EU-Steuerarten und Kz 61/62 ausgeschlossen) ──
$euPlaceholders = implode(',', array_fill(0, count(self::EU_STEUERARTEN), '?'));
$accountRows = Database::fetchAll(
'SELECT a.ustva_kz,
@ -82,6 +89,7 @@ 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)
@ -95,7 +103,28 @@ class UstvaCalculator
];
}
// ── 2. Steuerart-basierte Kennzahlen ─────────────────────────────────
// ── 2. Buchungsbasierte Vorsteuer Kz 61/62 (via ust_satz) ────────────
$vstRows = Database::fetchAll(
'SELECT bk.ust_satz, 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',
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']];
}
}
// ── 3. Steuerart-basierte Kennzahlen ─────────────────────────────────
$stRows = Database::fetchAll(
'SELECT bk.steuerart, SUM(bk.betrag_netto) AS summe_netto
FROM bookings bk
@ -111,7 +140,7 @@ class UstvaCalculator
$bySteuerart[$row['steuerart']] = (float)$row['summe_netto'];
}
// ── 3. Kennzahlen zusammensetzen ─────────────────────────────────────
// ── 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);
@ -153,14 +182,17 @@ class UstvaCalculator
/**
* Liefert alle Buchungszeilen, die zu einer Kennzahl beitragen (Drill-down).
* Kombiniert kontobasierte und steuerart-basierte Quellen per UNION.
* Kombiniert kontobasierte, buchungsbasierte und steuerart-basierte Quellen per UNION.
*/
public function fetchBookingsForKz(int $year, int $month, string $kz): array
{
$accountKzList = self::KZ_ACCOUNT_MAP[$kz] ?? [];
$steuerartList = self::KZ_STEUERART_MAP[$kz] ?? [];
if (empty($accountKzList) && empty($steuerartList)) {
// Kz 61/62: buchungsbasiert via ust_satz
$vstSatz = array_search($kz, self::VST_SATZ_KZ, true);
if (empty($accountKzList) && empty($steuerartList) && $vstSatz === false) {
return [];
}
@ -192,6 +224,20 @@ class UstvaCalculator
);
}
if ($vstSatz !== false) {
$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
);
}
if (!empty($steuerartList)) {
$ph = implode(',', array_fill(0, count($steuerartList), '?'));
$parts[] = $select . ' AND bk.steuerart IN (' . $ph . ')';