145 lines
6.5 KiB
PHP
145 lines
6.5 KiB
PHP
<?php
|
||
|
||
declare(strict_types=1);
|
||
|
||
namespace Steuerkram\Services;
|
||
|
||
use Steuerkram\Database;
|
||
|
||
/**
|
||
* Berechnet alle Kennzahlen (Kz) für die Umsatzsteuervoranmeldung.
|
||
*
|
||
* 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)
|
||
*
|
||
* Steuerart-basiert:
|
||
* steuerart = 'ig_erwerb_19' → Kz 89 (betrag_netto, Bemessungsgrundlage)
|
||
*
|
||
* Abgeleitete Kennzahlen:
|
||
* 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
|
||
{
|
||
private const EU_STEUERARTEN = ['ig_lieferung', 'export', 'ig_erwerb_19', 'ig_erwerb_7'];
|
||
|
||
private const KZ_ACCOUNT_MAP = [
|
||
'81' => ['81'],
|
||
'66' => ['66'],
|
||
'61' => ['61'],
|
||
'83' => ['83'],
|
||
];
|
||
|
||
private const KZ_STEUERART_MAP = [
|
||
'89' => ['ig_erwerb_19'],
|
||
];
|
||
|
||
public function calculate(int $year, int $month): array
|
||
{
|
||
// ── 1. Kontobasierte Kennzahlen (EU-Steuerarten ausgeschlossen) ─────────
|
||
$euPh = implode(',', array_fill(0, count(self::EU_STEUERARTEN), '?'));
|
||
$accountRows = Database::fetchAll(
|
||
'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 (' . $euPh . '))
|
||
GROUP BY a.ustva_kz',
|
||
array_merge([$year, $month], self::EU_STEUERARTEN)
|
||
);
|
||
|
||
$byKz = [];
|
||
foreach ($accountRows as $row) {
|
||
$byKz[$row['ustva_kz']] = (float)$row['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 = ?',
|
||
[$year, $month, 'ig_erwerb_19']
|
||
);
|
||
$kz89 = (float)($stRow['summe_netto'] ?? 0.0);
|
||
|
||
// ── 3. Kennzahlen zusammensetzen ─────────────────────────────────────
|
||
$n = fn(string $kz): float => (float)($byKz[$kz] ?? 0.0);
|
||
|
||
$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' => '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).
|
||
*/
|
||
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)) {
|
||
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 (!empty($steuerartList)) {
|
||
$ph = implode(',', array_fill(0, count($steuerartList), '?'));
|
||
$parts[] = $select . ' AND bk.steuerart IN (' . $ph . ')';
|
||
$params = array_merge($params, [$year, $month], $steuerartList);
|
||
}
|
||
|
||
return Database::fetchAll(
|
||
implode(' UNION ALL ', $parts) . ' ORDER BY buchungs_datum, id',
|
||
$params
|
||
);
|
||
}
|
||
}
|