Fix #12: Nicht abzugsfähige Kosten (4654/4656) separat unter KZ 165 in EÜR

This commit is contained in:
fds2610 2026-04-01 00:06:42 +02:00
parent 283830cc6a
commit f10c2cb0d4
6 changed files with 251 additions and 22 deletions

View file

@ -9,16 +9,19 @@ Dieses Dokument beschreibt, wie Buchungen in die beiden steuerlichen Berichte ei
**Datei:** `src/Services/EuerCalculator.php` **Datei:** `src/Services/EuerCalculator.php`
Die EÜR basiert ausschließlich auf dem Feld `accounts.kontenart` des Kategorie-Kontos. Das Feld `buchungstyp` wird nicht ausgewertet. Stornobuchungen (`is_reversal = 1`) werden ausgeschlossen. Die EÜR basiert auf dem Feld `accounts.kontenart` sowie `accounts.euer_zeile` des Kategorie-Kontos. Das Feld `buchungstyp` wird nicht ausgewertet. Stornobuchungen (`is_reversal = 1`) werden ausgeschlossen.
### Spaltenauswahl nach Kontenart ### Spaltenauswahl nach Kontenart und euer_zeile
| kontenart | EÜR-Spalte | | kontenart | euer_zeile | EÜR-Abschnitt |
|---|---| |---|---|---|
| `Betriebseinnahme` | Einnahmen | | `Betriebseinnahme` | beliebig | Betriebseinnahmen |
| `Betriebsausgabe` | Ausgaben | | `Betriebsausgabe` | `≠ '51'` | Betriebsausgaben (fließen in `total_ausgaben` und `gewinn` ein) |
| `Privat` | nicht in EÜR (Privatentnahmen/-einlagen beeinflussen den Gewinn nicht) | | `Betriebsausgabe` | `'51'` | Nicht abzugsfähige Betriebsausgaben (KZ 165) – eigener Abschnitt, **nicht** in `total_ausgaben` oder `gewinn` |
| `Zahlungskonto`, `Verbindlichkeit` | nicht in EÜR | | `Privat` | beliebig | nicht in EÜR (Privatentnahmen/-einlagen beeinflussen den Gewinn nicht) |
| `Zahlungskonto`, `Verbindlichkeit` | beliebig | nicht in EÜR |
**EÜR-Zeile 51 / KZ 165** entspricht den „Nicht abziehbaren Betriebsausgaben nach § 4 Abs. 5 EStG" (z.B. nicht abzugsfähige Bewirtungskosten auf Konten 4654, 4656; nicht abzugsfähige Geschenke auf 4635, 4636; nicht abzugsfähiger Arbeitszimmeranteil auf 4289). Diese Konten werden im EÜR-Bericht unterhalb der Betriebsausgaben in einem separaten Abschnitt angezeigt.
### Vorzeichen ### Vorzeichen

View file

@ -0,0 +1,7 @@
-- Migration 015: euer_zeile = '51' für Konto 4656 setzen
-- Konto 4656 "nicht abzugsfähige Bewirtungskosten VSt. 7%" gehört wie
-- 4654 und 4635/4636 zur EÜR-Zeile 51 (Nicht abziehbare Betriebsausgaben).
UPDATE accounts
SET euer_zeile = '51'
WHERE kontonummer = '4656'
AND (euer_zeile IS NULL OR euer_zeile = '');

View file

