UStVA umgebaut auf Konten anstelle von Belegart

This commit is contained in:
fds2610 2026-03-29 13:10:38 +02:00
parent fcd4d227a1
commit c9f5dd02f8
13 changed files with 609 additions and 315 deletions

View file

@ -42,30 +42,50 @@ Alle passenden Buchungen werden nach Kategorie-Konto gruppiert und als Betriebsa
**Datei:** `src/Services/UstvaCalculator.php`
Die UStVA verwendet **keine** Kontonummern, sondern die Buchungsfelder `buchungstyp`, `steuerart`, `ust_satz` und `betrag_ust`. Stornobuchungen (`is_reversal = 1`) werden ausgeschlossen.
Die UStVA basiert ausschließlich auf dem Feld `accounts.ustva_kz` des Kategorie-Kontos einer Buchung. Die Felder `buchungstyp` und `steuerart` werden nicht ausgewertet. Stornobuchungen (`is_reversal = 1`) werden ausgeschlossen.
### Direkt befüllte Kennzahlen
### Berechnungslogik
| Kz | Bezeichnung | Bedingung |
|---|---|---|
| **81** | Steuerpfl. Umsätze 19 % – Netto | `buchungstyp='Einnahme'`, `steuerart='normal'`, `ust_satz=19` |
| **86** | Steuerpfl. Umsätze 7 % – Netto | `buchungstyp='Einnahme'`, `steuerart='normal'`, `ust_satz=7` |
| **35** | Steuerfreie Umsätze (ohne Vorsteuerabzug) – Netto | `buchungstyp='Einnahme'`, `steuerart='normal'`, `ust_satz=0` |
| **41** | Steuerfreie ig. Lieferungen (§ 4 Nr. 1b) – Netto | `buchungstyp='Einnahme'`, `steuerart='ig_lieferung'` |
| **44** | Steuerfreie Ausfuhren (§ 4 Nr. 1a) – Netto | `buchungstyp='Einnahme'`, `steuerart='export'` |
| **89** | Ig. Erwerb 19 % – Bemessungsgrundlage | `buchungstyp='Ausgabe'`, `steuerart='ig_erwerb_19'` |
| **93** | Ig. Erwerb 7 % – Bemessungsgrundlage | `buchungstyp='Ausgabe'`, `steuerart='ig_erwerb_7'` |
| **61** | Vorsteuer aus Eingangsrechnungen 19 % | `buchungstyp='Ausgabe'`, `steuerart='normal'`, `ust_satz=19`, `betrag_ust>0` |
| **62** | Vorsteuer aus Eingangsrechnungen 7 % | `buchungstyp='Ausgabe'`, `steuerart='normal'`, `ust_satz=7`, `betrag_ust>0` |
Pro Monat wird eine einzige Aggregationsabfrage ausgeführt, die `betrag_netto` und `betrag_ust` nach `ustva_kz` des Kategorie-Kontos gruppiert:
### Abgeleitete Kennzahlen (berechnet, keine direkte Buchungszuordnung)
```sql
SELECT a.ustva_kz, SUM(betrag_netto), SUM(betrag_ust)
FROM bookings JOIN accounts a ON a.id = kategorie_konto_id
WHERE ... AND a.ustva_kz IS NOT NULL
GROUP BY a.ustva_kz
```
### Kontozuordnung und Kennzahlen
| Kz | Bezeichnung | Quelle | Relevante Konten (Beispiele) |
|---|---|---|---|
| **81** | Steuerpfl. Umsätze 19 % | `SUM(betrag_netto)` WHERE `ustva_kz='81'` | 8000, 8400, 8510, 8570, 8800, 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 |
| **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, 8160 |
| **41** | Steuerfreie ig. Lieferungen | `SUM(betrag_netto)` WHERE `ustva_kz='41'` | 8120, 8125, 8130 |
| **44** | Steuerfreie Ausfuhren | `SUM(betrag_netto)` WHERE `ustva_kz='44'` | 8150 |
| **61** | Vorsteuer 19 % | `SUM(betrag_ust)` WHERE `ustva_kz='61'` | alle Betriebsausgabe-Konten mit `steuercode='VSt19'` (z.B. 3100, 4200ff, 4900ff) |
| **62** | Vorsteuer 7 % | `SUM(betrag_ust)` WHERE `ustva_kz='62'` | alle Betriebsausgabe-Konten mit `steuercode='VSt7'` (z.B. 3110, 4610, 4680) |
| **89** | Ig. Erwerb 19 % – Bemessungsgrundlage | `SUM(betrag_netto)` WHERE `ustva_kz='89'` | 3425 |
| **91** | USt auf ig. Erwerb 19 % | `SUM(betrag_ust)` WHERE `ustva_kz='89'` | 3425 |
| **93** | Ig. Erwerb 7 % – Bemessungsgrundlage | `SUM(betrag_netto)` WHERE `ustva_kz='93'` | 3420 |
| **94** | USt auf ig. Erwerb 7 % | `SUM(betrag_ust)` WHERE `ustva_kz='93'` | 3420 |
### Abgeleitete Kennzahlen
| Kz | Bezeichnung | Berechnung |
|---|---|---|
| **83** | USt auf steuerpfl. Umsätze 19 % | Kz 81 × 19 % |
| **84** | USt auf steuerpfl. Umsätze 7 % | Kz 86 × 7 % |
| **91** | USt auf ig. Erwerb 19 % | Kz 89 × 19 % |
| **94** | USt auf ig. Erwerb 7 % | Kz 93 × 7 % |
| **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 |
### Neue Konten in Klasse 3 (Migration 010)
| Konto | Bezeichnung | ustva_kz |
|---|---|---|
| 3420 | Innergemeinschaftliche Erwerbe 7 % Vorsteuer | 93 |
| 3425 | Innergemeinschaftliche Erwerbe 19 % Vorsteuer | 89 |
Buchungen auf ig. Erwerb müssen auf Konto 3420 bzw. 3425 als Kategorie-Konto erfolgen.

View file

