- Anlage-Spalte in allen Buchungstabellen verlinkt jetzt auf /assets/{id}/edit
- bk.asset_id in UstvaCalculator und EuerCalculator-Queries ergänzt
- Fallback-Anzeige: Bezeichnung wenn anlage_nr leer ist
- Migration 032: anlage_nr für bestehende Anlagegüter nachgenerieren
- README: Feature Anlagevermögen dokumentiert
253 lines
12 KiB
PHP
253 lines
12 KiB
PHP
<?php
|
||
|
||
declare(strict_types=1);
|
||
|
||
namespace Steuerkram\Services;
|
||
|
||
use Steuerkram\Database;
|
||
|
||
/**
|
||
* Berechnet alle ELSTER-Kennzahlen (Kz) für die Umsatzsteuervoranmeldung.
|
||
*
|
||
* Zwei 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 = '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)
|
||
*
|
||
* 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)
|
||
* Kz 66 = Kz 61 + Kz 62 + Kz 67 (Vorsteuer gesamt)
|
||
* Kz 69 = (Kz 83 + Kz 84 + Kz 91 + Kz 94) − Kz 66 (Zahllast)
|
||
*/
|
||
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'],
|
||
'83' => ['81'],
|
||
'86' => ['86'],
|
||
'84' => ['86'],
|
||
'35' => ['35'],
|
||
'41' => ['41'],
|
||
'44' => ['44'],
|
||
];
|
||
|
||
/**
|
||
* Welche steuerart-Werte liefern Buchungen für eine Kennzahl.
|
||
*/
|
||
private const KZ_STEUERART_MAP = [
|
||
'41' => ['ig_lieferung'],
|
||
'44' => ['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'],
|
||
];
|
||
|
||
public function calculate(int $year, int $month): array
|
||
{
|
||
// ── 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,
|
||
SUM(bk.betrag_netto) AS summe_netto,
|
||
SUM(bk.betrag_ust) AS summe_ust
|
||
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 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)
|
||
);
|
||
|
||
$byKz = [];
|
||
foreach ($accountRows as $row) {
|
||
$byKz[$row['ustva_kz']] = [
|
||
'netto' => (float)$row['summe_netto'],
|
||
'ust' => (float)$row['summe_ust'],
|
||
];
|
||
}
|
||
|
||
// ── 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
|
||
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)
|
||
);
|
||
|
||
$bySteuerart = [];
|
||
foreach ($stRows as $row) {
|
||
$bySteuerart[$row['steuerart']] = (float)$row['summe_netto'];
|
||
}
|
||
|
||
// ── 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);
|
||
|
||
$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;
|
||
|
||
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)],
|
||
];
|
||
}
|
||
|
||
/**
|
||
* Liefert alle Buchungszeilen, die zu einer Kennzahl beitragen (Drill-down).
|
||
* 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] ?? [];
|
||
|
||
// Kz 61/62: buchungsbasiert via ust_satz
|
||
$vstSatz = array_search($kz, self::VST_SATZ_KZ, true);
|
||
|
||
if (empty($accountKzList) && empty($steuerartList) && $vstSatz === false) {
|
||
return [];
|
||
}
|
||
|
||
$select = 'SELECT bk.id, bk.buchungs_datum, bk.buchungstyp, bk.steuerart, bk.ust_satz,
|
||
bk.betrag_netto, bk.betrag_ust, bk.betrag_brutto, bk.buchungstext,
|
||
bk.is_reversal, bk.beleg_id, bk.asset_id, bl.beleg_nr,
|
||
k.kontonummer AS kategorie_nr, k.bezeichnung AS kategorie_name,
|
||
z.kontonummer AS zahlungs_nr, z.bezeichnung AS zahlungs_name,
|
||
a.anlage_nr, a.bezeichnung AS anlage_bezeichnung
|
||
FROM bookings bk
|
||
JOIN fiscal_years fy ON bk.fiscal_year_id = fy.id
|
||
LEFT JOIN belege bl ON bk.beleg_id = bl.id
|
||
JOIN accounts k ON bk.kategorie_konto_id = k.id
|
||
JOIN accounts z ON bk.zahlungskonto_id = z.id
|
||
LEFT JOIN assets a ON bk.asset_id = a.id
|
||
WHERE fy.year = ? AND MONTH(bk.buchungs_datum) = ?';
|
||
|
||
$parts = [];
|
||
$params = [];
|
||
|
||
if (!empty($accountKzList)) {
|
||
$ph = implode(',', array_fill(0, count($accountKzList), '?'));
|
||
$euPh = implode(',', array_fill(0, count(self::EU_STEUERARTEN), '?'));
|
||
$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
|
||
);
|
||
}
|
||
|
||
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 . ')';
|
||
$params = array_merge($params, [$year, $month], $steuerartList);
|
||
}
|
||
|
||
$sql = implode(' UNION ALL ', $parts) . ' ORDER BY buchungs_datum, id';
|
||
|
||
return Database::fetchAll($sql, $params);
|
||
}
|
||
}
|