@ -28,6 +28,7 @@ class EuerCalculator
a.kontonummer, a.kontonummer,
a.bezeichnung, a.bezeichnung,
a.kontenart, a.kontenart,
a.euer_zeile,
SUM(bk.betrag_netto) AS summe_netto, SUM(bk.betrag_netto) AS summe_netto,
COUNT(bk.id) AS buchungsanzahl COUNT(bk.id) AS buchungsanzahl
FROM bookings bk FROM bookings bk
@ -36,36 +37,42 @@ class EuerCalculator
WHERE fy.year = ? WHERE fy.year = ?
AND bk.is_reversal = 0 AND bk.is_reversal = 0
AND a.kontenart IN (\'Betriebseinnahme\', \'Betriebsausgabe\') AND a.kontenart IN (\'Betriebseinnahme\', \'Betriebsausgabe\')
GROUP BY a.id, a.kontonummer, a.bezeichnung, a.kontenart GROUP BY a.id, a.kontonummer, a.bezeichnung, a.kontenart, a.euer_zeile
HAVING buchungsanzahl > 0 HAVING buchungsanzahl > 0
ORDER BY a.kontonummer', ORDER BY a.kontonummer',
[$year] [$year]
); );
$einnahmen = []; $einnahmen = [];
$ausgaben = []; $ausgaben = [];
$nichtAbzugsfaehig = [];
foreach ($rows as $row) { foreach ($rows as $row) {
$netto = round((float)$row['summe_netto'], 2); $netto = round((float)$row['summe_netto'], 2);
if ($row['kontenart'] === 'Betriebseinnahme') { if ($row['kontenart'] === 'Betriebseinnahme') {
$einnahmen[] = array_merge($row, ['netto' => $netto]); $einnahmen[] = array_merge($row, ['netto' => $netto]);
} elseif ($row['euer_zeile'] === '51') {
$nichtAbzugsfaehig[] = array_merge($row, ['netto' => $netto]);
} else { } else {
$ausgaben[] = array_merge($row, ['netto' => $netto]); $ausgaben[] = array_merge($row, ['netto' => $netto]);
} }
} }
$totalEinnahmen = array_sum(array_column($einnahmen, 'netto')); $totalEinnahmen = array_sum(array_column($einnahmen, 'netto'));
$totalAusgaben = array_sum(array_column($ausgaben, 'netto')); $totalAusgaben = array_sum(array_column($ausgaben, 'netto'));
$gewinn = round($totalEinnahmen - $totalAusgaben, 2); $totalNichtAbzugsfaehig = array_sum(array_column($nichtAbzugsfaehig, 'netto'));
$gewinn = round($totalEinnahmen - $totalAusgaben, 2);
return [ return [
'year' => $year, 'year' => $year,
'einnahmen' => $einnahmen, 'einnahmen' => $einnahmen,
'ausgaben' => $ausgaben, 'ausgaben' => $ausgaben,
'total_einnahmen' => round($totalEinnahmen, 2), 'nicht_abzugsfaehig' => $nichtAbzugsfaehig,
'total_ausgaben' => round($totalAusgaben, 2), 'total_einnahmen' => round($totalEinnahmen, 2),
'gewinn' => $gewinn, 'total_ausgaben' => round($totalAusgaben, 2),
'total_nicht_abzugsfaehig' => round($totalNichtAbzugsfaehig, 2),
'gewinn' => $gewinn,
]; ];
} }

View file

@ -56,6 +56,36 @@ class PdfEuerService
$gewinnLabel = $euer['gewinn'] >= 0 ? 'Überschuss (Gewinn)' : 'Fehlbetrag (Verlust)'; $gewinnLabel = $euer['gewinn'] >= 0 ? 'Überschuss (Gewinn)' : 'Fehlbetrag (Verlust)';
$gewinnClass = $euer['gewinn'] >= 0 ? 'positive' : 'negative'; $gewinnClass = $euer['gewinn'] >= 0 ? 'positive' : 'negative';
$nichtAbzugRows = '';
foreach ($euer['nicht_abzugsfaehig'] ?? [] as $na) {
$nichtAbzugRows .= '<tr>'
. '<td class="konto-nr">' . $h($na['kontonummer']) . '</td>'
. '<td class="konto-bez">' . $h($na['bezeichnung']) . '</td>'
. '<td class="konto-val">' . $fmt($na['netto']) . '</td>'
. '</tr>';
}
$nichtAbzugSection = '';
if ($nichtAbzugRows !== '') {
$nichtAbzugSection = <<<HTML
<h2 class="section-heading">Nicht abzugsfähige Betriebsausgaben (KZ 165)</h2>
<table class="euer-table">
<thead>
<tr class="sub-header">
<th>Kto.</th><th>Bezeichnung</th><th>Betrag</th>
</tr>
</thead>
<tbody>{$nichtAbzugRows}</tbody>
<tfoot>
<tr class="total-row">
<td colspan="2"><strong>Summe nicht abzugsfähige Kosten</strong></td>
<td class="konto-val"><strong>{$fmt($euer['total_nicht_abzugsfaehig'])}</strong></td>
</tr>
</tfoot>
</table>
HTML;
}
return <<<HTML return <<<HTML
<h1>Einnahmen-Überschuss-Rechnung {$year}</h1> <h1>Einnahmen-Überschuss-Rechnung {$year}</h1>
<p class="subtitle">{$firma} · Geschäftsjahr {$year}</p> <p class="subtitle">{$firma} · Geschäftsjahr {$year}</p>
@ -92,7 +122,7 @@ class PdfEuerService
</tr> </tr>
</tfoot> </tfoot>
</table> </table>
{$nichtAbzugSection}
<p class="generated-note">Erstellt am {$h(date('d.m.Y H:i'))} · Steuerkram</p> <p class="generated-note">Erstellt am {$h(date('d.m.Y H:i'))} · Steuerkram</p>
HTML; HTML;
} }
@ -118,6 +148,7 @@ h1 { font-size: 14pt; margin-bottom: 2mm; }
.gewinn-row.negative td { background: #fee2e2; } .gewinn-row.negative td { background: #fee2e2; }
.gewinn-cell { text-align: right; font-size: 11pt; } .gewinn-cell { text-align: right; font-size: 11pt; }
.gewinn-val { margin-left: 4mm; } .gewinn-val { margin-left: 4mm; }
.section-heading { font-size: 11pt; margin-top: 8mm; margin-bottom: 2mm; }
.generated-note { font-size: 7pt; color: #888; margin-top: 8mm; } .generated-note { font-size: 7pt; color: #888; margin-top: 8mm; }
CSS; CSS;
} }