@ -2,6 +2,46 @@
---
## 2026-03-29 – UStVA: kontonummernbasierte Kennzahlenberechnung
### Geändert
- `UstvaCalculator::calculate()` wertet ausschließlich `accounts.ustva_kz` des Kategorie-Kontos aus – `buchungstyp` und `steuerart` werden nicht mehr benötigt
- `UstvaCalculator::fetchBookingsForKz()` filtert über `k.ustva_kz IN (...)` statt über Buchungsfelder
- Kz 83 und 84 kommen jetzt direkt aus `betrag_ust` der Erlöskonten (statt `Kz81 × 19 %`)
- Kz 91 und 94 kommen direkt aus `betrag_ust` der ig.-Erwerb-Konten (statt abgeleitet)
### Betrifft
- `src/Services/UstvaCalculator.php`
- Migration `010_ustva_kz_konto_mapping.sql`
- `BERICHTE.md` (UStVA-Kapitel vollständig neu)
### Migration 010 – Kontenanpassungen
- Alle Betriebsausgabe-Konten mit `steuercode='VSt19'`: `ustva_kz` von `'66'` auf `'61'`
- Alle Betriebsausgabe-Konten mit `steuercode='VSt7'`: `ustva_kz` von `'66'` auf `'62'`
- Neue Konten: 3425 „Innergemeinschaftliche Erwerbe 19 %" (`ustva_kz='89'`), 3420 „Innergemeinschaftliche Erwerbe 7 %" (`ustva_kz='93'`)
---
## 2026-03-28 – 30% Eigenanteil-Kennzeichnung und Korrekturbuchungen
### Hinzugefügt
- Checkbox „30% Eigenanteil buchen" in der Buchungszeilen-Erfassung (zwischen Nettobetrag und Submit-Button)
- Neues Feld `eigenanteil_30 TINYINT(1)` in der `belege`-Tabelle (Migration `009_beleg_eigenanteil_30.sql`)
- Der Checkbox-Wert wird beim Speichern einer Buchung in die Beleg-Metadaten übernommen
- Bei gesetzter Checkbox werden automatisch zwei Korrekturbuchungen angelegt oder aktualisiert:
- 30% des Nettobetrags an Konto 8922 (nicht abzugsfähiger Eigenanteil), Zahlungskonto 1800
- 30% der errechneten USt an Konto 1776 (Vorsteuerkorrektur), Zahlungskonto 1800 – nur wenn USt-Satz > 0
- Im Testmodus: bestehende Korrekturbuchungen werden direkt überschrieben
- Im Produktivmodus: bestehende Korrekturbuchungen werden GoBD-konform storniert und neu angelegt
### Betrifft
- `src/Views/belege/detail.php`
- `src/Controllers/BookingController.php`
- `src/Models/Booking.php` (neue Methoden `upsertEigenanteil30`, `upsertKorrekturbuchung`)
- Migration: `009_beleg_eigenanteil_30.sql`
---
## 2026-03-27 – DATEV-konformer SKR03 Kontenrahmen (#11)
### Geändert

View file

@ -0,0 +1,5 @@
-- Migration 009: Eigenanteil-30%-Kennzeichnung in belege-Tabelle
ALTER TABLE belege
ADD COLUMN eigenanteil_30 TINYINT(1) NOT NULL DEFAULT 0
COMMENT '1 = 30% Eigenanteil wurde beim Buchen markiert'
AFTER betrag_brutto;

View file

@ -0,0 +1,25 @@
-- Migration 010: UStVA-Kennzahlen direkt aus Kontozuordnung
--
-- 1. Betriebsausgabe-Konten: ustva_kz '66' → '61' (VSt19) bzw. '62' (VSt7)
-- Damit werden Kz 61 und 62 direkt aus betrag_ust der jeweiligen Buchungen summiert.
UPDATE accounts
SET ustva_kz = '61'
WHERE kontenart = 'Betriebsausgabe'
AND steuercode = 'VSt19';
UPDATE accounts
SET ustva_kz = '62'
WHERE kontenart = 'Betriebsausgabe'
AND steuercode = 'VSt7';
-- 2. Konten für Innergemeinschaftlichen Erwerb anlegen (Kz 89 / 93)
-- 3425: ig. Erwerb 19 % → Kz 89 (Bemessungsgrundlage); betrag_ust → Kz 91
-- 3420: ig. Erwerb 7 % → Kz 93 (Bemessungsgrundlage); betrag_ust → Kz 94
INSERT INTO accounts (kontonummer, bezeichnung, kontenart, steuercode, ustva_kz, aktiv, is_system)
VALUES
('3425', 'Innergemeinschaftliche Erwerbe 19 % Vorsteuer', 'Betriebsausgabe', 'VSt19', '89', 1, 1),
('3420', 'Innergemeinschaftliche Erwerbe 7 % Vorsteuer', 'Betriebsausgabe', 'VSt7', '93', 1, 1)
ON DUPLICATE KEY UPDATE
bezeichnung = VALUES(bezeichnung),
steuercode = VALUES(steuercode),
ustva_kz = VALUES(ustva_kz);

View file

@ -98,6 +98,7 @@ $router->get('/tax/ustva', fn($p) => (new UstvaController())->index($p));
$router->get('/tax/ustva/{year}/{month}', fn($p) => (new UstvaController())->show($p));
$router->post('/tax/ustva/{year}/{month}/save', fn($p) => (new UstvaController())->save($p));
$router->get('/tax/ustva/{year}/{month}/xml', fn($p) => (new UstvaController())->downloadXml($p));
$router->get('/tax/ustva/{year}/{month}/kz/{kz}', fn($p) => (new UstvaController())->showKzBookings($p));
// ── EÜR ─────────────────────────────────────────────────────
$router->get('/tax/euer', fn($p) => (new EuerController())->index($p));

View file

