Feature: ig. Erwerb 19 % ReverseCharge via Kindzeilen 1574/1774
This commit is contained in:
parent
067f1734be
commit
d05e9991bd
8 changed files with 329 additions and 115 deletions
10
Changes.md
10
Changes.md
|
|
@ -2,6 +2,16 @@
|
|||
|
||||
---
|
||||
|
||||
## 2026-05-10 – Feature: ig. Erwerb 19 % ReverseCharge über Kindzeilen
|
||||
|
||||
- Migration 047: ustva_kz='91' für Konto 1774; Kindzeilen (1574/1774) für bestehende ig_erwerb_19-Buchungen
|
||||
- `Booking::create()`: steuerart=ig_erwerb_19 erzeugt zwei Kindzeilen – VSt auf 1574 (Kz 67), USt auf 1774 (Kz 91)
|
||||
- `UstvaCalculator`: Kz 67/91 jetzt kontobasiert via 1574/1774; Kz 93/94 (ig_erwerb_7) entfernt
|
||||
- Buchungsformular: Option „ig. Erwerb 7 %" entfernt
|
||||
- Test 7 auf neue Logik angepasst; Test 30 neu
|
||||
|
||||
---
|
||||
|
||||
## 2026-05-10 – Feature: USt-Buchungszeilen für Ausgangsrechnungen
|
||||
|
||||
- Migration 046: Konten 1771 (USt 7 %) und 1776 (USt 19 %) erhalten `ustva_kz='84'`/'83'`; bestehende Einnahme-Buchungen mit USt erhalten nachträglich USt-Kindzeilen
|
||||
|
|
|
|||
|
|
@ -55,15 +55,23 @@ Diese Berechnung wird ausschließlich für **monatliche** Voranmeldungen unterst
|
|||
**Quelle 1 – kontobasiert** (`accounts.ustva_kz` des Kategorie-Kontos)
|
||||
SKR03-Erlös- und Steuerkonten tragen ihre Kennzahl direkt am Konto. Buchungen mit gefülltem `bookings.steuerart` werden aus dieser Quelle ausgeschlossen, um Doppelzählung mit Quelle 3 zu vermeiden. Konten mit `ustva_kz='61'` oder `'62'` werden **nicht mehr** kontobasiert ausgewertet (siehe Quelle 2).
|
||||
|
||||
**Quelle 2 – Steuerkonten-Buchungszeilen** (Kz 61/62 via 1571/1576, Kz 83/84 via 1771/1776)
|
||||
Jede Ausgabe-Buchung mit 7/19 % und `steuerart=normal` erzeugt automatisch eine VSt-Kindzeile auf Konto 1571 (7 %) bzw. 1576 (19 %); jede Einnahme-Buchung entsprechend eine USt-Kindzeile auf 1771 (7 %) bzw. 1776 (19 %). Kz 61/62 und Kz 83/84 werden aus `betrag_netto` dieser Kindzeilen aggregiert. Maßgeblich ist der tatsächliche USt-Satz der Buchung, nicht das Erlöskonto. EU-Buchungen (`steuerart` ∈ {ig_lieferung, export, ig_erwerb_19, ig_erwerb_7}) erhalten keine Steuerkonten-Kindzeilen.
|
||||
**Quelle 2 – Steuerkonten-Buchungszeilen** (Kz 61/62/67/83/84/91 via Steuerkonten)
|
||||
Normale Buchungen mit 7/19 % erzeugen automatisch Kindzeilen auf den zugehörigen Steuerkonten. Buchungen mit `steuerart=ig_erwerb_19` erzeugen zwei Kindzeilen auf 1574 (Kz 67) und 1774 (Kz 91) – jeweils `netto × 0,19`. Alle anderen EU-Steuerarten erhalten keine Kindzeilen.
|
||||
|
||||
| Buchungstyp | Steuersatz | Kindzeile Konto | Kennzahl |
|
||||
|---|---|---|---|
|
||||
| Ausgabe, steuerart=normal | 19 % | 1576 | Kz 61 |
|
||||
| Ausgabe, steuerart=normal | 7 % | 1571 | Kz 62 |
|
||||
| Einnahme, steuerart=normal | 19 % | 1776 | Kz 83 |
|
||||
| Einnahme, steuerart=normal | 7 % | 1771 | Kz 84 |
|
||||
| Ausgabe, steuerart=ig_erwerb_19 | – | 1574 (netto×0,19) | Kz 67 |
|
||||
| Ausgabe, steuerart=ig_erwerb_19 | – | 1774 (netto×0,19) | Kz 91 |
|
||||
|
||||
**Quelle 3 – steuerartbasiert** (`bookings.steuerart`)
|
||||
Auslandssachverhalte unabhängig vom gebuchten Konto:
|
||||
- `ig_lieferung` → Kz 41 (steuerfreie ig. Lieferung, § 4 Nr. 1b UStG)
|
||||
- `export` → Kz 44 (steuerfreie Ausfuhr in Drittland, § 4 Nr. 1a UStG)
|
||||
- `ig_erwerb_19` → Kz 89 (Bemessungsgrundlage) + Kz 91 (USt = 19 %)
|
||||
- `ig_erwerb_7` → Kz 93 (Bemessungsgrundlage) + Kz 94 (USt = 7 %)
|
||||
- `ig_erwerb_19` → Kz 89 (Bemessungsgrundlage) – Kz 91/67 laufen über Kindzeilen (Quelle 2)
|
||||
|
||||
### Implementierte Kennzahlen
|
||||
|
||||
|
|
@ -77,14 +85,12 @@ Auslandssachverhalte unabhängig vom gebuchten Konto:
|
|||
| **41** | Steuerfreie ig. Lieferungen mit USt-IdNr. | § 4 Nr. 1b i.V.m. § 6a UStG | Konto + Steuerart | `ustva_kz='41'` ODER `steuerart='ig_lieferung'` |
|
||||
| **44** | Steuerfreie Ausfuhrlieferungen Drittland | § 4 Nr. 1a i.V.m. § 6 UStG | Konto + Steuerart | `ustva_kz='44'` ODER `steuerart='export'` |
|
||||
| **89** | Ig. Erwerb 19 % – Bemessungsgrundlage | § 1a UStG, § 12 Abs. 1 UStG | Steuerart | `SUM(betrag_netto)` WHERE `steuerart='ig_erwerb_19'` |
|
||||
| **91** | USt auf ig. Erwerb 19 % | § 1a UStG | berechnet | `Kz 89 × 0,19` |
|
||||
| **93** | Ig. Erwerb 7 % – Bemessungsgrundlage | § 1a UStG, § 12 Abs. 2 UStG | Steuerart | `SUM(betrag_netto)` WHERE `steuerart='ig_erwerb_7'` |
|
||||
| **94** | USt auf ig. Erwerb 7 % | § 1a UStG | berechnet | `Kz 93 × 0,07` |
|
||||
| **91** | USt auf ig. Erwerb 19 % | § 1a UStG | Steuerkonto | `SUM(betrag_netto)` WHERE `ustva_kz='91'` (Konto 1774) |
|
||||
| **61** | Vorsteuerbeträge aus Eingangsrechnungen 19 % | § 15 Abs. 1 Nr. 1 UStG | Steuerkonto | `SUM(betrag_netto)` WHERE `ustva_kz='61'` (Konto 1576) |
|
||||
| **62** | Vorsteuerbeträge aus Eingangsrechnungen 7 % | § 15 Abs. 1 Nr. 1 UStG | Steuerkonto | `SUM(betrag_netto)` WHERE `ustva_kz='62'` (Konto 1571) |
|
||||
| **67** | Vorsteuer aus innergemeinschaftlichem Erwerb | § 15 Abs. 1 Nr. 3 UStG | abgeleitet | `Kz 91 + Kz 94` |
|
||||
| **67** | Vorsteuer aus innergemeinschaftlichem Erwerb | § 15 Abs. 1 Nr. 3 UStG | Steuerkonto | `SUM(betrag_netto)` WHERE `ustva_kz='67'` (Konto 1574) |
|
||||
| **66** | Vorsteuerbeträge gesamt | § 15 UStG | abgeleitet | `Kz 61 + Kz 62 + Kz 67` |
|
||||
| **69** | Verbleibende USt-Vorauszahlung / Erstattung | § 18 Abs. 1 UStG | abgeleitet | `(Kz 83 + Kz 84 + Kz 91 + Kz 94) − Kz 66` |
|
||||
| **69** | Verbleibende USt-Vorauszahlung / Erstattung | § 18 Abs. 1 UStG | abgeleitet | `(Kz 83 + Kz 84 + Kz 91) − Kz 66` |
|
||||
|
||||
> **Hinweis zur Bezeichnung Kz 61/62:** Die offizielle ELSTER-Bezeichnung „Kz 61 = Vorsteuer aus innergemeinschaftlichem Erwerb" (§ 15 Abs. 1 Nr. 3 UStG) und „Kz 62 = Einfuhrumsatzsteuer" (§ 15 Abs. 1 Nr. 2 UStG) wird hier abweichend für Inlands-VSt 19 % bzw. 7 % verwendet. Diese Konvention stammt aus den frühen Migrationen und wurde nie auf die offiziellen Kz 66 (Inlandsvorsteuer 19 %) und Kz 66 (Inlandsvorsteuer 7 %) zurückgeführt. Für die ELSTER-Übertragung ist das unkritisch, weil das XML alle Vorsteuerwerte rechnerisch in Kz 66 zusammenfasst – materiell-rechtlich sollte dieser Punkt aber bei der nächsten Refaktorierung geprüft werden (siehe „Offene Punkte"). Migration 017 dokumentiert die Diskrepanz und fixt sie für VSt-Konten teilweise; Migration 022 hat sie für die buchungsbasierte Auswertung bewusst wieder eingeführt.
|
||||
|
||||
|
|
|
|||
82
migrations/047_ige_rc_buchungszeilen.sql
Normal file
82
migrations/047_ige_rc_buchungszeilen.sql
Normal file
|
|
@ -0,0 +1,82 @@
|
|||
-- Migration 047: ig. Erwerb 19 % ReverseCharge über Kindzeilen (1574/1774)
|
||||
--
|
||||
-- Hintergrund: Bisher wurden Kz 91 (USt ig.E.) und Kz 67 (VSt ig.E.) durch den
|
||||
-- UstvaCalculator aus betrag_netto × 0,19 der Hauptbuchung berechnet.
|
||||
-- Ab jetzt erzeugt Booking::create() bei steuerart='ig_erwerb_19' zwei Kindzeilen:
|
||||
-- - 1574 (Vorsteuer ig.E. 19 %, ustva_kz='67') für Kz 67
|
||||
-- - 1774 (USt ig.E. 19 %, ustva_kz='91') für Kz 91
|
||||
|
||||
-- ── 1. ustva_kz für 1774 auf '91' setzen ─────────────────────────────────────
|
||||
|
||||
UPDATE accounts SET ustva_kz = '91' WHERE kontonummer = '1774';
|
||||
|
||||
-- ── 2. Kindzeilen für bestehende ig_erwerb_19 Hauptbuchungen anlegen ─────────
|
||||
-- Idempotenz: WHERE NOT EXISTS verhindert Doppel-Einfügung
|
||||
|
||||
-- 2a. VSt-Zeile auf 1574 (Kz 67)
|
||||
INSERT INTO bookings (
|
||||
buchungs_nr, beleg_id, parent_booking_id, buchungs_datum, buchungstyp,
|
||||
kategorie_konto_id, zahlungskonto_id,
|
||||
betrag_netto, ust_satz, steuerart, betrag_ust, betrag_brutto,
|
||||
buchungstext, is_reversal, fiscal_year_id
|
||||
)
|
||||
SELECT
|
||||
CONCAT('IGE-VST-', LPAD(b.id, 8, '0')),
|
||||
b.beleg_id,
|
||||
b.id,
|
||||
b.buchungs_datum,
|
||||
b.buchungstyp,
|
||||
a_vst.id,
|
||||
b.zahlungskonto_id,
|
||||
ROUND(b.betrag_netto * 0.19, 2),
|
||||
0.00,
|
||||
'normal',
|
||||
0.00,
|
||||
ROUND(b.betrag_netto * 0.19, 2),
|
||||
CONCAT('VSt ig.E. zu ', b.buchungs_nr),
|
||||
b.is_reversal,
|
||||
b.fiscal_year_id
|
||||
FROM bookings b
|
||||
JOIN accounts a_vst ON a_vst.kontonummer = '1574'
|
||||
WHERE b.steuerart = 'ig_erwerb_19'
|
||||
AND b.parent_booking_id IS NULL
|
||||
AND b.betrag_netto != 0
|
||||
AND NOT EXISTS (
|
||||
SELECT 1 FROM bookings k
|
||||
JOIN accounts ak ON k.kategorie_konto_id = ak.id
|
||||
WHERE k.parent_booking_id = b.id AND ak.kontonummer = '1574'
|
||||
);
|
||||
|
||||
-- 2b. USt-Zeile auf 1774 (Kz 91)
|
||||
INSERT INTO bookings (
|
||||
buchungs_nr, beleg_id, parent_booking_id, buchungs_datum, buchungstyp,
|
||||
kategorie_konto_id, zahlungskonto_id,
|
||||
betrag_netto, ust_satz, steuerart, betrag_ust, betrag_brutto,
|
||||
buchungstext, is_reversal, fiscal_year_id
|
||||
)
|
||||
SELECT
|
||||
CONCAT('IGE-UST-', LPAD(b.id, 8, '0')),
|
||||
b.beleg_id,
|
||||
b.id,
|
||||
b.buchungs_datum,
|
||||
b.buchungstyp,
|
||||
a_ust.id,
|
||||
b.zahlungskonto_id,
|
||||
ROUND(b.betrag_netto * 0.19, 2),
|
||||
0.00,
|
||||
'normal',
|
||||
0.00,
|
||||
ROUND(b.betrag_netto * 0.19, 2),
|
||||
CONCAT('USt ig.E. zu ', b.buchungs_nr),
|
||||
b.is_reversal,
|
||||
b.fiscal_year_id
|
||||
FROM bookings b
|
||||
JOIN accounts a_ust ON a_ust.kontonummer = '1774'
|
||||
WHERE b.steuerart = 'ig_erwerb_19'
|
||||
AND b.parent_booking_id IS NULL
|
||||
AND b.betrag_netto != 0
|
||||
AND NOT EXISTS (
|
||||
SELECT 1 FROM bookings k
|
||||
JOIN accounts ak ON k.kategorie_konto_id = ak.id
|
||||
WHERE k.parent_booking_id = b.id AND ak.kontonummer = '1774'
|
||||
);
|
||||
|
|
@ -177,6 +177,45 @@ class Booking
|
|||
}
|
||||
}
|
||||
|
||||
// ig. Erwerb 19 % ReverseCharge: Kindzeilen auf 1574 (VSt, Kz 67) und 1774 (USt, Kz 91)
|
||||
if ($data['buchungstyp'] === 'Ausgabe'
|
||||
&& $steuerart === 'ig_erwerb_19'
|
||||
&& abs($netto) > 0.0
|
||||
) {
|
||||
$igeUst = round($netto * 0.19, 2);
|
||||
foreach ([
|
||||
['1574', 'VSt ig.E. zu '],
|
||||
['1774', 'USt ig.E. zu '],
|
||||
] as [$kontonr, $prefix]) {
|
||||
$igeAcc = Account::findByKontonummer($kontonr);
|
||||
if ($igeAcc) {
|
||||
Database::execute(
|
||||
'INSERT INTO bookings (buchungs_nr, beleg_id, parent_booking_id, buchungs_datum, buchungstyp,
|
||||
kategorie_konto_id, zahlungskonto_id,
|
||||
betrag_netto, ust_satz, steuerart, betrag_ust, betrag_brutto,
|
||||
buchungstext, fiscal_year_id)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)',
|
||||
[
|
||||
self::generateBuchungsNr(),
|
||||
(int)$data['beleg_id'],
|
||||
$mainId,
|
||||
$data['buchungs_datum'],
|
||||
'Ausgabe',
|
||||
(int)$igeAcc['id'],
|
||||
(int)$data['zahlungskonto_id'],
|
||||
$igeUst,
|
||||
0.0,
|
||||
'normal',
|
||||
0.0,
|
||||
$igeUst,
|
||||
$prefix . $nr,
|
||||
$fy['id'],
|
||||
]
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $mainId;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -21,23 +21,23 @@ use Steuerkram\Database;
|
|||
* ustva_kz = '44' → Kz 44 (betrag_netto) [steuerfreie Ausfuhren via Konto]
|
||||
* ustva_kz = '61' → Kz 61 (betrag_netto) [Vorsteuer 19 % via Konto 1576]
|
||||
* ustva_kz = '62' → Kz 62 (betrag_netto) [Vorsteuer 7 % via Konto 1571]
|
||||
* ustva_kz = '67' → Kz 67 (betrag_netto) [VSt ig. Erwerb 19 % via Konto 1574]
|
||||
* ustva_kz = '91' → Kz 91 (betrag_netto) [USt ig. Erwerb 19 % via Konto 1774]
|
||||
*
|
||||
* 2. Steuerart-basiert (bookings.steuerart, unabhängig vom Konto):
|
||||
* steuerart = 'ig_lieferung' → Kz 41 (betrag_netto)
|
||||
* steuerart = 'export' → Kz 44 (betrag_netto)
|
||||
* steuerart = 'ig_erwerb_19' → Kz 89 (betrag_netto), Kz 91 (netto × 0,19)
|
||||
* steuerart = 'ig_erwerb_7' → Kz 93 (betrag_netto), Kz 94 (netto × 0,07)
|
||||
* steuerart = 'ig_erwerb_19' → Kz 89 (betrag_netto, Bemessungsgrundlage)
|
||||
* Buchungen mit diesen steuerart-Werten bleiben aus der kontobasierten Berechnung
|
||||
* ausgeschlossen, um Doppelzählung zu vermeiden.
|
||||
*
|
||||
* 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)
|
||||
* Kz 69 = (Kz 83 + Kz 84 + Kz 91) − Kz 66 (Zahllast)
|
||||
*/
|
||||
class UstvaCalculator
|
||||
{
|
||||
/** Steuerart-Werte die AUSSCHLIESSLICH steuerart-basiert berechnet werden. */
|
||||
/** Steuerart-Werte die aus der kontobasierten Berechnung ausgeschlossen werden. */
|
||||
private const EU_STEUERARTEN = ['ig_lieferung', 'export', 'ig_erwerb_19', 'ig_erwerb_7'];
|
||||
|
||||
/**
|
||||
|
|
@ -54,6 +54,8 @@ class UstvaCalculator
|
|||
'44' => ['44'],
|
||||
'61' => ['61'],
|
||||
'62' => ['62'],
|
||||
'67' => ['67'],
|
||||
'91' => ['91'],
|
||||
];
|
||||
|
||||
/**
|
||||
|
|
@ -63,11 +65,6 @@ class UstvaCalculator
|
|||
'41' => ['ig_lieferung'],
|
||||
'44' => ['export'],
|
||||
'89' => ['ig_erwerb_19'],
|
||||
'91' => ['ig_erwerb_19'],
|
||||
'93' => ['ig_erwerb_7'],
|
||||
'94' => ['ig_erwerb_7'],
|
||||
'67' => ['ig_erwerb_19', 'ig_erwerb_7'],
|
||||
'66' => ['ig_erwerb_19', 'ig_erwerb_7'],
|
||||
'69' => ['ig_lieferung', 'export', 'ig_erwerb_19', 'ig_erwerb_7'],
|
||||
];
|
||||
|
||||
|
|
@ -126,15 +123,13 @@ class UstvaCalculator
|
|||
$kz41 = $n('41') + $st('ig_lieferung');
|
||||
$kz44 = $n('44') + $st('export');
|
||||
$kz89 = $st('ig_erwerb_19');
|
||||
$kz91 = round($kz89 * 0.19, 2);
|
||||
$kz93 = $st('ig_erwerb_7');
|
||||
$kz94 = round($kz93 * 0.07, 2);
|
||||
// Kz 61/62 kontobasiert: betrag_netto der VSt-Buchungszeilen (1576/1571)
|
||||
// Kz 61/62/67/91 kontobasiert via Steuerkonten (1576/1571/1574/1774)
|
||||
$kz61 = $n('61');
|
||||
$kz62 = $n('62');
|
||||
$kz67 = $kz91 + $kz94;
|
||||
$kz67 = $n('67');
|
||||
$kz91 = $n('91');
|
||||
$kz66 = $kz61 + $kz62 + $kz67;
|
||||
$kz69 = ($kz83 + $kz84 + $kz91 + $kz94) - $kz66;
|
||||
$kz69 = ($kz83 + $kz84 + $kz91) - $kz66;
|
||||
|
||||
return [
|
||||
'81' => ['name' => 'Steuerpfl. Umsätze 19 %', 'betrag' => round($kz81, 2), 'steuer' => null],
|
||||
|
|
@ -146,11 +141,9 @@ class UstvaCalculator
|
|||
'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)],
|
||||
'67' => ['name' => 'Vorsteuer aus ig. Erwerb 19 %', '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)],
|
||||
];
|
||||
|
|
|
|||
|
|
@ -383,8 +383,7 @@ $suppliers = $suppliers ?? [];
|
|||
'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)',
|
||||
'ig_erwerb_19' => 'Innergemeinschaftlicher Erwerb 19 % – ReverseCharge (Kz 89/91/67)',
|
||||
];
|
||||
$currentSteuerart = $formData['steuerart'] ?? 'normal';
|
||||
foreach ($steuerartOptionen as $val => $label): ?>
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ 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.
|
||||
* 41, 44, 89, 91 und 67 einfließen.
|
||||
*/
|
||||
class UstvaAuslandTest extends TestCase
|
||||
{
|
||||
|
|
@ -29,13 +29,15 @@ class UstvaAuslandTest extends TestCase
|
|||
}
|
||||
$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;
|
||||
|
||||
$calc = new \Steuerkram\Services\UstvaCalculator();
|
||||
$kzPre = $calc->calculate($year, $month);
|
||||
|
||||
// ── 1. Steuerfreie ig. Lieferung → Kz 41 ──────────────────────────
|
||||
$belegIgL = \Steuerkram\Models\Beleg::create([
|
||||
'beleg_nr' => 'TEST-IGL-' . uniqid(),
|
||||
|
|
@ -102,30 +104,7 @@ class UstvaAuslandTest extends TestCase
|
|||
'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,
|
||||
'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();
|
||||
// ── UStVA berechnen und Delta prüfen ──────────────────────────────
|
||||
$kz = $calc->calculate($year, $month);
|
||||
|
||||
// Kz 41: ig. Lieferung 1000 €
|
||||
|
|
@ -140,58 +119,38 @@ class UstvaAuslandTest extends TestCase
|
|||
'Kz 44 (Ausfuhr): Betrag ≥ 2.000 €'
|
||||
);
|
||||
|
||||
// Kz 89: ig. Erwerb 19% Bemessungsgrundlage 500 €
|
||||
// Kz 89: ig. Erwerb 19% Bemessungsgrundlage +500 €
|
||||
$this->assert(
|
||||
(float)$kz['89']['betrag'] >= 500.00,
|
||||
'Kz 89 (ig. Erwerb 19% Bmgl.): Betrag ≥ 500 €'
|
||||
round((float)$kz['89']['betrag'] - (float)$kzPre['89']['betrag'], 2) >= 500.00,
|
||||
'Kz 89 (ig. Erwerb 19% Bmgl.): Delta ≥ 500 €'
|
||||
);
|
||||
|
||||
// Kz 91: USt auf ig. Erwerb 19% = 500 * 0.19 = 95 €
|
||||
// Kz 91: USt ig. Erwerb kontobasiert (1774) +95 €
|
||||
$this->assertEquals(
|
||||
round(500.00 * 0.19, 2),
|
||||
(float)$kz['91']['steuer'],
|
||||
'Kz 91 (USt ig. Erwerb 19%): Steuer = 95,00 €'
|
||||
95.00,
|
||||
round((float)$kz['91']['steuer'] - (float)$kzPre['91']['steuer'], 2),
|
||||
'Kz 91 (USt ig. Erwerb 19%): Delta = 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 €
|
||||
// Kz 67: VSt ig. Erwerb kontobasiert (1574) +95 €
|
||||
$this->assertEquals(
|
||||
round(300.00 * 0.07, 2),
|
||||
(float)$kz['94']['steuer'],
|
||||
'Kz 94 (USt ig. Erwerb 7%): Steuer = 21,00 €'
|
||||
95.00,
|
||||
round((float)$kz['67']['steuer'] - (float)$kzPre['67']['steuer'], 2),
|
||||
'Kz 67 (VSt ig. Erwerb): Delta = 95,00 €'
|
||||
);
|
||||
|
||||
// Kz 67: Vorsteuer aus ig. Erwerb = Kz 91 + Kz 94 = 116 €
|
||||
$erwartetKz67 = round((float)$kz['91']['steuer'] + (float)$kz['94']['steuer'], 2);
|
||||
// ReverseCharge ist zahllastneutral: Kz 69 unverändert
|
||||
$this->assertEquals(
|
||||
$erwartetKz67,
|
||||
(float)$kz['67']['steuer'],
|
||||
'Kz 67 (Vorsteuer ig. Erwerb) = Kz 91 + Kz 94'
|
||||
0.00,
|
||||
round((float)$kz['69']['steuer'] - (float)$kzPre['69']['steuer'], 2),
|
||||
'Kz 69 (Zahllast) durch ig. Erwerb nicht verändert'
|
||||
);
|
||||
|
||||
// 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
|
||||
// ig. Erwerb darf nicht in Kz 35 landen
|
||||
$this->assertEquals(
|
||||
0.0,
|
||||
(float)$kz['35']['betrag'],
|
||||
'Kz 35 (steuerfreie Inlandsumsätze) = 0 (kein normaler 0%-Umsatz gebucht)'
|
||||
'Kz 35 (steuerfreie Inlandsumsätze) = 0'
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
|||
126
tests/30_IgErwerbReverseChargeTest.php
Normal file
126
tests/30_IgErwerbReverseChargeTest.php
Normal file
|
|
@ -0,0 +1,126 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* Test 30: Innergemeinschaftlicher Erwerb 19 % – ReverseCharge-Kindzeilen.
|
||||
*
|
||||
* Prüft, dass ig_erwerb_19-Buchungen automatisch zwei Kindzeilen erhalten
|
||||
* (1574 für Kz 67, 1774 für Kz 91) und die UStVA diese kontobasiert ausweist.
|
||||
* Storno muss beide Kindzeilen mitaufheben.
|
||||
*/
|
||||
class IgErwerbReverseChargeTest extends TestCase
|
||||
{
|
||||
protected string $testName = 'Test 30: ig. Erwerb 19 % ReverseCharge-Kindzeilen';
|
||||
|
||||
public function run(): void
|
||||
{
|
||||
$year = (int)date('Y');
|
||||
$month = (int)date('n');
|
||||
$datum = date('Y-m-d');
|
||||
|
||||
$zahlungsId = $this->findAccountId('1200');
|
||||
$ausgabeId = $this->findAccountId('4120');
|
||||
$konto1574 = \Steuerkram\Database::fetchOne(
|
||||
"SELECT id FROM accounts WHERE kontonummer = '1574' AND aktiv = 1"
|
||||
);
|
||||
$konto1774 = \Steuerkram\Database::fetchOne(
|
||||
"SELECT id FROM accounts WHERE kontonummer = '1774' AND aktiv = 1"
|
||||
);
|
||||
|
||||
if (!$zahlungsId || !$ausgabeId || !$konto1574 || !$konto1774) {
|
||||
$this->assert(false, 'Benötigte Konten nicht gefunden (1200, 4120, 1574, 1774)');
|
||||
return;
|
||||
}
|
||||
$this->assert(true, 'Konten 1200, 4120, 1574, 1774 vorhanden');
|
||||
|
||||
$calc = new \Steuerkram\Services\UstvaCalculator();
|
||||
$kzPre = $calc->calculate($year, $month);
|
||||
|
||||
$belegId = \Steuerkram\Models\Beleg::create([
|
||||
'beleg_nr' => 'IGE-TEST-' . uniqid(),
|
||||
'filename' => 'ige-test.pdf',
|
||||
'storage_path' => '',
|
||||
'mime_type' => 'application/pdf',
|
||||
'file_hash' => hash('sha256', 'ige-test-' . uniqid()),
|
||||
'beleg_datum' => $datum,
|
||||
'belegtyp' => 'Eingangsrechnung',
|
||||
]);
|
||||
|
||||
// Ausgabe-Buchung 0 % USt, steuerart=ig_erwerb_19, Netto=1000 → IGE-USt=190
|
||||
$bookingId = \Steuerkram\Models\Booking::create([
|
||||
'beleg_id' => $belegId,
|
||||
'buchungs_datum' => $datum,
|
||||
'buchungstyp' => 'Ausgabe',
|
||||
'kategorie_konto_id' => $ausgabeId,
|
||||
'zahlungskonto_id' => $zahlungsId,
|
||||
'betrag_netto' => 1000.00,
|
||||
'ust_satz' => 0.0,
|
||||
'steuerart' => 'ig_erwerb_19',
|
||||
'buchungstext' => 'Test ig. Erwerb 19%',
|
||||
]);
|
||||
|
||||
// Zwei Kindzeilen müssen angelegt worden sein
|
||||
$kinder = \Steuerkram\Database::fetchAll(
|
||||
'SELECT bk.*, a.kontonummer FROM bookings bk
|
||||
JOIN accounts a ON bk.kategorie_konto_id = a.id
|
||||
WHERE bk.parent_booking_id = ?
|
||||
ORDER BY a.kontonummer',
|
||||
[$bookingId]
|
||||
);
|
||||
$this->assertEquals(2, count($kinder), 'Genau zwei Kindzeilen angelegt');
|
||||
|
||||
$kind1574 = null;
|
||||
$kind1774 = null;
|
||||
foreach ($kinder as $k) {
|
||||
if ($k['kontonummer'] === '1574') $kind1574 = $k;
|
||||
if ($k['kontonummer'] === '1774') $kind1774 = $k;
|
||||
}
|
||||
|
||||
$this->assert($kind1574 !== null, 'Kindzeile auf Konto 1574 vorhanden');
|
||||
$this->assertEquals(190.00, round((float)($kind1574['betrag_netto'] ?? 0), 2), '1574 betrag_netto = 190');
|
||||
$this->assertEquals(0.00, round((float)($kind1574['betrag_ust'] ?? 0), 2), '1574 betrag_ust = 0');
|
||||
|
||||
$this->assert($kind1774 !== null, 'Kindzeile auf Konto 1774 vorhanden');
|
||||
$this->assertEquals(190.00, round((float)($kind1774['betrag_netto'] ?? 0), 2), '1774 betrag_netto = 190');
|
||||
$this->assertEquals(0.00, round((float)($kind1774['betrag_ust'] ?? 0), 2), '1774 betrag_ust = 0');
|
||||
|
||||
// UStVA: Kz 89 (Bemessungsgrundlage), Kz 91 (USt), Kz 67 (VSt)
|
||||
$kzMit = $calc->calculate($year, $month);
|
||||
|
||||
$delta89 = round((float)$kzMit['89']['betrag'] - (float)$kzPre['89']['betrag'], 2);
|
||||
$delta91 = round((float)$kzMit['91']['steuer'] - (float)$kzPre['91']['steuer'], 2);
|
||||
$delta67 = round((float)$kzMit['67']['steuer'] - (float)$kzPre['67']['steuer'], 2);
|
||||
$delta69 = round((float)$kzMit['69']['steuer'] - (float)$kzPre['69']['steuer'], 2);
|
||||
|
||||
$this->assertEquals(1000.00, $delta89, 'Kz 89 stieg um 1000 €');
|
||||
$this->assertEquals(190.00, $delta91, 'Kz 91 stieg um 190 €');
|
||||
$this->assertEquals(190.00, $delta67, 'Kz 67 stieg um 190 €');
|
||||
$this->assertEquals(0.00, $delta69, 'Kz 69 (Zahllast) unverändert – ReverseCharge neutral');
|
||||
|
||||
// Drill-down Kz 91 enthält die 1774-Kindzeile
|
||||
$drilldown = $calc->fetchBookingsForKz($year, $month, '91');
|
||||
$found = false;
|
||||
foreach ($drilldown as $b) {
|
||||
if ((int)$b['id'] === (int)$kind1774['id']) { $found = true; break; }
|
||||
}
|
||||
$this->assert($found, 'Drill-down Kz 91 enthält die 1774-Kindzeile');
|
||||
|
||||
// Storno: beide Kindzeilen werden mitaufgehoben
|
||||
$reversalId = \Steuerkram\Models\Booking::reverse($bookingId, 'Test IGE-Storno');
|
||||
|
||||
$stornoKinder = \Steuerkram\Database::fetchAll(
|
||||
'SELECT bk.*, a.kontonummer FROM bookings bk
|
||||
JOIN accounts a ON bk.kategorie_konto_id = a.id
|
||||
WHERE bk.parent_booking_id = ? AND bk.is_reversal = 1',
|
||||
[$reversalId]
|
||||
);
|
||||
$this->assertEquals(2, count($stornoKinder), 'Storno erzeugt zwei Kindzeilen');
|
||||
|
||||
$kzNach = $calc->calculate($year, $month);
|
||||
$delta91n = round((float)$kzNach['91']['steuer'] - (float)$kzPre['91']['steuer'], 2);
|
||||
$delta67n = round((float)$kzNach['67']['steuer'] - (float)$kzPre['67']['steuer'], 2);
|
||||
$this->assertEquals(0.00, $delta91n, 'Kz 91 nach Storno wieder auf Ausgangswert');
|
||||
$this->assertEquals(0.00, $delta67n, 'Kz 67 nach Storno wieder auf Ausgangswert');
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Reference in a new issue