Feature: UStVA Kz 81/89 auf volle Euro abgerundet, Darstellung mit ,-
This commit is contained in:
parent
88aecd86c2
commit
3235030305
5 changed files with 148 additions and 18 deletions
|
|
@ -2,6 +2,13 @@
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
## 2026-05-14 – Feature: UStVA Kz 81/89 auf volle Euro abgerundet
|
||||||
|
|
||||||
|
- `UstvaCalculator`: Kz 81 und 89 werden mit `floor()` auf volle Euro abgerundet; USt wird auf dem gerundeten Wert berechnet
|
||||||
|
- Darstellung: Kz 81/89 zeigen "1.234,- €" statt "1.234,00 €" (in Tabelle und Zusammenfassung)
|
||||||
|
- USt-Felder (ust81, ust89) bleiben als Dezimalzahl unverändert
|
||||||
|
- Test 31: prüft floor-Verhalten für Kz 81 und 89
|
||||||
|
|
||||||
## 2026-05-14 – Refactor: UStVA auf 5-Kennzahlen-Schema vereinfacht
|
## 2026-05-14 – Refactor: UStVA auf 5-Kennzahlen-Schema vereinfacht
|
||||||
|
|
||||||
- Migration 048: Kontenrahmen bereinigt – nur noch `ustva_kz` ∈ {81, 83, 61, 66} aktiv; 1571/1576 → 66, 1574 → 61, 1771/1774 → 83, alle anderen auf NULL
|
- Migration 048: Kontenrahmen bereinigt – nur noch `ustva_kz` ∈ {81, 83, 61, 66} aktiv; 1571/1576 → 66, 1574 → 61, 1771/1774 → 83, alle anderen auf NULL
|
||||||
|
|
|
||||||
|
|
@ -47,8 +47,8 @@ Diese Berechnung wird ausschließlich für **monatliche** Voranmeldungen unterst
|
||||||
|
|
||||||
| Kz | Bezeichnung | Quelle | Berechnung |
|
| Kz | Bezeichnung | Quelle | Berechnung |
|
||||||
|---|---|---|---|
|
|---|---|---|---|
|
||||||
| **81** | Steuerpflichtige Umsätze zum Steuersatz von 19 % | Konto | `SUM(betrag_netto)` WHERE `ustva_kz='81'`, EU-Steuerarten ausgeschlossen |
|
| **81** | Steuerpflichtige Umsätze zum Steuersatz von 19 % | Konto | `floor(SUM(betrag_netto))` WHERE `ustva_kz='81'`, EU-Steuerarten ausgeschlossen – **auf volle Euro abgerundet (floor)** |
|
||||||
| **89** | Steuerpfl. innergemeinschaftliche Erwerbe 19 % | Steuerart | `SUM(betrag_netto)` WHERE `steuerart='ig_erwerb_19'` |
|
| **89** | Steuerpfl. innergemeinschaftliche Erwerbe 19 % | Steuerart | `floor(SUM(betrag_netto))` WHERE `steuerart='ig_erwerb_19'` – **auf volle Euro abgerundet (floor)** |
|
||||||
| **66** | Gesamte Vorsteuerbeträge | Konto | `SUM(betrag_netto)` WHERE `ustva_kz='66'` (Konten 1571, 1576) |
|
| **66** | Gesamte Vorsteuerbeträge | Konto | `SUM(betrag_netto)` WHERE `ustva_kz='66'` (Konten 1571, 1576) |
|
||||||
| **61** | Vorsteuerbeträge i.g. Erwerb | Konto | `SUM(betrag_netto)` WHERE `ustva_kz='61'` (Konto 1574) |
|
| **61** | Vorsteuerbeträge i.g. Erwerb | Konto | `SUM(betrag_netto)` WHERE `ustva_kz='61'` (Konto 1574) |
|
||||||
| **83** | Vorauszahlung / Überschuss | abgeleitet | `(Summe ustva_kz='83') − Kz 66 − Kz 61` |
|
| **83** | Vorauszahlung / Überschuss | abgeleitet | `(Summe ustva_kz='83') − Kz 66 − Kz 61` |
|
||||||
|
|
@ -57,8 +57,8 @@ Zusätzlich werden zwei **reine Anzeigefelder** berechnet (erscheinen im Bericht
|
||||||
|
|
||||||
| Feld | Bezeichnung | Berechnung |
|
| Feld | Bezeichnung | Berechnung |
|
||||||
|---|---|---|
|
|---|---|---|
|
||||||
| ust81 | Abzuführende USt aus Kz 81 (19 %) | `Kz 81 × 0,19` |
|
| ust81 | Abzuführende USt aus Kz 81 (19 %) | `round(Kz 81 × 0,19, 2)` (Kz 81 ist bereits abgerundet) |
|
||||||
| ust89 | Abzuführende USt aus Kz 89 (19 %) | `Kz 89 × 0,19` |
|
| ust89 | Abzuführende USt aus Kz 89 (19 %) | `round(Kz 89 × 0,19, 2)` (Kz 89 ist bereits abgerundet) |
|
||||||
|
|
||||||
### Datenquellen
|
### Datenquellen
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -75,7 +75,9 @@ class UstvaCalculator
|
||||||
// ── 3. Kennzahlen zusammensetzen ─────────────────────────────────────
|
// ── 3. Kennzahlen zusammensetzen ─────────────────────────────────────
|
||||||
$n = fn(string $kz): float => (float)($byKz[$kz] ?? 0.0);
|
$n = fn(string $kz): float => (float)($byKz[$kz] ?? 0.0);
|
||||||
|
|
||||||
$kz81 = $n('81');
|
// Kz 81 und 89 werden laut UStVA auf volle Euro abgerundet
|
||||||
|
$kz81 = floor($n('81'));
|
||||||
|
$kz89 = floor($kz89);
|
||||||
$kz66 = $n('66');
|
$kz66 = $n('66');
|
||||||
$kz61 = $n('61');
|
$kz61 = $n('61');
|
||||||
$kz83 = $n('83') - $kz66 - $kz61;
|
$kz83 = $n('83') - $kz66 - $kz61;
|
||||||
|
|
@ -83,13 +85,13 @@ class UstvaCalculator
|
||||||
$ust89 = round($kz89 * 0.19, 2);
|
$ust89 = round($kz89 * 0.19, 2);
|
||||||
|
|
||||||
return [
|
return [
|
||||||
'81' => ['name' => 'Steuerpflichtige Umsätze zum Steuersatz von 19 %', 'betrag' => round($kz81, 2), 'steuer' => null, 'xml' => true],
|
'81' => ['name' => 'Steuerpflichtige Umsätze zum Steuersatz von 19 %', 'betrag' => $kz81, 'steuer' => null, 'xml' => true, 'ganze_euro' => true],
|
||||||
'ust81' => ['name' => 'Abzuführende USt aus Kz 81 (19 %)', 'betrag' => null, 'steuer' => $ust81, 'xml' => false],
|
'ust81' => ['name' => 'Abzuführende USt aus Kz 81 (19 %)', 'betrag' => null, 'steuer' => $ust81, 'xml' => false, 'ganze_euro' => false],
|
||||||
'89' => ['name' => 'Steuerpfl. innergemeinschaftliche Erwerbe 19 %', 'betrag' => round($kz89, 2), 'steuer' => null, 'xml' => true],
|
'89' => ['name' => 'Steuerpfl. innergemeinschaftliche Erwerbe 19 %', 'betrag' => $kz89, 'steuer' => null, 'xml' => true, 'ganze_euro' => true],
|
||||||
'ust89' => ['name' => 'Abzuführende USt aus Kz 89 (19 %)', 'betrag' => null, 'steuer' => $ust89, 'xml' => false],
|
'ust89' => ['name' => 'Abzuführende USt aus Kz 89 (19 %)', 'betrag' => null, 'steuer' => $ust89, 'xml' => false, 'ganze_euro' => false],
|
||||||
'66' => ['name' => 'Gesamte Vorsteuerbeträge', 'betrag' => null, 'steuer' => round($kz66, 2), 'xml' => true],
|
'66' => ['name' => 'Gesamte Vorsteuerbeträge', 'betrag' => null, 'steuer' => round($kz66, 2), 'xml' => true, 'ganze_euro' => false],
|
||||||
'61' => ['name' => 'Vorsteuerbeträge i.g. Erwerb', 'betrag' => null, 'steuer' => round($kz61, 2), 'xml' => true],
|
'61' => ['name' => 'Vorsteuerbeträge i.g. Erwerb', 'betrag' => null, 'steuer' => round($kz61, 2), 'xml' => true, 'ganze_euro' => false],
|
||||||
'83' => ['name' => 'Vorauszahlung / Überschuss', 'betrag' => null, 'steuer' => round($kz83, 2), 'xml' => true],
|
'83' => ['name' => 'Vorauszahlung / Überschuss', 'betrag' => null, 'steuer' => round($kz83, 2), 'xml' => true, 'ganze_euro' => false],
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,7 @@ use Steuerkram\View;
|
||||||
$h = fn($v) => View::h($v);
|
$h = fn($v) => View::h($v);
|
||||||
$fmt = fn($v) => View::formatMoney($v);
|
$fmt = fn($v) => View::formatMoney($v);
|
||||||
$fmtKz = fn($v) => $v !== null ? number_format((float)$v, 2, ',', '.') . ' €' : '–';
|
$fmtKz = fn($v) => $v !== null ? number_format((float)$v, 2, ',', '.') . ' €' : '–';
|
||||||
|
$fmtKzGanz = fn($v) => $v !== null ? number_format((float)$v, 0, ',', '.') . ',- €' : '–';
|
||||||
?>
|
?>
|
||||||
|
|
||||||
<div style="display:grid;grid-template-columns:1fr 340px;gap:1.5rem;align-items:start">
|
<div style="display:grid;grid-template-columns:1fr 340px;gap:1.5rem;align-items:start">
|
||||||
|
|
@ -52,11 +53,13 @@ $fmtKz = fn($v) => $v !== null ? number_format((float)$v, 2, ',', '.') . ' €'
|
||||||
<td class="td-mono" style="color:var(--color-muted);width:50px"><?= $isDisplay ? '' : $h($nr) ?></td>
|
<td class="td-mono" style="color:var(--color-muted);width:50px"><?= $isDisplay ? '' : $h($nr) ?></td>
|
||||||
<td><?= $h($field['name']) ?></td>
|
<td><?= $h($field['name']) ?></td>
|
||||||
<td class="td-right td-mono">
|
<td class="td-right td-mono">
|
||||||
<?php if ($field['betrag'] !== null): ?>
|
<?php if ($field['betrag'] !== null):
|
||||||
|
$fmtB = ($field['ganze_euro'] ?? false) ? $fmtKzGanz : $fmtKz;
|
||||||
|
?>
|
||||||
<?php if (!$isDisplay): ?>
|
<?php if (!$isDisplay): ?>
|
||||||
<a href="/tax/ustva/<?= $year ?>/<?= $month ?>/kz/<?= $nr ?>" style="text-decoration:none;color:inherit"><?= $fmtKz($field['betrag']) ?></a>
|
<a href="/tax/ustva/<?= $year ?>/<?= $month ?>/kz/<?= $nr ?>" style="text-decoration:none;color:inherit"><?= $fmtB($field['betrag']) ?></a>
|
||||||
<?php else: ?>
|
<?php else: ?>
|
||||||
<?= $fmtKz($field['betrag']) ?>
|
<?= $fmtB($field['betrag']) ?>
|
||||||
<?php endif; ?>
|
<?php endif; ?>
|
||||||
<?php else: ?>
|
<?php else: ?>
|
||||||
<?= $isResult ? '' : '–' ?>
|
<?= $isResult ? '' : '–' ?>
|
||||||
|
|
@ -117,12 +120,12 @@ $fmtKz = fn($v) => $v !== null ? number_format((float)$v, 2, ',', '.') . ' €'
|
||||||
<table style="width:100%;font-size:.9rem">
|
<table style="width:100%;font-size:.9rem">
|
||||||
<tr>
|
<tr>
|
||||||
<td style="padding:.35rem 0;color:var(--color-muted)">Umsatz 19 % (Kz 81)</td>
|
<td style="padding:.35rem 0;color:var(--color-muted)">Umsatz 19 % (Kz 81)</td>
|
||||||
<td class="td-right td-mono"><?= $fmtKz($kz81val) ?></td>
|
<td class="td-right td-mono"><?= $fmtKzGanz($kz81val) ?></td>
|
||||||
</tr>
|
</tr>
|
||||||
<?php if ($kz89val != 0.0): ?>
|
<?php if ($kz89val != 0.0): ?>
|
||||||
<tr>
|
<tr>
|
||||||
<td style="padding:.35rem 0;color:var(--color-muted)">ig. Erwerb 19 % (Kz 89)</td>
|
<td style="padding:.35rem 0;color:var(--color-muted)">ig. Erwerb 19 % (Kz 89)</td>
|
||||||
<td class="td-right td-mono"><?= $fmtKz($kz89val) ?></td>
|
<td class="td-right td-mono"><?= $fmtKzGanz($kz89val) ?></td>
|
||||||
</tr>
|
</tr>
|
||||||
<?php endif; ?>
|
<?php endif; ?>
|
||||||
<tr>
|
<tr>
|
||||||
|
|
|
||||||
118
tests/31_UstvaFloorKz81_89Test.php
Normal file
118
tests/31_UstvaFloorKz81_89Test.php
Normal file
|
|
@ -0,0 +1,118 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Test 31: Kz 81 und Kz 89 werden auf volle Euro abgerundet (floor).
|
||||||
|
*
|
||||||
|
* Buchungen mit Cent-Beträgen dürfen in der UStVA nur als ganzer Euro-Betrag
|
||||||
|
* erscheinen; die USt wird auf dem abgerundeten Wert berechnet.
|
||||||
|
*/
|
||||||
|
class UstvaFloorKz81_89Test extends TestCase
|
||||||
|
{
|
||||||
|
protected string $testName = 'Test 31: UStVA Kz 81/89 – floor auf volle Euro';
|
||||||
|
|
||||||
|
public function run(): void
|
||||||
|
{
|
||||||
|
$year = (int)date('Y');
|
||||||
|
$month = (int)date('n');
|
||||||
|
$datum = date('Y-m-d');
|
||||||
|
|
||||||
|
$erloesId = $this->findAccountByNr('8400'); // Erlöse 19 % USt → ustva_kz=81
|
||||||
|
$ausgabeId = $this->findKonto('Betriebsausgabe');
|
||||||
|
$zahlungId = $this->findKonto('Zahlungskonto');
|
||||||
|
|
||||||
|
if (!$erloesId || !$ausgabeId || !$zahlungId) {
|
||||||
|
$this->assert(false, 'Benötigte Konten nicht gefunden');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$calc = new \Steuerkram\Services\UstvaCalculator();
|
||||||
|
$kzPre = $calc->calculate($year, $month);
|
||||||
|
|
||||||
|
// Kz-81-Buchung mit Cent-Betrag: 100,77 € netto
|
||||||
|
$belegId = \Steuerkram\Models\Beleg::create([
|
||||||
|
'beleg_nr' => 'TEST-FLOOR81-' . uniqid(),
|
||||||
|
'filename' => 'test-floor81.pdf',
|
||||||
|
'storage_path' => '',
|
||||||
|
'mime_type' => 'application/pdf',
|
||||||
|
'file_hash' => hash('sha256', 'test-floor81-' . uniqid()),
|
||||||
|
'beleg_datum' => $datum,
|
||||||
|
'belegtyp' => 'Ausgangsrechnung',
|
||||||
|
]);
|
||||||
|
\Steuerkram\Models\Booking::create([
|
||||||
|
'beleg_id' => $belegId,
|
||||||
|
'buchungs_datum' => $datum,
|
||||||
|
'buchungstyp' => 'Einnahme',
|
||||||
|
'steuerart' => null,
|
||||||
|
'ust_satz' => 19.0,
|
||||||
|
'kategorie_konto_id' => $erloesId,
|
||||||
|
'zahlungskonto_id' => $zahlungId,
|
||||||
|
'betrag_netto' => 100.77,
|
||||||
|
'buchungstext' => 'Test Floor Kz 81',
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Kz-89-Buchung mit Cent-Betrag: 200,55 € netto (ig. Erwerb 19 %)
|
||||||
|
$belegId2 = \Steuerkram\Models\Beleg::create([
|
||||||
|
'beleg_nr' => 'TEST-FLOOR89-' . uniqid(),
|
||||||
|
'filename' => 'test-floor89.pdf',
|
||||||
|
'storage_path' => '',
|
||||||
|
'mime_type' => 'application/pdf',
|
||||||
|
'file_hash' => hash('sha256', 'test-floor89-' . uniqid()),
|
||||||
|
'beleg_datum' => $datum,
|
||||||
|
'belegtyp' => 'Eingangsrechnung',
|
||||||
|
]);
|
||||||
|
\Steuerkram\Models\Booking::create([
|
||||||
|
'beleg_id' => $belegId2,
|
||||||
|
'buchungs_datum' => $datum,
|
||||||
|
'buchungstyp' => 'Ausgabe',
|
||||||
|
'steuerart' => 'ig_erwerb_19',
|
||||||
|
'ust_satz' => 0.0,
|
||||||
|
'kategorie_konto_id' => $ausgabeId,
|
||||||
|
'zahlungskonto_id' => $zahlungId,
|
||||||
|
'betrag_netto' => 200.55,
|
||||||
|
'buchungstext' => 'Test Floor Kz 89',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$kz = $calc->calculate($year, $month);
|
||||||
|
|
||||||
|
// Kz 81: Delta muss exakt 100,00 sein (floor von 100,77)
|
||||||
|
$delta81 = round((float)$kz['81']['betrag'] - (float)$kzPre['81']['betrag'], 2);
|
||||||
|
$this->assertEquals(100.00, $delta81, 'Kz 81: floor(100,77) = 100,00');
|
||||||
|
|
||||||
|
// USt Kz 81: 100 * 0,19 = 19,00 (nicht 100,77 * 0,19 = 19,15)
|
||||||
|
$deltaUst81 = round((float)$kz['ust81']['steuer'] - (float)$kzPre['ust81']['steuer'], 2);
|
||||||
|
$this->assertEquals(19.00, $deltaUst81, 'USt Kz 81: 100 × 0,19 = 19,00');
|
||||||
|
|
||||||
|
// Kz 89: Delta muss exakt 200,00 sein (floor von 200,55)
|
||||||
|
$delta89 = round((float)$kz['89']['betrag'] - (float)$kzPre['89']['betrag'], 2);
|
||||||
|
$this->assertEquals(200.00, $delta89, 'Kz 89: floor(200,55) = 200,00');
|
||||||
|
|
||||||
|
// USt Kz 89: 200 * 0,19 = 38,00 (nicht 200,55 * 0,19 = 38,10)
|
||||||
|
$deltaUst89 = round((float)$kz['ust89']['steuer'] - (float)$kzPre['ust89']['steuer'], 2);
|
||||||
|
$this->assertEquals(38.00, $deltaUst89, 'USt Kz 89: 200 × 0,19 = 38,00');
|
||||||
|
|
||||||
|
// ganze_euro-Flag muss gesetzt sein
|
||||||
|
$this->assert($kz['81']['ganze_euro'] === true, 'Kz 81 hat ganze_euro=true');
|
||||||
|
$this->assert($kz['89']['ganze_euro'] === true, 'Kz 89 hat ganze_euro=true');
|
||||||
|
$this->assert($kz['ust81']['ganze_euro'] === false, 'ust81 hat ganze_euro=false');
|
||||||
|
}
|
||||||
|
|
||||||
|
private function findKonto(string $kontenart): ?int
|
||||||
|
{
|
||||||
|
$row = \Steuerkram\Database::fetchOne(
|
||||||
|
"SELECT id FROM accounts WHERE kontenart = ? AND aktiv = 1 LIMIT 1",
|
||||||
|
[$kontenart]
|
||||||
|
);
|
||||||
|
return $row ? (int)$row['id'] : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function findAccountByNr(string $nr): ?int
|
||||||
|
{
|
||||||
|
$row = \Steuerkram\Database::fetchOne(
|
||||||
|
"SELECT id FROM accounts WHERE kontonummer = ? AND aktiv = 1 LIMIT 1",
|
||||||
|
[$nr]
|
||||||
|
);
|
||||||
|
return $row ? (int)$row['id'] : null;
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Add table
Reference in a new issue