@ -110,6 +110,22 @@ class BookingController
try {
$data['beleg_id'] = $belegId;
Booking::create($data);
$eigenanteil = isset($data['eigenanteil_30']) ? 1 : 0;
\Steuerkram\Database::execute(
'UPDATE belege SET eigenanteil_30 = ?, updated_at = NOW() WHERE id = ?',
[$eigenanteil, $belegId]
);
if ($eigenanteil === 1) {
Booking::upsertEigenanteil30(
$belegId,
(float)$data['betrag_netto'],
(float)($data['ust_satz'] ?? 0),
$data['buchungs_datum']
);
}
View::redirect("/belege/{$belegId}", 'Buchung erfasst.');
} catch (\PDOException $e) {
error_log('[Steuerkram] PDO-Fehler in BookingController::store(): ' . $e->getMessage());

View file

@ -98,6 +98,38 @@ class UstvaController
View::redirect("/tax/ustva/{$year}/{$month}", 'UStVA gespeichert.');
}
public function showKzBookings(array $params): void
{
$year = (int)$params['year'];
$month = (int)$params['month'];
$kz = $params['kz'] ?? '';
$validKz = ['81','83','86','84','35','41','44','89','91','93','94','61','62','67','66','69'];
if (!in_array($kz, $validKz, true)) {
View::redirect("/tax/ustva/{$year}/{$month}", 'Ungültige Kennzahl.', 'error');
return;
}
$calc = new UstvaCalculator();
$kzData = $calc->calculate($year, $month);
$bookings = $calc->fetchBookingsForKz($year, $month, $kz);
$months = ['Januar','Februar','März','April','Mai','Juni','Juli','August','September','Oktober','November','Dezember'];
$view = new View();
$view->render('layout/base.php', [
'pageTitle' => "Kz {$kz} – {$months[$month-1]} {$year}",
'content' => $this->renderView('tax/ustva_kz_bookings', [
'year' => $year,
'month' => $month,
'kz' => $kz,
'kzName' => $kzData[$kz]['name'] ?? '',
'bookings' => $bookings,
'months' => $months,
]),
]);
}
public function downloadXml(array $params): void
{
$year = (int)$params['year'];

View file

@ -182,14 +182,113 @@ class Booking
return $reversalId;
}
public static function upsertEigenanteil30(
int $belegId,
float $betragNetto,
float $ustSatz,
string $buchungsDatum
): void {
$acc8922 = Account::findByKontonummer('8922');
$acc1800 = Account::findByKontonummer('1800');
$acc1776 = Account::findByKontonummer('1776');
if (!$acc8922 || !$acc1800) {
throw new \RuntimeException('Konten 1800 und/oder 8922 nicht gefunden.');
}
$netto30 = round($betragNetto * 0.3, 2);
$ust30 = round($betragNetto * $ustSatz / 100 * 0.3, 2);
$isTest = Setting::isTestMode();
// Buchung 1: 30% Netto an 8922 (Eigenanteil Betriebsausgabe)
self::upsertKorrekturbuchung(
belegId: $belegId,
kategorieId: (int)$acc8922['id'],
zahlungsId: (int)$acc1800['id'],
betragNetto: $netto30,
buchungsDatum: $buchungsDatum,
buchungstext: '30% Eigenanteil (nicht abzugsfähig)',
isTest: $isTest,
);
// Buchung 2: 30% USt-Korrektur 1800→1776 (nur wenn USt > 0)
if ($ustSatz > 0 && $acc1776) {
self::upsertKorrekturbuchung(
belegId: $belegId,
kategorieId: (int)$acc1776['id'],
zahlungsId: (int)$acc1800['id'],
betragNetto: $ust30,
buchungsDatum: $buchungsDatum,
buchungstext: '30% Vorsteuerkorrektur Eigenanteil',
isTest: $isTest,
);
}
}
private static function upsertKorrekturbuchung(
int $belegId,
int $kategorieId,
int $zahlungsId,
float $betragNetto,
string $buchungsDatum,
string $buchungstext,
bool $isTest,
): void {
$existing = Database::fetchOne(
'SELECT * FROM bookings
WHERE beleg_id = ? AND kategorie_konto_id = ? AND zahlungskonto_id = ?
AND is_reversal = 0 AND reversed_by IS NULL
LIMIT 1',
[$belegId, $kategorieId, $zahlungsId]
);
if ($existing) {
if ($isTest) {
Database::execute(
'UPDATE bookings
SET betrag_netto = ?, betrag_ust = 0, betrag_brutto = ?,
buchungs_datum = ?, buchungstext = ?
WHERE id = ?',
[$betragNetto, $betragNetto, $buchungsDatum, $buchungstext, $existing['id']]
);
} else {
self::reverse((int)$existing['id'], 'Anpassung 30% Eigenanteil');
self::create([
'beleg_id' => $belegId,
'buchungs_datum' => $buchungsDatum,
'buchungstyp' => 'Ausgabe',
'kategorie_konto_id' => $kategorieId,
'zahlungskonto_id' => $zahlungsId,
'betrag_netto' => $betragNetto,
'ust_satz' => 0,
'steuerart' => 'normal',
'buchungstext' => $buchungstext,
]);
}
} else {
self::create([
'beleg_id' => $belegId,
'buchungs_datum' => $buchungsDatum,
'buchungstyp' => 'Ausgabe',
'kategorie_konto_id' => $kategorieId,
'zahlungskonto_id' => $zahlungsId,
'betrag_netto' => $betragNetto,
'ust_satz' => 0,
'steuerart' => 'normal',
'buchungstext' => $buchungstext,
]);
}
}
public static function generateBuchungsNr(): string
{
$year = date('Y');
$result = Database::fetchOne(
'SELECT COUNT(*) as cnt FROM bookings WHERE YEAR(buchungs_datum) = ?',
[$year]
"SELECT MAX(CAST(SUBSTRING_INDEX(buchungs_nr, '-', -1) AS UNSIGNED)) as maxnr
FROM bookings WHERE buchungs_nr LIKE ?",
["BU-{$year}-%"]
);
$next = (int)($result['cnt'] ?? 0) + 1;
$next = (int)($result['maxnr'] ?? 0) + 1;
return sprintf('BU-%s-%06d', $year, $next);
}

View file

