Feature: Vorsteuerkonten 1571/1576 aktivieren, VSt-Buchungszeilen einführen
- Migration 044: Konten 1574 (VSt i.g.E. 19%) und 1774 (USt i.g.E. 19%) anlegen - Migration 045: Konten 1571/1576 anlegen, ustva_kz='61'/'62' setzen, altes DATEV-Routing (ustva_kz '61'/'62' auf Betriebsausgabe-Konten) entfernen, parent_booking_id-Spalte, VSt-Buchungszeilen für Bestandsbuchungen migrieren - Booking::create(): automatisch VSt-Zeile auf 1576/1571 für Ausgaben mit 7/19 % - Booking::reverse(): VSt-Zeilen beim Storno mitaufheben (reversed_by setzen) - BookingController::destroy(): VSt-Kindzeilen beim Löschen (Testmodus) mit löschen - UstvaCalculator: Kz 61/62 jetzt kontobasiert via 1576/1571 (betrag_netto), Step 2 (buchungsbasiert via ust_satz) entfernt - Beleg::all/find: betrag_brutto-Aggregation und Suche schließen VSt-Zeilen aus - Tests 12, 18 angepasst; Test 28 neu
This commit is contained in:
parent
8a9fa120f2
commit
d3a6c379c9
10 changed files with 354 additions and 76 deletions
12
migrations/044_konten_ig_erwerb_ust_vst.sql
Normal file
12
migrations/044_konten_ig_erwerb_ust_vst.sql
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
-- Migration 044: Konten für ig. Erwerb (ReverseCharge) anlegen
|
||||
--
|
||||
-- 1574: Abziehbare Vorsteuer aus ig. Erwerb 19 % (nach Reset in 013 nicht mehr vorhanden)
|
||||
-- 1774: Umsatzsteuer aus ig. Erwerb 19 % (neu)
|
||||
|
||||
INSERT INTO accounts (kontonummer, bezeichnung, kontenart, steuercode, ustva_kz, is_system)
|
||||
VALUES ('1574', 'Abziehbare Vorsteuer aus innergemeinschaftlichem Erwerb 19 %', 'Zahlungskonto', 'VSt19', NULL, 1)
|
||||
ON DUPLICATE KEY UPDATE aktiv = 1;
|
||||
|
||||
INSERT INTO accounts (kontonummer, bezeichnung, kontenart, steuercode, ustva_kz, is_system)
|
||||
VALUES ('1774', 'Umsatzsteuer aus innergemeinschaftlichem Erwerb 19 %', 'Verbindlichkeit', 'USt19', NULL, 1)
|
||||
ON DUPLICATE KEY UPDATE aktiv = 1;
|
||||
78
migrations/045_vorsteuerkonten_buchungszeilen.sql
Normal file
78
migrations/045_vorsteuerkonten_buchungszeilen.sql
Normal file
|
|
@ -0,0 +1,78 @@
|
|||
-- Migration 045: Vorsteuerkonten aktivieren und VSt-Buchungszeilen einführen
|
||||
--
|
||||
-- Hintergrund: Bisher wurden Vorsteuerbeträge direkt in betrag_ust der Ausgabe-Buchung
|
||||
-- geführt. Ab jetzt erhält jede Ausgabe-Buchung mit ust_satz 7/19 (steuerart=normal)
|
||||
-- eine eigene VSt-Buchungszeile auf Konto 1571 (7%) bzw. 1576 (19%).
|
||||
--
|
||||
-- Änderungen:
|
||||
-- 1. Konten 1571 und 1576 anlegen (nach Reset in Migration 013 nicht mehr vorhanden)
|
||||
-- 2. ustva_kz: 1576→'61', 1571→'62' (bisher '66' Sammelkennzahl)
|
||||
-- 3. ustva_kz von Betriebsausgabe-Konten mit '61'/'62' leeren (DATEV-Altmapping)
|
||||
-- 4. Neue Spalte parent_booking_id in bookings (VSt-Zeile → Hauptbuchung)
|
||||
-- 5. VSt-Buchungszeilen für alle bestehenden qualifizierten Ausgabe-Buchungen
|
||||
|
||||
-- ── 1. Konten anlegen ────────────────────────────────────────────────────────
|
||||
|
||||
INSERT INTO accounts (kontonummer, bezeichnung, kontenart, steuercode, ustva_kz, euer_zeile, is_system, aktiv) VALUES
|
||||
('1571', 'Abziehbare Vorsteuer 7 %', 'Zahlungskonto', 'VSt7', '62', NULL, 1, 1),
|
||||
('1576', 'Abziehbare Vorsteuer 19 % (Eingangsrechnungen)', 'Zahlungskonto', 'VSt19', '61', NULL, 1, 1)
|
||||
ON DUPLICATE KEY UPDATE ustva_kz = VALUES(ustva_kz), aktiv = 1;
|
||||
|
||||
-- ── 3. Altes DATEV-Routing von Betriebsausgabe-Konten entfernen ─────────────────
|
||||
-- Diese Konten hatten historisch ustva_kz='61'/'62' für direkte Kz-Zuordnung.
|
||||
-- Kz 61/62 werden ab jetzt ausschließlich über VSt-Buchungszeilen auf 1571/1576 befüllt.
|
||||
|
||||
UPDATE accounts
|
||||
SET ustva_kz = NULL
|
||||
WHERE kontenart = 'Betriebsausgabe' AND ustva_kz IN ('61', '62');
|
||||
|
||||
-- ── 2. ustva_kz für 1574 auf '67' setzen (Vorsteuer ig. Erwerb, für späteres RC) ─
|
||||
|
||||
UPDATE accounts SET ustva_kz = '67' WHERE kontonummer = '1574' AND (ustva_kz IS NULL OR ustva_kz = '');
|
||||
|
||||
-- ── 3. Spalte parent_booking_id ──────────────────────────────────────────────
|
||||
|
||||
ALTER TABLE bookings
|
||||
ADD COLUMN IF NOT EXISTS parent_booking_id INT UNSIGNED NULL COMMENT 'ID der Hauptbuchung (für VSt-Buchungszeilen)' AFTER beleg_id;
|
||||
|
||||
-- ── 4. VSt-Buchungszeilen für Bestandsbuchungen ──────────────────────────────
|
||||
-- Kriterien: buchungstyp=Ausgabe, ust_satz IN (7,19), steuerart=normal, betrag_ust!=0
|
||||
-- Idempotenz: WHERE NOT EXISTS verhindert Doppel-Einfügung
|
||||
|
||||
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('VST-M-', LPAD(b.id, 8, '0')),
|
||||
b.beleg_id,
|
||||
b.id,
|
||||
b.buchungs_datum,
|
||||
'Ausgabe',
|
||||
a_vst.id,
|
||||
b.zahlungskonto_id,
|
||||
b.betrag_ust,
|
||||
0.00,
|
||||
'normal',
|
||||
0.00,
|
||||
b.betrag_ust,
|
||||
CONCAT('Vorsteuer zu ', b.buchungs_nr),
|
||||
b.is_reversal,
|
||||
b.fiscal_year_id
|
||||
FROM bookings b
|
||||
JOIN accounts a_vst ON (
|
||||
(ROUND(b.ust_satz) = 19 AND a_vst.kontonummer = '1576')
|
||||
OR
|
||||
(ROUND(b.ust_satz) = 7 AND a_vst.kontonummer = '1571')
|
||||
)
|
||||
WHERE b.buchungstyp = 'Ausgabe'
|
||||
AND b.betrag_ust != 0
|
||||
AND b.steuerart = 'normal'
|
||||
AND ROUND(b.ust_satz) IN (7, 19)
|
||||
AND b.parent_booking_id IS NULL
|
||||
AND NOT EXISTS (
|
||||
SELECT 1 FROM bookings vst
|
||||
WHERE vst.parent_booking_id = b.id
|
||||
);
|
||||
|
|
@ -262,6 +262,7 @@ class BookingController
|
|||
return;
|
||||
}
|
||||
|
||||
\Steuerkram\Database::execute('DELETE FROM bookings WHERE parent_booking_id = ?', [$id]);
|
||||
\Steuerkram\Database::execute('DELETE FROM bookings WHERE id = ?', [$id]);
|
||||
$redirect = !empty($_POST['beleg_id'])
|
||||
? '/belege/' . (int)$_POST['beleg_id']
|
||||
|
|
|
|||
|
|
@ -19,7 +19,7 @@ class Beleg
|
|||
OR s.name LIKE ?
|
||||
OR b.bewirtungsgrund LIKE ? OR b.teilnehmerliste LIKE ?
|
||||
OR (SELECT CAST(SUM(bk2s.betrag_brutto) AS CHAR) FROM bookings bk2s
|
||||
WHERE bk2s.beleg_id = b.id AND bk2s.is_reversal = 0) LIKE ?)';
|
||||
WHERE bk2s.beleg_id = b.id AND bk2s.is_reversal = 0 AND bk2s.parent_booking_id IS NULL) LIKE ?)';
|
||||
$s = '%' . $filter['search'] . '%';
|
||||
$sBrutto = '%' . str_replace(',', '.', $filter['search']) . '%';
|
||||
$params = array_merge($params, [$s, $s, $s, $s, $s, $s, $s, $s, $sBrutto]);
|
||||
|
|
@ -47,7 +47,7 @@ class Beleg
|
|||
$offset = ($page - 1) * $perPage;
|
||||
$sql = 'SELECT b.*,
|
||||
(SELECT SUM(bk.betrag_brutto) FROM bookings bk
|
||||
WHERE bk.beleg_id = b.id AND bk.is_reversal = 0
|
||||
WHERE bk.beleg_id = b.id AND bk.is_reversal = 0 AND bk.parent_booking_id IS NULL
|
||||
) AS betrag_brutto,
|
||||
fy.year as fiscal_year_nr,
|
||||
s.name as supplier_name,
|
||||
|
|
@ -77,7 +77,7 @@ class Beleg
|
|||
return Database::fetchOne(
|
||||
'SELECT b.*,
|
||||
(SELECT SUM(bk.betrag_brutto) FROM bookings bk
|
||||
WHERE bk.beleg_id = b.id AND bk.is_reversal = 0
|
||||
WHERE bk.beleg_id = b.id AND bk.is_reversal = 0 AND bk.parent_booking_id IS NULL
|
||||
) AS betrag_brutto,
|
||||
fy.year as fiscal_year_nr,
|
||||
s.name as supplier_name,
|
||||
|
|
|
|||
|
|
@ -131,7 +131,46 @@ class Booking
|
|||
]
|
||||
);
|
||||
|
||||
return (int)Database::lastInsertId();
|
||||
$mainId = (int)Database::lastInsertId();
|
||||
|
||||
// VSt-Buchungszeile für normale Ausgaben mit 7 % oder 19 % anlegen
|
||||
$ustSatzInt = (int)round((float)($data['ust_satz'] ?? 0));
|
||||
if ($data['buchungstyp'] === 'Ausgabe'
|
||||
&& in_array($ustSatzInt, [7, 19], true)
|
||||
&& $steuerart === 'normal'
|
||||
&& abs($ust) > 0.0
|
||||
) {
|
||||
$vstKontonummer = $ustSatzInt === 19 ? '1576' : '1571';
|
||||
$vstAcc = Account::findByKontonummer($vstKontonummer);
|
||||
if ($vstAcc) {
|
||||
$vstNr = self::generateBuchungsNr();
|
||||
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 (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)',
|
||||
[
|
||||
$vstNr,
|
||||
(int)$data['beleg_id'],
|
||||
$mainId,
|
||||
$data['buchungs_datum'],
|
||||
'Ausgabe',
|
||||
(int)$vstAcc['id'],
|
||||
(int)$data['zahlungskonto_id'],
|
||||
$ust,
|
||||
0.0,
|
||||
'normal',
|
||||
0.0,
|
||||
$ust,
|
||||
'Vorsteuer zu ' . $nr,
|
||||
$fy['id'],
|
||||
]
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return $mainId;
|
||||
}
|
||||
|
||||
public static function reverse(int $id, string $reason): int
|
||||
|
|
@ -182,6 +221,40 @@ class Booking
|
|||
// Link original to reversal
|
||||
Database::execute('UPDATE bookings SET reversed_by=? WHERE id=?', [$reversalId, $id]);
|
||||
|
||||
// Verknüpfte VSt-Buchungszeilen ebenfalls stornieren
|
||||
$vstBookings = Database::fetchAll(
|
||||
'SELECT * FROM bookings WHERE parent_booking_id = ? AND reversed_by IS NULL',
|
||||
[$id]
|
||||
);
|
||||
foreach ($vstBookings as $vst) {
|
||||
$vstNr = self::generateBuchungsNr();
|
||||
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, is_reversal, fiscal_year_id)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 1, ?)',
|
||||
[
|
||||
$vstNr,
|
||||
$vst['beleg_id'],
|
||||
$reversalId,
|
||||
date('Y-m-d'),
|
||||
$vst['buchungstyp'],
|
||||
$vst['kategorie_konto_id'],
|
||||
$vst['zahlungskonto_id'],
|
||||
-1 * (float)$vst['betrag_netto'],
|
||||
$vst['ust_satz'],
|
||||
$vst['steuerart'],
|
||||
-1 * (float)$vst['betrag_ust'],
|
||||
-1 * (float)$vst['betrag_brutto'],
|
||||
'STORNO zu ' . $vst['buchungs_nr'] . ': ' . $reason,
|
||||
$fy['id'],
|
||||
]
|
||||
);
|
||||
$vstReversalId = (int)Database::lastInsertId();
|
||||
Database::execute('UPDATE bookings SET reversed_by=? WHERE id=?', [$vstReversalId, $vst['id']]);
|
||||
}
|
||||
|
||||
return $reversalId;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -17,21 +17,16 @@ use Steuerkram\Database;
|
|||
* ustva_kz = '35' → Kz 35 (betrag_netto)
|
||||
* ustva_kz = '41' → Kz 41 (betrag_netto) [steuerfreie ig. Lieferungen via Konto]
|
||||
* ustva_kz = '44' → Kz 44 (betrag_netto) [steuerfreie Ausfuhren via Konto]
|
||||
* (Kz 61/62 werden NICHT mehr über ustva_kz geroutet)
|
||||
* ustva_kz = '61' → Kz 61 (betrag_netto) [Vorsteuer 19 % via Konto 1576]
|
||||
* ustva_kz = '62' → Kz 62 (betrag_netto) [Vorsteuer 7 % via Konto 1571]
|
||||
*
|
||||
* 2. Buchungsbasiert (bookings.ust_satz, für Vorsteuer):
|
||||
* buchungstyp = 'Ausgabe', ust_satz = 19 → Kz 61 (betrag_ust)
|
||||
* buchungstyp = 'Ausgabe', ust_satz = 7 → Kz 62 (betrag_ust)
|
||||
* Damit entfällt die Notwendigkeit separater VSt19/VSt7-Konten.
|
||||
*
|
||||
* 3. Steuerart-basiert (bookings.steuerart, unabhängig vom Konto):
|
||||
* 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)
|
||||
* Buchungen mit diesen steuerart-Werten bleiben aus der kontobasierten Berechnung
|
||||
* ausgeschlossen, um Doppelzählung zu vermeiden.
|
||||
* Buchungen mit EU-Steuerarten fließen ebenfalls NICHT in Kz 61/62.
|
||||
*
|
||||
* Abgeleitete Kennzahlen:
|
||||
* Kz 67 = Kz 91 + Kz 94 (Vorsteuer aus ig. Erwerb)
|
||||
|
|
@ -43,13 +38,9 @@ class UstvaCalculator
|
|||
/** Steuerart-Werte die AUSSCHLIESSLICH steuerart-basiert berechnet werden. */
|
||||
private const EU_STEUERARTEN = ['ig_lieferung', 'export', 'ig_erwerb_19', 'ig_erwerb_7'];
|
||||
|
||||
/** Vorsteuer-Kennzahl je USt-Satz (buchungsbasiert). */
|
||||
private const VST_SATZ_KZ = [19 => '61', 7 => '62'];
|
||||
|
||||
/**
|
||||
* Welche accounts.ustva_kz-Werte liefern Buchungen für die Drill-down-Ansicht.
|
||||
* Nur für den kontobasierten Teil; EU-Steuerarten kommen aus KZ_STEUERART_MAP.
|
||||
* Kz 61/62 sind hier nicht mehr enthalten (buchungsbasiert via VST_SATZ_KZ).
|
||||
*/
|
||||
private const KZ_ACCOUNT_MAP = [
|
||||
'81' => ['81'],
|
||||
|
|
@ -59,6 +50,8 @@ class UstvaCalculator
|
|||
'35' => ['35'],
|
||||
'41' => ['41'],
|
||||
'44' => ['44'],
|
||||
'61' => ['61'],
|
||||
'62' => ['62'],
|
||||
];
|
||||
|
||||
/**
|
||||
|
|
@ -78,7 +71,8 @@ class UstvaCalculator
|
|||
|
||||
public function calculate(int $year, int $month): array
|
||||
{
|
||||
// ── 1. Kontobasierte Kennzahlen (EU-Steuerarten und Kz 61/62 ausgeschlossen) ──
|
||||
// ── 1. Kontobasierte Kennzahlen (EU-Steuerarten ausgeschlossen) ─────────
|
||||
// Kz 61 und 62 werden jetzt kontobasiert über 1576/1571 (betrag_netto) ermittelt.
|
||||
$euPlaceholders = implode(',', array_fill(0, count(self::EU_STEUERARTEN), '?'));
|
||||
$accountRows = Database::fetchAll(
|
||||
'SELECT a.ustva_kz,
|
||||
|
|
@ -89,7 +83,6 @@ class UstvaCalculator
|
|||
JOIN accounts a ON a.id = bk.kategorie_konto_id
|
||||
WHERE fy.year = ? AND MONTH(bk.buchungs_datum) = ?
|
||||
AND a.ustva_kz IS NOT NULL AND a.ustva_kz != \'\'
|
||||
AND a.ustva_kz NOT IN (\'61\', \'62\')
|
||||
AND (bk.steuerart IS NULL OR bk.steuerart NOT IN (' . $euPlaceholders . '))
|
||||
GROUP BY a.ustva_kz',
|
||||
array_merge([$year, $month], self::EU_STEUERARTEN)
|
||||
|
|
@ -103,28 +96,7 @@ class UstvaCalculator
|
|||
];
|
||||
}
|
||||
|
||||
// ── 2. Buchungsbasierte Vorsteuer Kz 61/62 (via ust_satz) ────────────
|
||||
$vstRows = Database::fetchAll(
|
||||
'SELECT bk.ust_satz, SUM(bk.betrag_ust) AS summe_ust
|
||||
FROM bookings bk
|
||||
JOIN fiscal_years fy ON bk.fiscal_year_id = fy.id
|
||||
WHERE fy.year = ? AND MONTH(bk.buchungs_datum) = ?
|
||||
AND bk.buchungstyp = \'Ausgabe\'
|
||||
AND bk.betrag_ust != 0
|
||||
AND (bk.steuerart IS NULL OR bk.steuerart NOT IN (' . $euPlaceholders . '))
|
||||
GROUP BY bk.ust_satz',
|
||||
array_merge([$year, $month], self::EU_STEUERARTEN)
|
||||
);
|
||||
|
||||
foreach ($vstRows as $row) {
|
||||
$satz = (int)round((float)$row['ust_satz']);
|
||||
$kzNr = self::VST_SATZ_KZ[$satz] ?? null;
|
||||
if ($kzNr !== null) {
|
||||
$byKz[$kzNr] = ['netto' => 0.0, 'ust' => (float)$row['summe_ust']];
|
||||
}
|
||||
}
|
||||
|
||||
// ── 3. Steuerart-basierte Kennzahlen ─────────────────────────────────
|
||||
// ── 2. Steuerart-basierte Kennzahlen ─────────────────────────────────
|
||||
$stRows = Database::fetchAll(
|
||||
'SELECT bk.steuerart, SUM(bk.betrag_netto) AS summe_netto
|
||||
FROM bookings bk
|
||||
|
|
@ -140,7 +112,7 @@ class UstvaCalculator
|
|||
$bySteuerart[$row['steuerart']] = (float)$row['summe_netto'];
|
||||
}
|
||||
|
||||
// ── 4. Kennzahlen zusammensetzen ─────────────────────────────────────
|
||||
// ── 3. Kennzahlen zusammensetzen ─────────────────────────────────────
|
||||
$n = fn(string $kz): float => (float)($byKz[$kz]['netto'] ?? 0.0);
|
||||
$u = fn(string $kz): float => (float)($byKz[$kz]['ust'] ?? 0.0);
|
||||
$st = fn(string $art): float => (float)($bySteuerart[$art] ?? 0.0);
|
||||
|
|
@ -154,8 +126,9 @@ class UstvaCalculator
|
|||
$kz91 = round($kz89 * 0.19, 2);
|
||||
$kz93 = $st('ig_erwerb_7');
|
||||
$kz94 = round($kz93 * 0.07, 2);
|
||||
$kz61 = $u('61');
|
||||
$kz62 = $u('62');
|
||||
// Kz 61/62 kontobasiert: betrag_netto der VSt-Buchungszeilen (1576/1571)
|
||||
$kz61 = $n('61');
|
||||
$kz62 = $n('62');
|
||||
$kz67 = $kz91 + $kz94;
|
||||
$kz66 = $kz61 + $kz62 + $kz67;
|
||||
$kz69 = ($kz83 + $kz84 + $kz91 + $kz94) - $kz66;
|
||||
|
|
@ -182,17 +155,15 @@ class UstvaCalculator
|
|||
|
||||
/**
|
||||
* Liefert alle Buchungszeilen, die zu einer Kennzahl beitragen (Drill-down).
|
||||
* Kombiniert kontobasierte, buchungsbasierte und steuerart-basierte Quellen per UNION.
|
||||
* Kombiniert kontobasierte und steuerart-basierte Quellen per UNION.
|
||||
* Kz 61/62 werden kontobasiert über 1576/1571 geliefert (VSt-Buchungszeilen).
|
||||
*/
|
||||
public function fetchBookingsForKz(int $year, int $month, string $kz): array
|
||||
{
|
||||
$accountKzList = self::KZ_ACCOUNT_MAP[$kz] ?? [];
|
||||
$steuerartList = self::KZ_STEUERART_MAP[$kz] ?? [];
|
||||
|
||||
// Kz 61/62: buchungsbasiert via ust_satz
|
||||
$vstSatz = array_search($kz, self::VST_SATZ_KZ, true);
|
||||
|
||||
if (empty($accountKzList) && empty($steuerartList) && $vstSatz === false) {
|
||||
if (empty($accountKzList) && empty($steuerartList)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
|
|
@ -226,20 +197,6 @@ class UstvaCalculator
|
|||
);
|
||||
}
|
||||
|
||||
if ($vstSatz !== false) {
|
||||
$euPh = implode(',', array_fill(0, count(self::EU_STEUERARTEN), '?'));
|
||||
$parts[] = $select
|
||||
. ' AND bk.buchungstyp = \'Ausgabe\''
|
||||
. ' AND bk.betrag_ust != 0'
|
||||
. ' AND ROUND(bk.ust_satz) = ?'
|
||||
. ' AND (bk.steuerart IS NULL OR bk.steuerart NOT IN (' . $euPh . '))';
|
||||
$params = array_merge($params,
|
||||
[$year, $month],
|
||||
[$vstSatz],
|
||||
self::EU_STEUERARTEN
|
||||
);
|
||||
}
|
||||
|
||||
if (!empty($steuerartList)) {
|
||||
$ph = implode(',', array_fill(0, count($steuerartList), '?'));
|
||||
$parts[] = $select . ' AND bk.steuerart IN (' . $ph . ')';
|
||||
|
|
|
|||
|
|
@ -1 +1 @@
|
|||
1-260506
|
||||
1-260510
|
||||
|
|
|
|||
|
|
@ -55,7 +55,8 @@ class BelegGebuechtStatusTest extends TestCase
|
|||
$beleg = Beleg::find($belegId);
|
||||
$this->assertEquals(1, (int)($beleg['booked'] ?? 0), 'Nach Buchung: booked = 1 (abgeleitet)');
|
||||
|
||||
// Buchung löschen
|
||||
// Buchung löschen (inkl. zugehöriger VSt-Buchungszeilen)
|
||||
Database::execute('DELETE FROM bookings WHERE parent_booking_id = ?', [$buchungId]);
|
||||
Database::execute('DELETE FROM bookings WHERE id = ?', [$buchungId]);
|
||||
|
||||
// Nach Löschen der Buchung: booked = 0
|
||||
|
|
|
|||
|
|
@ -93,22 +93,30 @@ class StornoBuchungTest extends TestCase
|
|||
);
|
||||
|
||||
// ── UStVA: Vorsteuer-KZ hebt sich auf ─────────────────────────
|
||||
// Kz 61 enthält Vorsteuer aus dem Ausgabe-Konto (ustva_kz='61').
|
||||
// Original: +19, Storno: -19 → Summe = 0 (oder weniger als andere Tests einbringen).
|
||||
// Da weitere Tests parallel laufen können, prüfen wir nur, dass die Storno-Buchung
|
||||
// als eigenständige Zeile in fetchBookingsForKz erscheint.
|
||||
// Kz 61 wird jetzt kontobasiert über VSt-Buchungszeilen (Konto 1576) ermittelt.
|
||||
// Zur Original-Ausgabe gehört eine VSt-Zeile (+19), zum Storno eine VSt-Storno-Zeile (-19).
|
||||
// Wir prüfen, dass die VSt-Storno-Zeile im Drill-down erscheint.
|
||||
$vstReversalRow = \Steuerkram\Database::fetchOne(
|
||||
'SELECT * FROM bookings WHERE parent_booking_id = ? AND is_reversal = 1',
|
||||
[$reversalId]
|
||||
);
|
||||
$this->assert($vstReversalRow !== false, 'VSt-Stornobuchung wurde zu Storno angelegt');
|
||||
if ($vstReversalRow) {
|
||||
$this->assertEquals(-19.00, round((float)$vstReversalRow['betrag_netto'], 2), 'VSt-Storno: betrag_netto = -19');
|
||||
}
|
||||
|
||||
$ustvaCalc = new \Steuerkram\Services\UstvaCalculator();
|
||||
$bookings = $ustvaCalc->fetchBookingsForKz($year, $month, '61');
|
||||
|
||||
$reversalFound = false;
|
||||
$vstReversalFound = false;
|
||||
foreach ($bookings as $b) {
|
||||
if ((int)$b['id'] === $reversalId) {
|
||||
$reversalFound = true;
|
||||
$this->assertEquals(1, (int)$b['is_reversal'], 'UStVA-Drilldown: Stornobuchung is_reversal = 1');
|
||||
$this->assertEquals(-19.00, round((float)$b['betrag_ust'], 2), 'UStVA-Drilldown: Storno betrag_ust = -19');
|
||||
if ($vstReversalRow && (int)$b['id'] === (int)$vstReversalRow['id']) {
|
||||
$vstReversalFound = true;
|
||||
$this->assertEquals(1, (int)$b['is_reversal'], 'UStVA-Drilldown: VSt-Stornobuchung is_reversal = 1');
|
||||
$this->assertEquals(-19.00, round((float)$b['betrag_netto'], 2), 'UStVA-Drilldown: VSt-Storno betrag_netto = -19');
|
||||
break;
|
||||
}
|
||||
}
|
||||
$this->assert($reversalFound, 'UStVA-Drilldown Kz 61: Stornobuchung erscheint in Detailansicht');
|
||||
$this->assert($vstReversalFound, 'UStVA-Drilldown Kz 61: VSt-Stornobuchung erscheint in Detailansicht');
|
||||
}
|
||||
}
|
||||
|
|
|
|||
148
tests/28_VorsteuerkontenTest.php
Normal file
148
tests/28_VorsteuerkontenTest.php
Normal file
|
|
@ -0,0 +1,148 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* Test 28: Vorsteuerkonten-Buchungszeilen.
|
||||
*
|
||||
* Prüft, dass Ausgabe-Buchungen mit USt 7 % / 19 % automatisch eine
|
||||
* VSt-Buchungszeile auf 1571 / 1576 erhalten, die UStVA Kz 61/62 daraus
|
||||
* kontobasiert ableitet und ein Storno die VSt-Zeilen mitaufhebt.
|
||||
*/
|
||||
class VorsteuerkontenTest extends TestCase
|
||||
{
|
||||
protected string $testName = 'Test 28: Vorsteuerkonten-Buchungszeilen';
|
||||
|
||||
public function run(): void
|
||||
{
|
||||
$year = (int)date('Y');
|
||||
$month = (int)date('n');
|
||||
$datum = date('Y-m-d');
|
||||
|
||||
$zahlungsId = $this->findAccountId('1200');
|
||||
$ausgabeId = $this->findAccountId('4920');
|
||||
$konto1576 = \Steuerkram\Database::fetchOne(
|
||||
"SELECT id FROM accounts WHERE kontonummer = '1576' AND aktiv = 1"
|
||||
);
|
||||
$konto1571 = \Steuerkram\Database::fetchOne(
|
||||
"SELECT id FROM accounts WHERE kontonummer = '1571' AND aktiv = 1"
|
||||
);
|
||||
|
||||
if (!$zahlungsId || !$ausgabeId || !$konto1576 || !$konto1571) {
|
||||
$this->assert(false, 'Benötigte Konten nicht gefunden (1200, 4920, 1571, 1576)');
|
||||
return;
|
||||
}
|
||||
$this->assert(true, 'Konten 1200, 4920, 1571, 1576 vorhanden');
|
||||
|
||||
// ── UStVA-Stand vor den Testbuchungen ─────────────────────────
|
||||
$calc = new \Steuerkram\Services\UstvaCalculator();
|
||||
$kzPre = $calc->calculate($year, $month);
|
||||
|
||||
// ── Beleg anlegen ──────────────────────────────────────────────
|
||||
$belegId = \Steuerkram\Models\Beleg::create([
|
||||
'beleg_nr' => 'VST-TEST-' . uniqid(),
|
||||
'filename' => 'vst-test.pdf',
|
||||
'storage_path' => '',
|
||||
'mime_type' => 'application/pdf',
|
||||
'file_hash' => hash('sha256', 'vst-test-' . uniqid()),
|
||||
'beleg_datum' => $datum,
|
||||
'belegtyp' => 'Eingangsrechnung',
|
||||
]);
|
||||
|
||||
// ── Ausgabe-Buchung 19 % (Netto=200, VSt=38) ──────────────────
|
||||
$bookingId19 = \Steuerkram\Models\Booking::create([
|
||||
'beleg_id' => $belegId,
|
||||
'buchungs_datum' => $datum,
|
||||
'buchungstyp' => 'Ausgabe',
|
||||
'kategorie_konto_id' => $ausgabeId,
|
||||
'zahlungskonto_id' => $zahlungsId,
|
||||
'betrag_netto' => 200.00,
|
||||
'ust_satz' => 19.0,
|
||||
'buchungstext' => 'Test VSt 19%',
|
||||
]);
|
||||
|
||||
// VSt-Buchungszeile muss auf 1576 angelegt worden sein
|
||||
$vstRow19 = \Steuerkram\Database::fetchOne(
|
||||
'SELECT bk.*, a.kontonummer FROM bookings bk
|
||||
JOIN accounts a ON bk.kategorie_konto_id = a.id
|
||||
WHERE bk.parent_booking_id = ?',
|
||||
[$bookingId19]
|
||||
);
|
||||
$this->assert($vstRow19 !== false, 'VSt-Buchungszeile für 19%-Buchung angelegt');
|
||||
$this->assertEquals('1576', $vstRow19['kontonummer'] ?? '', 'VSt-Buchungszeile auf Konto 1576');
|
||||
$this->assertEquals(38.00, round((float)($vstRow19['betrag_netto'] ?? 0), 2), 'VSt betrag_netto = 38');
|
||||
$this->assertEquals(0.00, round((float)($vstRow19['betrag_ust'] ?? 0), 2), 'VSt betrag_ust = 0');
|
||||
|
||||
// ── Ausgabe-Buchung 7 % (Netto=100, VSt=7) ────────────────────
|
||||
$bookingId7 = \Steuerkram\Models\Booking::create([
|
||||
'beleg_id' => $belegId,
|
||||
'buchungs_datum' => $datum,
|
||||
'buchungstyp' => 'Ausgabe',
|
||||
'kategorie_konto_id' => $ausgabeId,
|
||||
'zahlungskonto_id' => $zahlungsId,
|
||||
'betrag_netto' => 100.00,
|
||||
'ust_satz' => 7.0,
|
||||
'buchungstext' => 'Test VSt 7%',
|
||||
]);
|
||||
|
||||
$vstRow7 = \Steuerkram\Database::fetchOne(
|
||||
'SELECT bk.*, a.kontonummer FROM bookings bk
|
||||
JOIN accounts a ON bk.kategorie_konto_id = a.id
|
||||
WHERE bk.parent_booking_id = ?',
|
||||
[$bookingId7]
|
||||
);
|
||||
$this->assert($vstRow7 !== false, 'VSt-Buchungszeile für 7%-Buchung angelegt');
|
||||
$this->assertEquals('1571', $vstRow7['kontonummer'] ?? '', 'VSt-Buchungszeile auf Konto 1571');
|
||||
$this->assertEquals(7.00, round((float)($vstRow7['betrag_netto'] ?? 0), 2), 'VSt betrag_netto = 7');
|
||||
|
||||
// ── UStVA: Kz 61 und 62 aus Konten lesen ──────────────────────
|
||||
$kzMit = $calc->calculate($year, $month);
|
||||
|
||||
$this->assert(
|
||||
round((float)$kzMit['61']['steuer'] - (float)$kzPre['61']['steuer'], 2) >= 38.00,
|
||||
'UStVA Kz 61 stieg durch 19%-Buchung um ≥ 38 €'
|
||||
);
|
||||
$this->assert(
|
||||
round((float)$kzMit['62']['steuer'] - (float)$kzPre['62']['steuer'], 2) >= 7.00,
|
||||
'UStVA Kz 62 stieg durch 7%-Buchung um ≥ 7 €'
|
||||
);
|
||||
$this->assert(
|
||||
round((float)$kzMit['66']['steuer'] - (float)$kzPre['66']['steuer'], 2) >= 45.00,
|
||||
'UStVA Kz 66 stieg um ≥ 45 €'
|
||||
);
|
||||
|
||||
// ── Drill-down Kz 61 enthält die VSt-Buchungszeile ────────────
|
||||
$drilldown = $calc->fetchBookingsForKz($year, $month, '61');
|
||||
$vstFound = false;
|
||||
foreach ($drilldown as $b) {
|
||||
if ((int)$b['id'] === (int)$vstRow19['id']) {
|
||||
$vstFound = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
$this->assert($vstFound, 'Drill-down Kz 61 enthält die VSt-Buchungszeile');
|
||||
|
||||
// ── Storno: VSt-Zeile muss mitaufgehoben werden ───────────────
|
||||
$reversalId = \Steuerkram\Models\Booking::reverse($bookingId19, 'Testfall VSt-Storno');
|
||||
|
||||
$vstStornoRow = \Steuerkram\Database::fetchOne(
|
||||
'SELECT * FROM bookings WHERE parent_booking_id = ? AND is_reversal = 1',
|
||||
[$reversalId]
|
||||
);
|
||||
$this->assert($vstStornoRow !== false, 'VSt-Stornobuchung wurde angelegt');
|
||||
$this->assertEquals(-38.00, round((float)($vstStornoRow['betrag_netto'] ?? 0), 2), 'VSt-Storno betrag_netto = -38');
|
||||
|
||||
// Original-VSt-Zeile muss als storniert markiert sein
|
||||
$vstOriginal = \Steuerkram\Database::fetchOne(
|
||||
'SELECT reversed_by FROM bookings WHERE id = ?',
|
||||
[(int)$vstRow19['id']]
|
||||
);
|
||||
$this->assert(!empty($vstOriginal['reversed_by']), 'Original-VSt-Buchungszeile hat reversed_by gesetzt');
|
||||
|
||||
// UStVA Kz 61 nach Storno: netto Änderung gegenüber vor den Testbuchungen = 0
|
||||
// (die 19%-Buchung und ihr Storno heben sich auf; 7%-Buchung geht in Kz 62)
|
||||
$kzNach = $calc->calculate($year, $month);
|
||||
$delta61 = round((float)$kzNach['61']['steuer'] - (float)$kzPre['61']['steuer'], 2);
|
||||
$this->assertEquals(0.00, $delta61, 'Kz 61: Buchung+Storno heben sich gegenüber Ausgangslage auf');
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Reference in a new issue