Fix #6: Auslandsumsätze und ig. Erwerb korrekt in UStVA abbilden
This commit is contained in:
parent
2516623143
commit
7609d1e4e3
7 changed files with 393 additions and 42 deletions
12
migrations/006_steuerart.sql
Normal file
12
migrations/006_steuerart.sql
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
-- Migration 006: steuerart für Auslandsbuchungen
|
||||
-- Unterscheidet Buchungsarten für USt-freie Auslandsumsätze (UStVA-Pflicht)
|
||||
|
||||
ALTER TABLE bookings
|
||||
ADD COLUMN steuerart ENUM(
|
||||
'normal', -- Standard-Inlandsgeschäft
|
||||
'ig_lieferung', -- Steuerfreie ig. Lieferung § 4 Nr. 1b (Kz 41)
|
||||
'export', -- Ausfuhr § 4 Nr. 1a (Kz 44)
|
||||
'ig_erwerb_19', -- Innergemeinschaftlicher Erwerb 19 % (Kz 89/91)
|
||||
'ig_erwerb_7' -- Innergemeinschaftlicher Erwerb 7 % (Kz 93/94)
|
||||
) NOT NULL DEFAULT 'normal'
|
||||
AFTER ust_satz;
|
||||
|
|
@ -96,11 +96,16 @@ class Booking
|
|||
|
||||
$nr = self::generateBuchungsNr();
|
||||
|
||||
$steuerart = $data['steuerart'] ?? 'normal';
|
||||
if (!in_array($steuerart, ['normal','ig_lieferung','export','ig_erwerb_19','ig_erwerb_7'], true)) {
|
||||
$steuerart = 'normal';
|
||||
}
|
||||
|
||||
Database::execute(
|
||||
'INSERT INTO bookings (buchungs_nr, beleg_id, buchungs_datum, buchungstyp,
|
||||
kategorie_konto_id, zahlungskonto_id,
|
||||
betrag_netto, ust_satz, betrag_ust, betrag_brutto, buchungstext, fiscal_year_id)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)',
|
||||
betrag_netto, ust_satz, steuerart, betrag_ust, betrag_brutto, buchungstext, fiscal_year_id)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)',
|
||||
[
|
||||
$nr,
|
||||
(int)$data['beleg_id'],
|
||||
|
|
@ -110,6 +115,7 @@ class Booking
|
|||
(int)$data['zahlungskonto_id'],
|
||||
$netto,
|
||||
(float)($data['ust_satz'] ?? 0),
|
||||
$steuerart,
|
||||
$ust,
|
||||
$brutto,
|
||||
$data['buchungstext'],
|
||||
|
|
|
|||
|
|
@ -153,17 +153,52 @@ class ElsterXmlService
|
|||
$kzEl = $dom->createElement('Kz35', (string)round($kz['35']['betrag']));
|
||||
$ustva->appendChild($kzEl);
|
||||
}
|
||||
// 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);
|
||||
|
|
|
|||
|
|
@ -9,70 +9,120 @@ use Steuerkram\Database;
|
|||
/**
|
||||
* Berechnet alle ELSTER-Kennzahlen (Kz) für die Umsatzsteuervoranmeldung.
|
||||
*
|
||||
* Maßgeblich sind buchungstyp, ust_satz und betrag_ust der Buchung selbst.
|
||||
* Maßgeblich sind buchungstyp, steuerart, ust_satz und betrag_ust der Buchung.
|
||||
*
|
||||
* Buchungslogik:
|
||||
* Umsatzsteuer (Kz 81/86): buchungstyp = 'Einnahme' mit ust_satz > 0
|
||||
* Vorsteuer (Kz 61/62): buchungstyp = 'Ausgabe' mit ust_satz > 0
|
||||
* Steuerfreie Umsätze (Kz 35): buchungstyp = 'Einnahme' mit ust_satz = 0
|
||||
* 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
|
||||
*/
|
||||
class UstvaCalculator
|
||||
{
|
||||
public function calculate(int $year, int $month): array
|
||||
{
|
||||
$kz = [
|
||||
'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 EU-Lieferungen (§ 4 Nr. 1b)', 'betrag' => 0.0, 'steuer' => null],
|
||||
'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],
|
||||
'61' => ['name' => 'Vorsteuer aus Eingangsrechnungen 19 %', 'betrag' => null, 'steuer' => 0.0],
|
||||
'62' => ['name' => 'Vorsteuer aus Eingangsrechnungen 7 %', 'betrag' => null, 'steuer' => 0.0],
|
||||
'66' => ['name' => 'Vorsteuerbeträge gesamt', 'betrag' => null, 'steuer' => 0.0],
|
||||
'69' => ['name' => 'Verbleibende USt-Vorauszahlung/Erstattung', 'betrag' => null, 'steuer' => 0.0],
|
||||
// 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],
|
||||
];
|
||||
|
||||
$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'];
|
||||
|
||||
// ── Umsatzsteuer (Einnahmen) ─────────────────────────────
|
||||
if ($buchungstyp === 'Einnahme' && $ustSatz > 0) {
|
||||
if (abs($ustSatz - 19) < 0.01) {
|
||||
$kz['81']['betrag'] += $betragNetto;
|
||||
} elseif (abs($ustSatz - 7) < 0.01) {
|
||||
$kz['86']['betrag'] += $betragNetto;
|
||||
// ── 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Steuerfreie Umsätze: Einnahme mit ust_satz = 0
|
||||
if ($buchungstyp === 'Einnahme' && $ustSatz == 0) {
|
||||
$kz['35']['betrag'] += $betragNetto;
|
||||
}
|
||||
|
||||
// ── Vorsteuer (Ausgaben) ─────────────────────────────────
|
||||
if ($buchungstyp === 'Ausgabe' && $ustSatz > 0 && $betragUst > 0) {
|
||||
if (abs($ustSatz - 19) < 0.01) {
|
||||
$kz['61']['steuer'] += $betragUst;
|
||||
} elseif (abs($ustSatz - 7) < 0.01) {
|
||||
$kz['62']['steuer'] += $betragUst;
|
||||
// ── 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
$kz['66']['steuer'] += $betragUst;
|
||||
}
|
||||
}
|
||||
|
||||
// Abgeleitete Kennzahlen
|
||||
// ── Abgeleitete Kennzahlen ───────────────────────────────────────
|
||||
$kz['83']['steuer'] = round($kz['81']['betrag'] * 0.19, 2);
|
||||
$kz['84']['steuer'] = round($kz['86']['betrag'] * 0.07, 2);
|
||||
|
||||
$ustGesamt = $kz['83']['steuer'] + $kz['84']['steuer'];
|
||||
$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);
|
||||
|
|
@ -84,7 +134,7 @@ class UstvaCalculator
|
|||
private function fetchBookings(int $year, int $month): array
|
||||
{
|
||||
return Database::fetchAll(
|
||||
'SELECT bk.buchungstyp, bk.ust_satz, bk.betrag_netto, bk.betrag_ust
|
||||
'SELECT bk.buchungstyp, bk.steuerart, bk.ust_satz, bk.betrag_netto, bk.betrag_ust
|
||||
FROM bookings bk
|
||||
JOIN fiscal_years fy ON bk.fiscal_year_id = fy.id
|
||||
WHERE fy.year = ? AND MONTH(bk.buchungs_datum) = ?
|
||||
|
|
|
|||
|
|
@ -290,7 +290,7 @@ $testMode = Setting::isTestMode();
|
|||
</div>
|
||||
<div class="form-group">
|
||||
<label>USt-Satz (%)</label>
|
||||
<select name="ust_satz">
|
||||
<select name="ust_satz" id="ust_satz_select" onchange="updateSteuerartVisibility()">
|
||||
<?php foreach (['0','7','19'] as $s): ?>
|
||||
<option value="<?= $s ?>"<?= ($formData['ust_satz'] ?? '19') === $s ? ' selected' : '' ?>><?= $s ?> %</option>
|
||||
<?php endforeach; ?>
|
||||
|
|
@ -298,6 +298,28 @@ $testMode = Setting::isTestMode();
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Steuerart (für Auslandsumsätze) -->
|
||||
<div class="form-group" id="steuerart_group">
|
||||
<label>Steuerart (Ausland / EU)</label>
|
||||
<select name="steuerart" id="steuerart_select">
|
||||
<?php
|
||||
$steuerartOptionen = [
|
||||
'normal' => 'Normal (Inland)',
|
||||
'ig_lieferung' => 'Steuerfreie ig. Lieferung § 4 Nr. 1b (Kz 41)',
|
||||
'export' => 'Steuerfreie Ausfuhr § 4 Nr. 1a (Kz 44)',
|
||||
'ig_erwerb_19' => 'Innergemeinschaftlicher Erwerb 19 % (Kz 89/91)',
|
||||
'ig_erwerb_7' => 'Innergemeinschaftlicher Erwerb 7 % (Kz 93/94)',
|
||||
];
|
||||
$currentSteuerart = $formData['steuerart'] ?? 'normal';
|
||||
foreach ($steuerartOptionen as $val => $label): ?>
|
||||
<option value="<?= $val ?>"<?= $currentSteuerart === $val ? ' selected' : '' ?>><?= $label ?></option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
<p class="text-muted text-sm" style="margin-top:.25rem">
|
||||
Nur relevant bei USt-Satz 0 % für Auslandsumsätze und EU-Erwerbe.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- Kategorie-Konto -->
|
||||
<div class="form-group">
|
||||
<label for="kategorie_konto_id">Kategorie *
|
||||
|
|
@ -406,5 +428,20 @@ function filterKategorieKonten(buchungstyp) {
|
|||
document.addEventListener('DOMContentLoaded', function() {
|
||||
const checked = document.querySelector('input[name="buchungstyp"]:checked');
|
||||
if (checked) filterKategorieKonten(checked.value);
|
||||
updateSteuerartVisibility();
|
||||
});
|
||||
|
||||
function updateSteuerartVisibility() {
|
||||
const ustSatz = document.getElementById('ust_satz_select');
|
||||
const steuerartGroup = document.getElementById('steuerart_group');
|
||||
const steuerartSelect = document.getElementById('steuerart_select');
|
||||
if (!ustSatz || !steuerartGroup) return;
|
||||
|
||||
if (ustSatz.value === '0') {
|
||||
steuerartGroup.style.display = '';
|
||||
} else {
|
||||
steuerartGroup.style.display = 'none';
|
||||
if (steuerartSelect) steuerartSelect.value = 'normal';
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
|
|
|||
|
|
@ -30,10 +30,11 @@ $fmtKz = fn($v) => $v !== null ? number_format((float)$v, 2, ',', '.') . ' €'
|
|||
<tbody>
|
||||
<?php
|
||||
$sections = [
|
||||
'Steuerpflichtige Umsätze' => ['81', '86', '35', '41', '44'],
|
||||
'Umsatzsteuer auf Umsätze' => ['83', '84'],
|
||||
'Vorsteuerbeträge' => ['66', '61', '62'],
|
||||
'Verbleibende Zahllast' => ['69'],
|
||||
'Steuerpflichtige Umsätze' => ['81', '86', '35', '41', '44'],
|
||||
'Umsatzsteuer auf Umsätze' => ['83', '84'],
|
||||
'Innergemeinschaftlicher Erwerb' => ['89', '91', '93', '94'],
|
||||
'Vorsteuerbeträge' => ['61', '62', '67', '66'],
|
||||
'Verbleibende Zahllast' => ['69'],
|
||||
];
|
||||
foreach ($sections as $sectionTitle => $kzNrs):
|
||||
?>
|
||||
|
|
|
|||
210
tests/07_UstvaAuslandTest.php
Normal file
210
tests/07_UstvaAuslandTest.php
Normal file
|
|
@ -0,0 +1,210 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* Test 7: UStVA-Kennzahlen für Auslandsumsätze und ig. Erwerb.
|
||||
*
|
||||
* Prüft, dass steuerart-Buchungen korrekt in die UStVA-Kennzahlen
|
||||
* 41, 44, 89, 91, 93, 94 und 67 einfließen.
|
||||
*/
|
||||
class UstvaAuslandTest extends TestCase
|
||||
{
|
||||
protected string $testName = 'Test 7: UStVA Auslandsumsätze und ig. Erwerb';
|
||||
|
||||
public function run(): void
|
||||
{
|
||||
$year = (int)date('Y');
|
||||
$month = (int)date('n');
|
||||
$datum = date('Y-m-d');
|
||||
|
||||
// Konten aus SKR03
|
||||
$erloesId = $this->findKonto('Betriebseinnahme');
|
||||
$ausgabeId = $this->findKonto('Betriebsausgabe');
|
||||
$zahlungId = $this->findKonto('Zahlungskonto');
|
||||
|
||||
if (!$erloesId || !$ausgabeId || !$zahlungId) {
|
||||
$this->assert(false, 'Benötigte Konten nicht gefunden – SKR03-Seed vorhanden?');
|
||||
return;
|
||||
}
|
||||
$this->assert(true, 'Konten vorhanden');
|
||||
|
||||
// Zweites Zahlungskonto für Ausgabe-Buchungen
|
||||
$bankId = \Steuerkram\Database::fetchOne(
|
||||
"SELECT id FROM accounts WHERE kontenart = 'Zahlungskonto' AND aktiv = 1 AND id != ? LIMIT 1",
|
||||
[$zahlungId]
|
||||
);
|
||||
$bankId = $bankId ? (int)$bankId['id'] : $zahlungId;
|
||||
|
||||
// ── 1. Steuerfreie ig. Lieferung → Kz 41 ──────────────────────────
|
||||
$belegIgL = \Steuerkram\Models\Beleg::create([
|
||||
'beleg_nr' => 'TEST-IGL-' . uniqid(),
|
||||
'filename' => 'test-igl.pdf',
|
||||
'storage_path' => '',
|
||||
'mime_type' => 'application/pdf',
|
||||
'file_hash' => hash('sha256', 'test-igl'),
|
||||
'beleg_datum' => $datum,
|
||||
'eingangs_datum' => $datum,
|
||||
'belegtyp' => 'Ausgangsrechnung',
|
||||
]);
|
||||
\Steuerkram\Models\Booking::create([
|
||||
'beleg_id' => $belegIgL,
|
||||
'buchungs_datum' => $datum,
|
||||
'buchungstyp' => 'Einnahme',
|
||||
'steuerart' => 'ig_lieferung',
|
||||
'kategorie_konto_id' => $erloesId,
|
||||
'zahlungskonto_id' => $zahlungId,
|
||||
'betrag_netto' => 1000.00,
|
||||
'ust_satz' => 0.0,
|
||||
'buchungstext' => 'Test ig. Lieferung EU',
|
||||
]);
|
||||
|
||||
// ── 2. Steuerfreie Ausfuhr → Kz 44 ───────────────────────────────
|
||||
$belegExp = \Steuerkram\Models\Beleg::create([
|
||||
'beleg_nr' => 'TEST-EXP-' . uniqid(),
|
||||
'filename' => 'test-exp.pdf',
|
||||
'storage_path' => '',
|
||||
'mime_type' => 'application/pdf',
|
||||
'file_hash' => hash('sha256', 'test-exp'),
|
||||
'beleg_datum' => $datum,
|
||||
'eingangs_datum' => $datum,
|
||||
'belegtyp' => 'Ausgangsrechnung',
|
||||
]);
|
||||
\Steuerkram\Models\Booking::create([
|
||||
'beleg_id' => $belegExp,
|
||||
'buchungs_datum' => $datum,
|
||||
'buchungstyp' => 'Einnahme',
|
||||
'steuerart' => 'export',
|
||||
'kategorie_konto_id' => $erloesId,
|
||||
'zahlungskonto_id' => $zahlungId,
|
||||
'betrag_netto' => 2000.00,
|
||||
'ust_satz' => 0.0,
|
||||
'buchungstext' => 'Test Ausfuhr Drittland',
|
||||
]);
|
||||
|
||||
// ── 3. Ig. Erwerb 19 % → Kz 89/91/67 ─────────────────────────────
|
||||
$belegIgE19 = \Steuerkram\Models\Beleg::create([
|
||||
'beleg_nr' => 'TEST-IGE19-' . uniqid(),
|
||||
'filename' => 'test-ige19.pdf',
|
||||
'storage_path' => '',
|
||||
'mime_type' => 'application/pdf',
|
||||
'file_hash' => hash('sha256', 'test-ige19'),
|
||||
'beleg_datum' => $datum,
|
||||
'eingangs_datum' => $datum,
|
||||
'belegtyp' => 'Eingangsrechnung',
|
||||
]);
|
||||
\Steuerkram\Models\Booking::create([
|
||||
'beleg_id' => $belegIgE19,
|
||||
'buchungs_datum' => $datum,
|
||||
'buchungstyp' => 'Ausgabe',
|
||||
'steuerart' => 'ig_erwerb_19',
|
||||
'kategorie_konto_id' => $ausgabeId,
|
||||
'zahlungskonto_id' => $bankId,
|
||||
'betrag_netto' => 500.00,
|
||||
'ust_satz' => 0.0,
|
||||
'buchungstext' => 'Test ig. Erwerb 19%',
|
||||
]);
|
||||
|
||||
// ── 4. Ig. Erwerb 7 % → Kz 93/94/67 ──────────────────────────────
|
||||
$belegIgE7 = \Steuerkram\Models\Beleg::create([
|
||||
'beleg_nr' => 'TEST-IGE7-' . uniqid(),
|
||||
'filename' => 'test-ige7.pdf',
|
||||
'storage_path' => '',
|
||||
'mime_type' => 'application/pdf',
|
||||
'file_hash' => hash('sha256', 'test-ige7'),
|
||||
'beleg_datum' => $datum,
|
||||
'eingangs_datum' => $datum,
|
||||
'belegtyp' => 'Eingangsrechnung',
|
||||
]);
|
||||
\Steuerkram\Models\Booking::create([
|
||||
'beleg_id' => $belegIgE7,
|
||||
'buchungs_datum' => $datum,
|
||||
'buchungstyp' => 'Ausgabe',
|
||||
'steuerart' => 'ig_erwerb_7',
|
||||
'kategorie_konto_id' => $ausgabeId,
|
||||
'zahlungskonto_id' => $bankId,
|
||||
'betrag_netto' => 300.00,
|
||||
'ust_satz' => 0.0,
|
||||
'buchungstext' => 'Test ig. Erwerb 7%',
|
||||
]);
|
||||
|
||||
// ── UStVA berechnen und Kennzahlen prüfen ─────────────────────────
|
||||
$calc = new \Steuerkram\Services\UstvaCalculator();
|
||||
$kz = $calc->calculate($year, $month);
|
||||
|
||||
// Kz 41: ig. Lieferung 1000 €
|
||||
$this->assert(
|
||||
(float)$kz['41']['betrag'] >= 1000.00,
|
||||
'Kz 41 (ig. Lieferung): Betrag ≥ 1.000 €'
|
||||
);
|
||||
|
||||
// Kz 44: Ausfuhr 2000 €
|
||||
$this->assert(
|
||||
(float)$kz['44']['betrag'] >= 2000.00,
|
||||
'Kz 44 (Ausfuhr): Betrag ≥ 2.000 €'
|
||||
);
|
||||
|
||||
// Kz 89: ig. Erwerb 19% Bemessungsgrundlage 500 €
|
||||
$this->assert(
|
||||
(float)$kz['89']['betrag'] >= 500.00,
|
||||
'Kz 89 (ig. Erwerb 19% Bmgl.): Betrag ≥ 500 €'
|
||||
);
|
||||
|
||||
// Kz 91: USt auf ig. Erwerb 19% = 500 * 0.19 = 95 €
|
||||
$this->assertEquals(
|
||||
round(500.00 * 0.19, 2),
|
||||
(float)$kz['91']['steuer'],
|
||||
'Kz 91 (USt ig. Erwerb 19%): Steuer = 95,00 €'
|
||||
);
|
||||
|
||||
// Kz 93: ig. Erwerb 7% Bemessungsgrundlage 300 €
|
||||
$this->assert(
|
||||
(float)$kz['93']['betrag'] >= 300.00,
|
||||
'Kz 93 (ig. Erwerb 7% Bmgl.): Betrag ≥ 300 €'
|
||||
);
|
||||
|
||||
// Kz 94: USt auf ig. Erwerb 7% = 300 * 0.07 = 21 €
|
||||
$this->assertEquals(
|
||||
round(300.00 * 0.07, 2),
|
||||
(float)$kz['94']['steuer'],
|
||||
'Kz 94 (USt ig. Erwerb 7%): Steuer = 21,00 €'
|
||||
);
|
||||
|
||||
// Kz 67: Vorsteuer aus ig. Erwerb = Kz 91 + Kz 94 = 116 €
|
||||
$erwartetKz67 = round((float)$kz['91']['steuer'] + (float)$kz['94']['steuer'], 2);
|
||||
$this->assertEquals(
|
||||
$erwartetKz67,
|
||||
(float)$kz['67']['steuer'],
|
||||
'Kz 67 (Vorsteuer ig. Erwerb) = Kz 91 + Kz 94'
|
||||
);
|
||||
|
||||
// Kz 69 Plausibilitätsprüfung: ig. Erwerb neutralisiert sich durch Kz 67
|
||||
// USt auf Umsätze (83+84) + ig. Erwerb (91+94) - Vorsteuer (66) = 69
|
||||
$ust = (float)$kz['83']['steuer'] + (float)$kz['84']['steuer']
|
||||
+ (float)$kz['91']['steuer'] + (float)$kz['94']['steuer'];
|
||||
$erwartetKz69 = round($ust - (float)$kz['66']['steuer'], 2);
|
||||
$this->assertEquals(
|
||||
$erwartetKz69,
|
||||
(float)$kz['69']['steuer'],
|
||||
'Kz 69 (Zahllast) korrekt berechnet'
|
||||
);
|
||||
|
||||
// ig. Erwerb darf NICHT in Kz 35 oder Kz 61/62 landen
|
||||
// (kz35 enthält nur normale 0%-Einnahmen, kz61 nur normale Vorsteuer)
|
||||
// Wir buchten keine normalen 0%-Einnahmen, also muss Kz 35 = 0 sein
|
||||
$this->assertEquals(
|
||||
0.0,
|
||||
(float)$kz['35']['betrag'],
|
||||
'Kz 35 (steuerfreie Inlandsumsätze) = 0 (kein normaler 0%-Umsatz gebucht)'
|
||||
);
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Reference in a new issue