@ -8,8 +8,7 @@ use Steuerkram\Models\Setting;
/**
* Erzeugt eine ELSTER-konforme XML-Datei für die UStVA.
* Kompatibel mit dem Elster Online Portal (Dateiupload, keine ERiC-Signatur nötig).
* Schema: ElsterOnline v11
* Format: Anmeldungssteuern Upload-Schema v{year} (kein ERiC nötig).
*/
class ElsterXmlService
{
@ -18,203 +17,151 @@ class ElsterXmlService
$settings = Setting::get();
$steuernummer = preg_replace('/[^0-9]/', '', $settings['ustva_steuernummer'] ?? '');
$bundesland = strtoupper($settings['ustva_bundesland'] ?? 'DE');
$erstelltAm = date('Y-m-d\TH:i:s');
$zeitraum = sprintf('%04d%02d', $year, $month);
$firma = htmlspecialchars($settings['firma'] ?? '', ENT_XML1);
$email = htmlspecialchars($settings['email'] ?? '', ENT_XML1);
$inhaber = $settings['inhaber'] ?? $settings['firma'] ?? '';
$firma = $settings['firma'] ?? '';
// Build Kz fields XML
$kzXml = '';
$kzMap = [
'09' => 'Kz09', // Steuernummer (wird im Header gesetzt)
'10' => 'Kz10', // Voranmeldungszeitraum
'22' => 'Kz22', // Berichtigungsvoranmeldung (0=nein)
'81' => 'Kz81',
'86' => 'Kz86',
'35' => 'Kz35',
'41' => 'Kz41',
'44' => 'Kz44',
'83' => 'Kz83',
'84' => 'Kz84',
'66' => 'Kz66',
'61' => 'Kz61',
'62' => 'Kz62',
'69' => 'Kz69',
];
[$strasse, $hausnummer] = $this->splitStrasse($settings['strasse'] ?? '');
[$vorname, $nachname] = $this->splitName($inhaber);
// Helper: format amount for ELSTER (integer cents, no decimals for Beträge; or in EUR with 2 dec)
$fmtBetrag = fn($v) => $v !== null ? number_format((float)$v, 2, '.', '') : '0.00';
$fmtSteuer = fn($v) => $v !== null ? number_format((float)$v, 2, '.', '') : '0.00';
$x = fn(string $v): string => htmlspecialchars($v, ENT_XML1, 'UTF-8');
$fields = [];
// Steuernummer und Voranmeldungszeitraum
$fields['09'] = $steuernummer;
$fields['10'] = $zeitraum;
$fields['22'] = '0'; // Keine Berichtigungsvoranmeldung
foreach ($kz as $nr => $data) {
if ($data['betrag'] !== null && $data['betrag'] != 0) {
$fields[$nr . '_betrag'] = $fmtBetrag($data['betrag']);
}
if ($data['steuer'] !== null && $data['steuer'] != 0) {
$fields[$nr . '_steuer'] = $fmtSteuer($data['steuer']);
}
}
// Build XML
$dom = new \DOMDocument('1.0', 'UTF-8');
$dom->formatOutput = true;
$elster = $dom->createElementNS('http://www.elster.de/elsterxml/schema/v11', 'Elster');
$dom->appendChild($elster);
// TransferHeader
$th = $dom->createElement('TransferHeader');
$th->appendChild($dom->createElement('Version', '11'));
$th->appendChild($dom->createElement('TransferTicket', ''));
$th->appendChild($dom->createElement('Testmerker', '700000004'));
$th->appendChild($dom->createElement('HerstellerID', '74931'));
$th->appendChild($dom->createElement('DatenArt', 'UStVA'));
$th->appendChild($dom->createElement('Vorgang', 'send-Auth'));
$elster->appendChild($th);
// DatenTeil
$dt = $dom->createElement('DatenTeil');
$elster->appendChild($dt);
$nb = $dom->createElement('Nutzdatenblock');
$dt->appendChild($nb);
// NutzdatenHeader
$ndh = $dom->createElement('NutzdatenHeader');
$ndh->setAttribute('version', '11');
$ndh->appendChild($dom->createElement('NutzdatenTicket', '1'));
$empf = $dom->createElement('Empfaenger');
$empf->setAttribute('id', 'F');
$empf->nodeValue = $this->getFinanzamtCode($bundesland);
$ndh->appendChild($empf);
$hersteller = $dom->createElement('Hersteller');
$hersteller->appendChild($dom->createElement('ProduktName', 'Steuerkram'));
$hersteller->appendChild($dom->createElement('ProduktVersion', '1.0'));
$ndh->appendChild($hersteller);
$nb->appendChild($ndh);
// Nutzdaten
$nd = $dom->createElement('Nutzdaten');
$nb->appendChild($nd);
// Anmeldungssteuern
$anm = $dom->createElement('Anmeldungssteuern');
$anm->setAttribute('art', 'UStVA');
$anm->setAttribute('version', sprintf('%04d%02d', $year, $month));
$nd->appendChild($anm);
$ns = sprintf('http://finkonsens.de/elster/elsteranmeldung/ustva/v%d', $year);
$anm = $dom->createElementNS($ns, 'Anmeldungssteuern');
$anm->setAttributeNS('http://www.w3.org/2000/xmlns/', 'xmlns', $ns);
$anm->setAttribute('version', (string)$year);
$dom->appendChild($anm);
// DatenLieferant
$dl = $dom->createElement('DatenLieferant');
$dl->appendChild($dom->createElement('Name', $firma));
$dl->appendChild($dom->createElement('Strasse', htmlspecialchars($settings['strasse'] ?? '', ENT_XML1)));
$dl->appendChild($dom->createElement('PLZ', htmlspecialchars($settings['plz'] ?? '', ENT_XML1)));
$dl->appendChild($dom->createElement('Ort', htmlspecialchars($settings['ort'] ?? '', ENT_XML1)));
if ($email) $dl->appendChild($dom->createElement('Email', $email));
foreach ([
'Name' => $x($inhaber),
'Strasse' => $x($settings['strasse'] ?? ''),
'PLZ' => $x($settings['plz'] ?? ''),
'Ort' => $x($settings['ort'] ?? ''),
'Telefon' => $x($settings['telefon'] ?? ''),
'Email' => $x($settings['email'] ?? ''),
] as $tag => $val) {
$dl->appendChild($dom->createElement($tag, $val));
}
$anm->appendChild($dl);
// Erstellungsdatum
$anm->appendChild($dom->createElement('Erstellungsdatum', date('Ymd')));
// Steuerfall
$sf = $dom->createElement('Steuerfall');
$anm->appendChild($sf);
// Berater (leer)
$berater = $dom->createElement('Berater');
foreach ([
'Bezeichnung','Name','Vorname','Namensvorsatz','Namenszusatz',
'Str','Hausnummer','HNrZusatz','AnschriftenZusatz','Ort','PLZ',
'AuslandsPLZ','Land','PostfachOrt','Postfach','PostfachPLZ',
'GKPLZ','Telefon','Email',
] as $tag) {
$berater->appendChild($dom->createElement($tag));
}
$sf->appendChild($berater);
// Mandant
$mandant = $dom->createElement('Mandant');
foreach ([
'Name' => $x($nachname),
'Vorname' => $x($vorname),
'MandantenNr' => '',
'Bearbeiterkennzeichen' => '',
] as $tag => $val) {
$mandant->appendChild($dom->createElement($tag, $val));
}
$sf->appendChild($mandant);
// Unternehmer
$unternehmer = $dom->createElement('Unternehmer');
foreach ([
'Bezeichnung' => $x($firma),
'Name' => $x($nachname),
'Vorname' => $x($vorname),
'Namensvorsatz' => '',
'Namenszusatz' => '',
'Str' => $x($strasse),
'Hausnummer' => $x($hausnummer),
'HNrZusatz' => '',
'AnschriftenZusatz' => '',
'Ort' => $x($settings['ort'] ?? ''),
'PLZ' => $x($settings['plz'] ?? ''),
'AuslandsPLZ' => '',
'Land' => 'Deutschland',
'PostfachOrt' => '',
'Postfach' => '',
'PostfachPLZ' => '',
'GKPLZ' => '',
'Telefon' => $x($settings['telefon'] ?? ''),
'Email' => $x($settings['email'] ?? ''),
] as $tag => $val) {
$unternehmer->appendChild($dom->createElement($tag, $val));
}
$sf->appendChild($unternehmer);
// Umsatzsteuervoranmeldung
$ustva = $dom->createElement('Umsatzsteuervoranmeldung');
$sf->appendChild($ustva);
// Jahr, Zeitraum
$ustva->appendChild($dom->createElement('Jahr', (string)$year));
$ustva->appendChild($dom->createElement('Zeitraum', sprintf('%02d', $month)));
// Steuernummer
$ustva->appendChild($dom->createElement('Steuernummer', $steuernummer));
// Kz-Felder
// Kz81 (Umsatz 19%)
if (!empty($kz['81']['betrag'])) {
$kzEl = $dom->createElement('Kz81', (string)round($kz['81']['betrag']));
$ustva->appendChild($kzEl);
// 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.
$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'];
foreach ($betragsfelder as $nr) {
$val = (float)($kz[$nr]['betrag'] ?? 0.0);
if ($val != 0.0) {
$ustva->appendChild($dom->createElement('Kz' . $nr, $kzBetrag($val)));
}
// Kz86 (Umsatz 7%)
if (!empty($kz['86']['betrag'])) {
$kzEl = $dom->createElement('Kz86', (string)round($kz['86']['betrag']));
$ustva->appendChild($kzEl);
}
// Kz35 (steuerfreie Umsätze)
if (!empty($kz['35']['betrag'])) {
$kzEl = $dom->createElement('Kz35', (string)round($kz['35']['betrag']));
$ustva->appendChild($kzEl);
$steuerfelder = ['61', '62', '67', '66'];
foreach ($steuerfelder as $nr) {
$val = (float)($kz[$nr]['steuer'] ?? 0.0);
if ($val != 0.0) {
$ustva->appendChild($dom->createElement('Kz' . $nr, $kzSteuer($val)));
}
// Kz41 (steuerfreie ig. Lieferungen)
if (!empty($kz['41']['betrag'])) {
$kzEl = $dom->createElement('Kz41', (string)round($kz['41']['betrag']));
$ustva->appendChild($kzEl);
}
// Kz44 (steuerfreie Ausfuhren)
if (!empty($kz['44']['betrag'])) {
$kzEl = $dom->createElement('Kz44', (string)round($kz['44']['betrag']));
$ustva->appendChild($kzEl);
}
// Kz83 (USt 19%)
$kzEl = $dom->createElement('Kz83', (string)(round($kz['83']['steuer'] * 100)));
$ustva->appendChild($kzEl);
// Kz84 (USt 7%)
$kzEl = $dom->createElement('Kz84', (string)(round($kz['84']['steuer'] * 100)));
$ustva->appendChild($kzEl);
// Kz89 (ig. Erwerb 19% Bemessungsgrundlage)
if (!empty($kz['89']['betrag'])) {
$kzEl = $dom->createElement('Kz89', (string)round($kz['89']['betrag']));
$ustva->appendChild($kzEl);
}
// Kz91 (USt auf ig. Erwerb 19%)
if (!empty($kz['91']['steuer'])) {
$kzEl = $dom->createElement('Kz91', (string)(round($kz['91']['steuer'] * 100)));
$ustva->appendChild($kzEl);
}
// Kz93 (ig. Erwerb 7% Bemessungsgrundlage)
if (!empty($kz['93']['betrag'])) {
$kzEl = $dom->createElement('Kz93', (string)round($kz['93']['betrag']));
$ustva->appendChild($kzEl);
}
// Kz94 (USt auf ig. Erwerb 7%)
if (!empty($kz['94']['steuer'])) {
$kzEl = $dom->createElement('Kz94', (string)(round($kz['94']['steuer'] * 100)));
$ustva->appendChild($kzEl);
}
// Kz66 (Vorsteuer gesamt)
if (!empty($kz['66']['steuer'])) {
$kzEl = $dom->createElement('Kz66', (string)(round($kz['66']['steuer'] * 100)));
$ustva->appendChild($kzEl);
}
// Kz67 (Vorsteuer aus ig. Erwerb)
if (!empty($kz['67']['steuer'])) {
$kzEl = $dom->createElement('Kz67', (string)(round($kz['67']['steuer'] * 100)));
$ustva->appendChild($kzEl);
}
// Kz69 (Vorauszahlung / Überschuss)
$kzEl = $dom->createElement('Kz69', (string)(round($kz['69']['steuer'] * 100)));
$ustva->appendChild($kzEl);
// Zahllast / Erstattung → Kz39
$zahllast = (float)($kz['69']['steuer'] ?? 0.0);
$ustva->appendChild($dom->createElement('Kz39', $kzSteuer($zahllast)));
return $dom->saveXML();
}
private function getFinanzamtCode(string $bundesland): string
/**
* Trennt Straßenname und Hausnummer. "Bietigheimer Str. 67" → ["Bietigheimer Str.", "67"]
*/
private function splitStrasse(string $strasse): array
{
// Standard-Finanzamt-Code pro Bundesland für ELSTER
$codes = [
'BW' => '2800', 'BY' => '9200', 'BE' => '1100', 'BB' => '3000',
'HB' => '2400', 'HH' => '2200', 'HE' => '2600', 'MV' => '4000',
'NI' => '2300', 'NW' => '5100', 'RP' => '2700', 'SL' => '1001',
'SN' => '3200', 'ST' => '3100', 'SH' => '2100', 'TH' => '4100',
];
return $codes[$bundesland] ?? '9999';
if (preg_match('/^(.*?)\s+(\d+\w*)$/', trim($strasse), $m)) {
return [$m[1], $m[2]];
}
return [$strasse, ''];
}
/**
* Trennt Vorname und Nachname. "Felix Schepmann" → ["Felix", "Schepmann"]
*/
private function splitName(string $name): array
{
$parts = explode(' ', trim($name), 2);
if (count($parts) === 2) {
return [$parts[0], $parts[1]];
}
return ['', $name];
}
}

View file

@ -9,137 +9,134 @@ use Steuerkram\Database;
/**
* Berechnet alle ELSTER-Kennzahlen (Kz) für die Umsatzsteuervoranmeldung.
*
* Maßgeblich sind buchungstyp, steuerart, ust_satz und betrag_ust der Buchung.
* Maßgeblich ist ausschließlich das Feld accounts.ustva_kz des Kategorie-Kontos.
*
* Buchungslogik:
* Umsatzsteuer (Kz 81/86): buchungstyp = 'Einnahme', steuerart = 'normal', ust_satz > 0
* Steuerfreie Umsätze (Kz 35): buchungstyp = 'Einnahme', steuerart = 'normal', ust_satz = 0
* Steuerfreie ig. Lieferungen (Kz 41): buchungstyp = 'Einnahme', steuerart = 'ig_lieferung'
* Ausfuhren (Kz 44): buchungstyp = 'Einnahme', steuerart = 'export'
* Vorsteuer (Kz 61/62): buchungstyp = 'Ausgabe', steuerart = 'normal', ust_satz > 0
* Ig. Erwerb 19 % (Kz 89/91): steuerart = 'ig_erwerb_19'
* Ig. Erwerb 7 % (Kz 93/94): steuerart = 'ig_erwerb_7'
* Vorsteuer aus ig. Erwerb (Kz 67): abgeleitet aus Kz 91 + Kz 94
* Kontozuordnung:
* 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)
* ustva_kz = '44' → Kz 44 (betrag_netto)
* ustva_kz = '61' → Kz 61 (betrag_ust) [Betriebsausgabe-Konten mit VSt19]
* ustva_kz = '62' → Kz 62 (betrag_ust) [Betriebsausgabe-Konten mit VSt7]
* ustva_kz = '89' → Kz 89 (betrag_netto), Kz 91 (betrag_ust) [ig. Erwerb 19 %]
* ustva_kz = '93' → Kz 93 (betrag_netto), Kz 94 (betrag_ust) [ig. Erwerb 7 %]
*
* 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
{
public function calculate(int $year, int $month): array
{
$kz = [
// Steuerpflichtige und steuerfreie Umsätze
'81' => ['name' => 'Steuerpfl. Umsätze 19 %', 'betrag' => 0.0, 'steuer' => 0.0],
'86' => ['name' => 'Steuerpfl. Umsätze 7 %', 'betrag' => 0.0, 'steuer' => 0.0],
'35' => ['name' => 'Steuerfreie Umsätze (ohne Vorsteuerabzug)', 'betrag' => 0.0, 'steuer' => null],
'41' => ['name' => 'Steuerfreie ig. Lieferungen (§ 4 Nr. 1b)', 'betrag' => 0.0, 'steuer' => null],
'44' => ['name' => 'Steuerfreie Ausfuhren (§ 4 Nr. 1a)', 'betrag' => 0.0, 'steuer' => null],
// USt auf Umsätze (abgeleitet)
'83' => ['name' => 'USt auf steuerpfl. Umsätze 19 %', 'betrag' => null, 'steuer' => 0.0],
'84' => ['name' => 'USt auf steuerpfl. Umsätze 7 %', 'betrag' => null, 'steuer' => 0.0],
// Innergemeinschaftlicher Erwerb
'89' => ['name' => 'Ig. Erwerb 19 % – Bemessungsgrundlage', 'betrag' => 0.0, 'steuer' => null],
'91' => ['name' => 'USt auf ig. Erwerb 19 %', 'betrag' => null, 'steuer' => 0.0],
'93' => ['name' => 'Ig. Erwerb 7 % – Bemessungsgrundlage', 'betrag' => 0.0, 'steuer' => null],
'94' => ['name' => 'USt auf ig. Erwerb 7 %', 'betrag' => null, 'steuer' => 0.0],
// Vorsteuer
'61' => ['name' => 'Vorsteuer aus Eingangsrechnungen 19 %', 'betrag' => null, 'steuer' => 0.0],
'62' => ['name' => 'Vorsteuer aus Eingangsrechnungen 7 %', 'betrag' => null, 'steuer' => 0.0],
'67' => ['name' => 'Vorsteuer aus ig. Erwerb', 'betrag' => null, 'steuer' => 0.0],
'66' => ['name' => 'Vorsteuerbeträge gesamt', 'betrag' => null, 'steuer' => 0.0],
// Ergebnis
'69' => ['name' => 'Verbleibende USt-Vorauszahlung/Erstattung', 'betrag' => null, 'steuer' => 0.0],
/**
* Welche accounts.ustva_kz-Werte liefern Buchungen für die Drill-down-Ansicht einer Kennzahl.
*/
private const KZ_ACCOUNT_MAP = [
'81' => ['81'],
'83' => ['81'],
'86' => ['86'],
'84' => ['86'],
'35' => ['35'],
'41' => ['41'],
'44' => ['44'],
'61' => ['61'],
'62' => ['62'],
'89' => ['89'],
'91' => ['89'],
'93' => ['93'],
'94' => ['93'],
'67' => ['89', '93'],
'66' => ['61', '62', '89', '93'],
'69' => ['81', '86', '35', '41', '44', '61', '62', '89', '93'],
];
$bookings = $this->fetchBookings($year, $month);
foreach ($bookings as $b) {
$steuerart = $b['steuerart'] ?? 'normal';
$ustSatz = (float)$b['ust_satz'];
$betragUst = abs((float)$b['betrag_ust']);
$betragNetto = abs((float)$b['betrag_netto']);
$buchungstyp = $b['buchungstyp'];
// ── Einnahmen ───────────────────────────────────────────────
if ($buchungstyp === 'Einnahme') {
switch ($steuerart) {
case 'ig_lieferung':
$kz['41']['betrag'] += $betragNetto;
break;
case 'export':
$kz['44']['betrag'] += $betragNetto;
break;
default: // normal
if ($ustSatz > 0) {
if (abs($ustSatz - 19) < 0.01) {
$kz['81']['betrag'] += $betragNetto;
} elseif (abs($ustSatz - 7) < 0.01) {
$kz['86']['betrag'] += $betragNetto;
}
} else {
$kz['35']['betrag'] += $betragNetto;
}
}
}
// ── Ausgaben ────────────────────────────────────────────────
if ($buchungstyp === 'Ausgabe') {
switch ($steuerart) {
case 'ig_erwerb_19':
$kz['89']['betrag'] += $betragNetto;
break;
case 'ig_erwerb_7':
$kz['93']['betrag'] += $betragNetto;
break;
default: // normal
if ($ustSatz > 0 && $betragUst > 0) {
if (abs($ustSatz - 19) < 0.01) {
$kz['61']['steuer'] += $betragUst;
} elseif (abs($ustSatz - 7) < 0.01) {
$kz['62']['steuer'] += $betragUst;
}
}
}
}
}
// ── Abgeleitete Kennzahlen ───────────────────────────────────────
$kz['83']['steuer'] = round($kz['81']['betrag'] * 0.19, 2);
$kz['84']['steuer'] = round($kz['86']['betrag'] * 0.07, 2);
$kz['91']['steuer'] = round($kz['89']['betrag'] * 0.19, 2);
$kz['94']['steuer'] = round($kz['93']['betrag'] * 0.07, 2);
// Vorsteuer aus ig. Erwerb = Steuer auf ig. Erwerb (vollständig abziehbar)
$kz['67']['steuer'] = round($kz['91']['steuer'] + $kz['94']['steuer'], 2);
// Vorsteuer gesamt
$kz['66']['steuer'] = round(
$kz['61']['steuer'] + $kz['62']['steuer'] + $kz['67']['steuer'],
2
);
// Verbleibende Zahllast
$ustGesamt = $kz['83']['steuer'] + $kz['84']['steuer']
+ $kz['91']['steuer'] + $kz['94']['steuer'];
$kz['69']['steuer'] = round($ustGesamt - $kz['66']['steuer'], 2);
// Runden
foreach ($kz as &$field) {
if ($field['betrag'] !== null) $field['betrag'] = round($field['betrag'], 2);
if ($field['steuer'] !== null) $field['steuer'] = round($field['steuer'], 2);
}
return $kz;
}
private function fetchBookings(int $year, int $month): array
public function calculate(int $year, int $month): array
{
return Database::fetchAll(
'SELECT bk.buchungstyp, bk.steuerart, bk.ust_satz, bk.betrag_netto, bk.betrag_ust
$rows = 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 bk.is_reversal = 0',
AND bk.is_reversal = 0
AND a.ustva_kz IS NOT NULL AND a.ustva_kz != \'\'
GROUP BY a.ustva_kz',
[$year, $month]
);
$byKz = [];
foreach ($rows as $row) {
$byKz[$row['ustva_kz']] = [
'netto' => (float)$row['summe_netto'],
'ust' => (float)$row['summe_ust'],
];
}
$n = fn(string $kz): float => abs($byKz[$kz]['netto'] ?? 0.0);
$u = fn(string $kz): float => abs($byKz[$kz]['ust'] ?? 0.0);
$kz81 = $n('81'); $kz83 = $u('81');
$kz86 = $n('86'); $kz84 = $u('86');
$kz35 = $n('35');
$kz41 = $n('41');
$kz44 = $n('44');
$kz89 = $n('89'); $kz91 = $u('89');
$kz93 = $n('93'); $kz94 = $u('93');
$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)],
];
}
public function fetchBookingsForKz(int $year, int $month, string $kz): array
{
$accountKzList = self::KZ_ACCOUNT_MAP[$kz] ?? null;
if ($accountKzList === null) {
return [];
}
$placeholders = implode(',', array_fill(0, count($accountKzList), '?'));
$params = array_merge([$year, $month], $accountKzList);
return Database::fetchAll(
'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.beleg_id, bl.beleg_nr,
k.kontonummer AS kategorie_nr, k.bezeichnung AS kategorie_name,
z.kontonummer AS zahlungs_nr, z.bezeichnung AS zahlungs_name
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
WHERE fy.year = ? AND MONTH(bk.buchungs_datum) = ?
AND bk.is_reversal = 0
AND k.ustva_kz IN (' . $placeholders . ')
ORDER BY bk.buchungs_datum, bk.id',
$params
);
}
}