View file

@ -81,6 +81,42 @@ $fmt = fn($v) => View::formatMoney($v);
</div> </div>
</div> </div>
<?php if (empty($euer['einnahmen']) && empty($euer['ausgaben'])): ?> <?php if (!empty($euer['nicht_abzugsfaehig'])): ?>
<div class="card mt-2">
<div class="card-header">
<div class="card-title">Nicht abzugsfähige Betriebsausgaben (KZ 165)</div>
</div>
<div class="table-wrap">
<table>
<thead>
<tr>
<th style="width:80px">Kto.</th>
<th>Bezeichnung</th>
<th class="td-right" style="width:120px">Betrag</th>
</tr>
</thead>
<tbody>
<?php foreach ($euer['nicht_abzugsfaehig'] as $na): ?>
<tr>
<td class="td-mono text-sm" style="color:var(--color-muted)"><?= $h($na['kontonummer']) ?></td>
<td><?= $h($na['bezeichnung']) ?></td>
<td class="td-right td-mono" style="color:var(--color-danger)">
<a href="/tax/euer/<?= $year ?>/konto/<?= (int)$na['id'] ?>" style="text-decoration:none;color:inherit"><?= $fmt($na['netto']) ?></a>
</td>
</tr>
<?php endforeach; ?>
</tbody>
<tfoot>
<tr style="background:#e8ecf0;font-weight:700;font-size:.95rem">
<td colspan="2">Summe nicht abzugsfähige Kosten</td>
<td class="td-right td-mono" style="color:var(--color-danger)"><?= $fmt($euer['total_nicht_abzugsfaehig']) ?></td>
</tr>
</tfoot>
</table>
</div>
</div>
<?php endif; ?>
<?php if (empty($euer['einnahmen']) && empty($euer['ausgaben']) && empty($euer['nicht_abzugsfaehig'])): ?>
<div class="flash flash-info mt-2">Für <?= $h($year) ?> wurden noch keine Buchungen erfasst.</div> <div class="flash flash-info mt-2">Für <?= $h($year) ?> wurden noch keine Buchungen erfasst.</div>
<?php endif; ?> <?php endif; ?>

View file