View file

@ -391,7 +391,7 @@ $suppliers = $suppliers ?? [];
<div class="form-grid form-grid-2">
<div class="form-group">
<label>Nettobetrag (€) *</label>
<input type="number" name="betrag_netto" step="0.01" min="0.01" required
<input type="number" name="betrag_netto" step="0.01" required
placeholder="0.00"
value="<?= $h($formData['betrag_netto'] ?? '') ?>"
class="<?= isset($formErrors['betrag_netto']) ? 'input-error' : '' ?>">
@ -410,6 +410,13 @@ $suppliers = $suppliers ?? [];
</div>
</div>
<div class="form-group" style="margin-bottom:.75rem">
<label style="display:flex;align-items:center;gap:.5rem;cursor:pointer">
<input type="checkbox" name="eigenanteil_30" value="1">
30% Eigenanteil buchen
</label>
</div>
<button type="submit" class="btn btn-primary">Buchung erfassen</button>
</form>
</div>

View file

@ -0,0 +1,97 @@
<?php
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.
// Einnahmen mit USt = positiv, Ausgaben mit Vorsteuer = negativ,
// ig_erwerb heben sich gegenseitig auf → 0.
$ustBeitragKz69 = function(array $b): float {
$steuerart = $b['steuerart'] ?? 'normal';
$ustSatz = (float)$b['ust_satz'];
$betragUst = (float)$b['betrag_ust'];
if ($b['buchungstyp'] === 'Einnahme' && $steuerart === 'normal' && $ustSatz > 0) {
return $betragUst; // Umsatzsteuer → positiv
}
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)
};
// Ausgaben werden als negative Beträge angezeigt (Geld fließt ab).
$signedBetrag = fn(array $b, string $field): float =>
$b['buchungstyp'] === 'Ausgabe' ? -abs((float)$b[$field]) : abs((float)$b[$field]);
?>
<div class="card">
<div class="card-header">
<div>
<div class="card-title">Kz <?= $h($kz) ?> – <?= $h($kzName) ?></div>
<div class="text-muted text-sm"><?= $h($months[$month-1]) ?> <?= $h($year) ?></div>
</div>
<a href="/tax/ustva/<?= $h($year) ?>/<?= $h($month) ?>" class="btn btn-secondary btn-sm">← Zurück zur UStVA</a>
</div>
<?php if (empty($bookings)): ?>
<div class="card-body">
<p class="text-muted">Keine Buchungen für diesen Zeitraum.</p>
</div>
<?php else: ?>
<div class="table-wrap">
<table>
<thead>
<tr>
<th>Datum</th>
<th>Beleg</th>
<th>Buchungstext</th>
<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">Brutto</th>
</tr>
</thead>
<tbody>
<?php
$sumNetto = 0.0;
$sumUst = 0.0;
$sumBrutto = 0.0;
foreach ($bookings as $b):
$ust = $kz === '69' ? $ustBeitragKz69($b) : $signedBetrag($b, 'betrag_ust');
$netto = $signedBetrag($b, 'betrag_netto');
$brutto = $signedBetrag($b, 'betrag_brutto');
$sumNetto += $netto;
$sumUst += $ust;
$sumBrutto += $brutto;
?>
<tr>
<td class="td-mono text-sm"><?= $h($b['buchungs_datum']) ?></td>
<td>
<?php if ($b['beleg_id']): ?>
<a href="/belege/<?= (int)$b['beleg_id'] ?>"><?= $h($b['beleg_nr'] ?? '#' . $b['beleg_id']) ?></a>
<?php else: ?>
<span class="text-muted">–</span>
<?php endif; ?>
</td>
<td><?= $h($b['buchungstext']) ?></td>
<td class="text-sm text-muted"><?= $h($b['kategorie_nr']) ?> <?= $h($b['kategorie_name']) ?></td>
<td class="text-sm text-muted"><?= $h($b['zahlungs_nr']) ?> <?= $h($b['zahlungs_name']) ?></td>
<td class="td-right td-mono"><?= $fmtKz($netto) ?></td>
<td class="td-right td-mono"><?= $fmtKz($ust) ?></td>
<td class="td-right td-mono"><?= $fmtKz($brutto) ?></td>
</tr>
<?php endforeach; ?>
</tbody>
<tfoot>
<tr style="font-weight:700;border-top:2px solid #000">
<td colspan="5">Summe (<?= count($bookings) ?> Buchungen)</td>
<td class="td-right td-mono"><?= $fmtKz($sumNetto) ?></td>
<td class="td-right td-mono"><?= $fmtKz($sumUst) ?></td>
<td class="td-right td-mono"><?= $fmtKz($sumBrutto) ?></td>
</tr>
</tfoot>
</table>
</div>
<?php endif; ?>
</div>

View file

@ -53,10 +53,18 @@ $fmtKz = fn($v) => $v !== null ? number_format((float)$v, 2, ',', '.') . ' €'
<td class="td-mono" style="color:var(--color-muted);width:50px"><?= $h($nr) ?></td>
<td><?= $h($field['name']) ?></td>
<td class="td-right td-mono">
<?= $field['betrag'] !== null ? $fmtKz($field['betrag']) : '–' ?>
<?php if ($field['betrag'] !== null): ?>
<a href="/tax/ustva/<?= $year ?>/<?= $month ?>/kz/<?= $nr ?>" style="text-decoration:none;color:inherit"><?= $fmtKz($field['betrag']) ?></a>
<?php else: ?>
<?= $isResult ? '' : '–' ?>
<?php endif; ?>
</td>
<td class="td-right td-mono" style="<?= $isNegative ? 'color:var(--color-success)' : ($isResult ? 'color:var(--color-danger)' : '') ?>">
<?= $field['steuer'] !== null ? $fmtKz($field['steuer']) : '–' ?>
<?php if ($field['steuer'] !== null): ?>
<a href="/tax/ustva/<?= $year ?>/<?= $month ?>/kz/<?= $nr ?>" style="text-decoration:none;color:inherit"><?= $fmtKz($field['steuer']) ?></a>
<?php else: ?>
–
<?php endif; ?>
</td>
</tr>
<?php endforeach; ?>