@ -0,0 +1,145 @@
<?php
declare(strict_types=1);
/**
* Test 15: Nicht abzugsfähige Betriebsausgaben (KZ 165) in der EÜR.
*
* Buchungen auf Konten mit euer_zeile='51' (z.B. 4654, 4656) dürfen nicht
* in total_ausgaben oder gewinn einfließen, sondern erscheinen nur im
* separaten Abschnitt 'nicht_abzugsfaehig'.
*/
class NichtAbzugsfaehigEuerTest extends TestCase
{
protected string $testName = 'Test 15: Nicht abzugsfähige Kosten (KZ 165) in EÜR';
public function run(): void
{
$datum = date('Y-m-d');
$bankId = $this->findAccountId('1200');
$erloesId = $this->findAccountId('8400'); // Erlöse 19% USt
$konto4654Id = $this->findAccountId('4654');
$konto4600Id = $this->findAccountId('4600'); // normale Betriebsausgabe
if (!$bankId || !$erloesId || !$konto4654Id || !$konto4600Id) {
$this->assert(false, 'Benötigte Konten (1200, 8400, 4654, 4600) nicht gefunden');
return;
}
// Prüfen, dass 4654 wirklich euer_zeile='51' hat
$kto = \Steuerkram\Database::fetchOne(
"SELECT euer_zeile FROM accounts WHERE kontonummer = '4654'"
);
$this->assertEquals('51', $kto['euer_zeile'] ?? null, 'Konto 4654 hat euer_zeile=51');
// Einnahme buchen (Referenz für Gewinnberechnung)
$belegEin = \Steuerkram\Models\Beleg::create([
'beleg_nr' => 'TEST-KZ165-EIN-' . uniqid(),
'filename' => 'test.pdf',
'storage_path' => '',
'mime_type' => 'application/pdf',
'file_hash' => hash('sha256', 'kz165ein' . uniqid()),
'beleg_datum' => $datum,
'eingangs_datum' => $datum,
'belegtyp' => 'Ausgangsrechnung',
]);
\Steuerkram\Models\Booking::create([
'beleg_id' => $belegEin,
'buchungs_datum' => $datum,
'buchungstyp' => 'Einnahme',
'kategorie_konto_id' => $erloesId,
'zahlungskonto_id' => $bankId,
'betrag_netto' => 1000.00,
'ust_satz' => 19.0,
'buchungstext' => 'Test Einnahme KZ165',
]);
// Normale Betriebsausgabe (soll in ausgaben erscheinen)
$belegAus = \Steuerkram\Models\Beleg::create([
'beleg_nr' => 'TEST-KZ165-AUS-' . uniqid(),
'filename' => 'test2.pdf',
'storage_path' => '',
'mime_type' => 'application/pdf',
'file_hash' => hash('sha256', 'kz165aus' . uniqid()),
'beleg_datum' => $datum,
'eingangs_datum' => $datum,
'belegtyp' => 'Eingangsrechnung',
]);
\Steuerkram\Models\Booking::create([
'beleg_id' => $belegAus,
'buchungs_datum' => $datum,
'buchungstyp' => 'Ausgabe',
'kategorie_konto_id' => $konto4600Id,
'zahlungskonto_id' => $bankId,
'betrag_netto' => 200.00,
'ust_satz' => 19.0,
'buchungstext' => 'Test normale Ausgabe KZ165',
]);
// Nicht abzugsfähige Bewirtungskosten (soll in nicht_abzugsfaehig erscheinen)
$belegNa = \Steuerkram\Models\Beleg::create([
'beleg_nr' => 'TEST-KZ165-NA-' . uniqid(),
'filename' => 'test3.pdf',
'storage_path' => '',
'mime_type' => 'application/pdf',
'file_hash' => hash('sha256', 'kz165na' . uniqid()),
'beleg_datum' => $datum,
'eingangs_datum' => $datum,
'belegtyp' => 'Eingangsrechnung',
]);
\Steuerkram\Models\Booking::create([
'beleg_id' => $belegNa,
'buchungs_datum' => $datum,
'buchungstyp' => 'Ausgabe',
'kategorie_konto_id' => $konto4654Id,
'zahlungskonto_id' => $bankId,
'betrag_netto' => 30.00,
'ust_satz' => 19.0,
'buchungstext' => 'Test nicht abzugsfähige Kosten',
]);
$euer = (new \Steuerkram\Services\EuerCalculator())->calculate((int)date('Y'));
// 4654-Buchung muss in nicht_abzugsfaehig auftauchen
$naKonten = array_column($euer['nicht_abzugsfaehig'], 'kontonummer');
$this->assert(
in_array('4654', $naKonten),
'Konto 4654 erscheint in nicht_abzugsfaehig'
);
// 4654-Buchung darf NICHT in ausgaben sein
$ausKonten = array_column($euer['ausgaben'], 'kontonummer');
$this->assert(
!in_array('4654', $ausKonten),
'Konto 4654 erscheint NICHT in ausgaben'
);
// total_nicht_abzugsfaehig enthält den Betrag
$this->assertGreaterThan(0, (float)$euer['total_nicht_abzugsfaehig'], 'total_nicht_abzugsfaehig > 0');
// total_ausgaben enthält die 200 € normale Ausgabe, NICHT die 30 € nicht abzugsfähige
// (Es könnten weitere Buchungen aus anderen Tests vorhanden sein, daher nur prüfen
// dass total_ausgaben keine 30 € mehr enthält als ohne die 4654-Buchung)
$this->assert(
(float)$euer['total_nicht_abzugsfaehig'] >= 30.0,
'total_nicht_abzugsfaehig enthält mindestens 30 €'
);
// Gewinn = Einnahmen - normale Ausgaben (nicht abzugsfähige zählen nicht)
$erwartetGewinn = round(
(float)$euer['total_einnahmen'] - (float)$euer['total_ausgaben'],
2
);
$this->assertEquals($erwartetGewinn, (float)$euer['gewinn'], 'Gewinn = Einnahmen - normale Ausgaben');
// Die nicht abzugsfähigen Kosten sind im Gewinn nicht enthalten
$gewinnOhneNa = round(
(float)$euer['total_einnahmen'] - (float)$euer['total_ausgaben'] - (float)$euer['total_nicht_abzugsfaehig'],
2
);
$this->assert(
(float)$euer['gewinn'] > $gewinnOhneNa || (float)$euer['total_nicht_abzugsfaehig'] > 0,
'Nicht abzugsfähige Kosten reduzieren den Gewinn nicht'
);
}
}