UStVA umgebaut auf Konten anstelle von Belegart
This commit is contained in:
parent
fcd4d227a1
commit
c9f5dd02f8
13 changed files with 609 additions and 315 deletions
56
BERICHTE.md
56
BERICHTE.md
|
|
@ -42,30 +42,50 @@ Alle passenden Buchungen werden nach Kategorie-Konto gruppiert und als Betriebsa
|
|||
|
||||
**Datei:** `src/Services/UstvaCalculator.php`
|
||||
|
||||
Die UStVA verwendet **keine** Kontonummern, sondern die Buchungsfelder `buchungstyp`, `steuerart`, `ust_satz` und `betrag_ust`. Stornobuchungen (`is_reversal = 1`) werden ausgeschlossen.
|
||||
Die UStVA basiert ausschließlich auf dem Feld `accounts.ustva_kz` des Kategorie-Kontos einer Buchung. Die Felder `buchungstyp` und `steuerart` werden nicht ausgewertet. Stornobuchungen (`is_reversal = 1`) werden ausgeschlossen.
|
||||
|
||||
### Direkt befüllte Kennzahlen
|
||||
### Berechnungslogik
|
||||
|
||||
| Kz | Bezeichnung | Bedingung |
|
||||
|---|---|---|
|
||||
| **81** | Steuerpfl. Umsätze 19 % – Netto | `buchungstyp='Einnahme'`, `steuerart='normal'`, `ust_satz=19` |
|
||||
| **86** | Steuerpfl. Umsätze 7 % – Netto | `buchungstyp='Einnahme'`, `steuerart='normal'`, `ust_satz=7` |
|
||||
| **35** | Steuerfreie Umsätze (ohne Vorsteuerabzug) – Netto | `buchungstyp='Einnahme'`, `steuerart='normal'`, `ust_satz=0` |
|
||||
| **41** | Steuerfreie ig. Lieferungen (§ 4 Nr. 1b) – Netto | `buchungstyp='Einnahme'`, `steuerart='ig_lieferung'` |
|
||||
| **44** | Steuerfreie Ausfuhren (§ 4 Nr. 1a) – Netto | `buchungstyp='Einnahme'`, `steuerart='export'` |
|
||||
| **89** | Ig. Erwerb 19 % – Bemessungsgrundlage | `buchungstyp='Ausgabe'`, `steuerart='ig_erwerb_19'` |
|
||||
| **93** | Ig. Erwerb 7 % – Bemessungsgrundlage | `buchungstyp='Ausgabe'`, `steuerart='ig_erwerb_7'` |
|
||||
| **61** | Vorsteuer aus Eingangsrechnungen 19 % | `buchungstyp='Ausgabe'`, `steuerart='normal'`, `ust_satz=19`, `betrag_ust>0` |
|
||||
| **62** | Vorsteuer aus Eingangsrechnungen 7 % | `buchungstyp='Ausgabe'`, `steuerart='normal'`, `ust_satz=7`, `betrag_ust>0` |
|
||||
Pro Monat wird eine einzige Aggregationsabfrage ausgeführt, die `betrag_netto` und `betrag_ust` nach `ustva_kz` des Kategorie-Kontos gruppiert:
|
||||
|
||||
### Abgeleitete Kennzahlen (berechnet, keine direkte Buchungszuordnung)
|
||||
```sql
|
||||
SELECT a.ustva_kz, SUM(betrag_netto), SUM(betrag_ust)
|
||||
FROM bookings JOIN accounts a ON a.id = kategorie_konto_id
|
||||
WHERE ... AND a.ustva_kz IS NOT NULL
|
||||
GROUP BY a.ustva_kz
|
||||
```
|
||||
|
||||
### Kontozuordnung und Kennzahlen
|
||||
|
||||
| Kz | Bezeichnung | Quelle | Relevante Konten (Beispiele) |
|
||||
|---|---|---|---|
|
||||
| **81** | Steuerpfl. Umsätze 19 % | `SUM(betrag_netto)` WHERE `ustva_kz='81'` | 8000, 8400, 8510, 8570, 8800, 8820, 8922 |
|
||||
| **83** | USt auf steuerpfl. Umsätze 19 % | `SUM(betrag_ust)` WHERE `ustva_kz='81'` | s.o. |
|
||||
| **86** | Steuerpfl. Umsätze 7 % | `SUM(betrag_netto)` WHERE `ustva_kz='86'` | 8300 |
|
||||
| **84** | USt auf steuerpfl. Umsätze 7 % | `SUM(betrag_ust)` WHERE `ustva_kz='86'` | s.o. |
|
||||
| **35** | Steuerfreie Umsätze | `SUM(betrag_netto)` WHERE `ustva_kz='35'` | 8100, 8105, 8110, 8160 |
|
||||
| **41** | Steuerfreie ig. Lieferungen | `SUM(betrag_netto)` WHERE `ustva_kz='41'` | 8120, 8125, 8130 |
|
||||
| **44** | Steuerfreie Ausfuhren | `SUM(betrag_netto)` WHERE `ustva_kz='44'` | 8150 |
|
||||
| **61** | Vorsteuer 19 % | `SUM(betrag_ust)` WHERE `ustva_kz='61'` | alle Betriebsausgabe-Konten mit `steuercode='VSt19'` (z.B. 3100, 4200ff, 4900ff) |
|
||||
| **62** | Vorsteuer 7 % | `SUM(betrag_ust)` WHERE `ustva_kz='62'` | alle Betriebsausgabe-Konten mit `steuercode='VSt7'` (z.B. 3110, 4610, 4680) |
|
||||
| **89** | Ig. Erwerb 19 % – Bemessungsgrundlage | `SUM(betrag_netto)` WHERE `ustva_kz='89'` | 3425 |
|
||||
| **91** | USt auf ig. Erwerb 19 % | `SUM(betrag_ust)` WHERE `ustva_kz='89'` | 3425 |
|
||||
| **93** | Ig. Erwerb 7 % – Bemessungsgrundlage | `SUM(betrag_netto)` WHERE `ustva_kz='93'` | 3420 |
|
||||
| **94** | USt auf ig. Erwerb 7 % | `SUM(betrag_ust)` WHERE `ustva_kz='93'` | 3420 |
|
||||
|
||||
### Abgeleitete Kennzahlen
|
||||
|
||||
| Kz | Bezeichnung | Berechnung |
|
||||
|---|---|---|
|
||||
| **83** | USt auf steuerpfl. Umsätze 19 % | Kz 81 × 19 % |
|
||||
| **84** | USt auf steuerpfl. Umsätze 7 % | Kz 86 × 7 % |
|
||||
| **91** | USt auf ig. Erwerb 19 % | Kz 89 × 19 % |
|
||||
| **94** | USt auf ig. Erwerb 7 % | Kz 93 × 7 % |
|
||||
| **67** | Vorsteuer aus ig. Erwerb | Kz 91 + Kz 94 |
|
||||
| **66** | Vorsteuerbeträge gesamt | Kz 61 + Kz 62 + Kz 67 |
|
||||
| **69** | Verbleibende USt-Vorauszahlung / Erstattung | (Kz 83 + Kz 84 + Kz 91 + Kz 94) − Kz 66 |
|
||||
|
||||
### Neue Konten in Klasse 3 (Migration 010)
|
||||
|
||||
| Konto | Bezeichnung | ustva_kz |
|
||||
|---|---|---|
|
||||
| 3420 | Innergemeinschaftliche Erwerbe 7 % Vorsteuer | 93 |
|
||||
| 3425 | Innergemeinschaftliche Erwerbe 19 % Vorsteuer | 89 |
|
||||
|
||||
Buchungen auf ig. Erwerb müssen auf Konto 3420 bzw. 3425 als Kategorie-Konto erfolgen.
|
||||
|
|
|
|||
40
Changes.md
40
Changes.md
|
|
@ -2,6 +2,46 @@
|
|||
|
||||
---
|
||||
|
||||
## 2026-03-29 – UStVA: kontonummernbasierte Kennzahlenberechnung
|
||||
|
||||
### Geändert
|
||||
- `UstvaCalculator::calculate()` wertet ausschließlich `accounts.ustva_kz` des Kategorie-Kontos aus – `buchungstyp` und `steuerart` werden nicht mehr benötigt
|
||||
- `UstvaCalculator::fetchBookingsForKz()` filtert über `k.ustva_kz IN (...)` statt über Buchungsfelder
|
||||
- Kz 83 und 84 kommen jetzt direkt aus `betrag_ust` der Erlöskonten (statt `Kz81 × 19 %`)
|
||||
- Kz 91 und 94 kommen direkt aus `betrag_ust` der ig.-Erwerb-Konten (statt abgeleitet)
|
||||
|
||||
### Betrifft
|
||||
- `src/Services/UstvaCalculator.php`
|
||||
- Migration `010_ustva_kz_konto_mapping.sql`
|
||||
- `BERICHTE.md` (UStVA-Kapitel vollständig neu)
|
||||
|
||||
### Migration 010 – Kontenanpassungen
|
||||
- Alle Betriebsausgabe-Konten mit `steuercode='VSt19'`: `ustva_kz` von `'66'` auf `'61'`
|
||||
- Alle Betriebsausgabe-Konten mit `steuercode='VSt7'`: `ustva_kz` von `'66'` auf `'62'`
|
||||
- Neue Konten: 3425 „Innergemeinschaftliche Erwerbe 19 %" (`ustva_kz='89'`), 3420 „Innergemeinschaftliche Erwerbe 7 %" (`ustva_kz='93'`)
|
||||
|
||||
---
|
||||
|
||||
## 2026-03-28 – 30% Eigenanteil-Kennzeichnung und Korrekturbuchungen
|
||||
|
||||
### Hinzugefügt
|
||||
- Checkbox „30% Eigenanteil buchen" in der Buchungszeilen-Erfassung (zwischen Nettobetrag und Submit-Button)
|
||||
- Neues Feld `eigenanteil_30 TINYINT(1)` in der `belege`-Tabelle (Migration `009_beleg_eigenanteil_30.sql`)
|
||||
- Der Checkbox-Wert wird beim Speichern einer Buchung in die Beleg-Metadaten übernommen
|
||||
- Bei gesetzter Checkbox werden automatisch zwei Korrekturbuchungen angelegt oder aktualisiert:
|
||||
- 30% des Nettobetrags an Konto 8922 (nicht abzugsfähiger Eigenanteil), Zahlungskonto 1800
|
||||
- 30% der errechneten USt an Konto 1776 (Vorsteuerkorrektur), Zahlungskonto 1800 – nur wenn USt-Satz > 0
|
||||
- Im Testmodus: bestehende Korrekturbuchungen werden direkt überschrieben
|
||||
- Im Produktivmodus: bestehende Korrekturbuchungen werden GoBD-konform storniert und neu angelegt
|
||||
|
||||
### Betrifft
|
||||
- `src/Views/belege/detail.php`
|
||||
- `src/Controllers/BookingController.php`
|
||||
- `src/Models/Booking.php` (neue Methoden `upsertEigenanteil30`, `upsertKorrekturbuchung`)
|
||||
- Migration: `009_beleg_eigenanteil_30.sql`
|
||||
|
||||
---
|
||||
|
||||
## 2026-03-27 – DATEV-konformer SKR03 Kontenrahmen (#11)
|
||||
|
||||
### Geändert
|
||||
|
|
|
|||
5
migrations/009_beleg_eigenanteil_30.sql
Normal file
5
migrations/009_beleg_eigenanteil_30.sql
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
-- Migration 009: Eigenanteil-30%-Kennzeichnung in belege-Tabelle
|
||||
ALTER TABLE belege
|
||||
ADD COLUMN eigenanteil_30 TINYINT(1) NOT NULL DEFAULT 0
|
||||
COMMENT '1 = 30% Eigenanteil wurde beim Buchen markiert'
|
||||
AFTER betrag_brutto;
|
||||
25
migrations/010_ustva_kz_konto_mapping.sql
Normal file
25
migrations/010_ustva_kz_konto_mapping.sql
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
-- Migration 010: UStVA-Kennzahlen direkt aus Kontozuordnung
|
||||
--
|
||||
-- 1. Betriebsausgabe-Konten: ustva_kz '66' → '61' (VSt19) bzw. '62' (VSt7)
|
||||
-- Damit werden Kz 61 und 62 direkt aus betrag_ust der jeweiligen Buchungen summiert.
|
||||
UPDATE accounts
|
||||
SET ustva_kz = '61'
|
||||
WHERE kontenart = 'Betriebsausgabe'
|
||||
AND steuercode = 'VSt19';
|
||||
|
||||
UPDATE accounts
|
||||
SET ustva_kz = '62'
|
||||
WHERE kontenart = 'Betriebsausgabe'
|
||||
AND steuercode = 'VSt7';
|
||||
|
||||
-- 2. Konten für Innergemeinschaftlichen Erwerb anlegen (Kz 89 / 93)
|
||||
-- 3425: ig. Erwerb 19 % → Kz 89 (Bemessungsgrundlage); betrag_ust → Kz 91
|
||||
-- 3420: ig. Erwerb 7 % → Kz 93 (Bemessungsgrundlage); betrag_ust → Kz 94
|
||||
INSERT INTO accounts (kontonummer, bezeichnung, kontenart, steuercode, ustva_kz, aktiv, is_system)
|
||||
VALUES
|
||||
('3425', 'Innergemeinschaftliche Erwerbe 19 % Vorsteuer', 'Betriebsausgabe', 'VSt19', '89', 1, 1),
|
||||
('3420', 'Innergemeinschaftliche Erwerbe 7 % Vorsteuer', 'Betriebsausgabe', 'VSt7', '93', 1, 1)
|
||||
ON DUPLICATE KEY UPDATE
|
||||
bezeichnung = VALUES(bezeichnung),
|
||||
steuercode = VALUES(steuercode),
|
||||
ustva_kz = VALUES(ustva_kz);
|
||||
|
|
@ -98,6 +98,7 @@ $router->get('/tax/ustva', fn($p) => (new UstvaController())->index($p));
|
|||
$router->get('/tax/ustva/{year}/{month}', fn($p) => (new UstvaController())->show($p));
|
||||
$router->post('/tax/ustva/{year}/{month}/save', fn($p) => (new UstvaController())->save($p));
|
||||
$router->get('/tax/ustva/{year}/{month}/xml', fn($p) => (new UstvaController())->downloadXml($p));
|
||||
$router->get('/tax/ustva/{year}/{month}/kz/{kz}', fn($p) => (new UstvaController())->showKzBookings($p));
|
||||
|
||||
// ── EÜR ─────────────────────────────────────────────────────
|
||||
$router->get('/tax/euer', fn($p) => (new EuerController())->index($p));
|
||||
|
|
|
|||
|
|
@ -110,6 +110,22 @@ class BookingController
|
|||
try {
|
||||
$data['beleg_id'] = $belegId;
|
||||
Booking::create($data);
|
||||
|
||||
$eigenanteil = isset($data['eigenanteil_30']) ? 1 : 0;
|
||||
\Steuerkram\Database::execute(
|
||||
'UPDATE belege SET eigenanteil_30 = ?, updated_at = NOW() WHERE id = ?',
|
||||
[$eigenanteil, $belegId]
|
||||
);
|
||||
|
||||
if ($eigenanteil === 1) {
|
||||
Booking::upsertEigenanteil30(
|
||||
$belegId,
|
||||
(float)$data['betrag_netto'],
|
||||
(float)($data['ust_satz'] ?? 0),
|
||||
$data['buchungs_datum']
|
||||
);
|
||||
}
|
||||
|
||||
View::redirect("/belege/{$belegId}", 'Buchung erfasst.');
|
||||
} catch (\PDOException $e) {
|
||||
error_log('[Steuerkram] PDO-Fehler in BookingController::store(): ' . $e->getMessage());
|
||||
|
|
|
|||
|
|
@ -98,6 +98,38 @@ class UstvaController
|
|||
View::redirect("/tax/ustva/{$year}/{$month}", 'UStVA gespeichert.');
|
||||
}
|
||||
|
||||
public function showKzBookings(array $params): void
|
||||
{
|
||||
$year = (int)$params['year'];
|
||||
$month = (int)$params['month'];
|
||||
$kz = $params['kz'] ?? '';
|
||||
|
||||
$validKz = ['81','83','86','84','35','41','44','89','91','93','94','61','62','67','66','69'];
|
||||
if (!in_array($kz, $validKz, true)) {
|
||||
View::redirect("/tax/ustva/{$year}/{$month}", 'Ungültige Kennzahl.', 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
$calc = new UstvaCalculator();
|
||||
$kzData = $calc->calculate($year, $month);
|
||||
$bookings = $calc->fetchBookingsForKz($year, $month, $kz);
|
||||
|
||||
$months = ['Januar','Februar','März','April','Mai','Juni','Juli','August','September','Oktober','November','Dezember'];
|
||||
|
||||
$view = new View();
|
||||
$view->render('layout/base.php', [
|
||||
'pageTitle' => "Kz {$kz} – {$months[$month-1]} {$year}",
|
||||
'content' => $this->renderView('tax/ustva_kz_bookings', [
|
||||
'year' => $year,
|
||||
'month' => $month,
|
||||
'kz' => $kz,
|
||||
'kzName' => $kzData[$kz]['name'] ?? '',
|
||||
'bookings' => $bookings,
|
||||
'months' => $months,
|
||||
]),
|
||||
]);
|
||||
}
|
||||
|
||||
public function downloadXml(array $params): void
|
||||
{
|
||||
$year = (int)$params['year'];
|
||||
|
|
|
|||
|
|
@ -182,14 +182,113 @@ class Booking
|
|||
return $reversalId;
|
||||
}
|
||||
|
||||
public static function upsertEigenanteil30(
|
||||
int $belegId,
|
||||
float $betragNetto,
|
||||
float $ustSatz,
|
||||
string $buchungsDatum
|
||||
): void {
|
||||
$acc8922 = Account::findByKontonummer('8922');
|
||||
$acc1800 = Account::findByKontonummer('1800');
|
||||
$acc1776 = Account::findByKontonummer('1776');
|
||||
|
||||
if (!$acc8922 || !$acc1800) {
|
||||
throw new \RuntimeException('Konten 1800 und/oder 8922 nicht gefunden.');
|
||||
}
|
||||
|
||||
$netto30 = round($betragNetto * 0.3, 2);
|
||||
$ust30 = round($betragNetto * $ustSatz / 100 * 0.3, 2);
|
||||
$isTest = Setting::isTestMode();
|
||||
|
||||
// Buchung 1: 30% Netto an 8922 (Eigenanteil Betriebsausgabe)
|
||||
self::upsertKorrekturbuchung(
|
||||
belegId: $belegId,
|
||||
kategorieId: (int)$acc8922['id'],
|
||||
zahlungsId: (int)$acc1800['id'],
|
||||
betragNetto: $netto30,
|
||||
buchungsDatum: $buchungsDatum,
|
||||
buchungstext: '30% Eigenanteil (nicht abzugsfähig)',
|
||||
isTest: $isTest,
|
||||
);
|
||||
|
||||
// Buchung 2: 30% USt-Korrektur 1800→1776 (nur wenn USt > 0)
|
||||
if ($ustSatz > 0 && $acc1776) {
|
||||
self::upsertKorrekturbuchung(
|
||||
belegId: $belegId,
|
||||
kategorieId: (int)$acc1776['id'],
|
||||
zahlungsId: (int)$acc1800['id'],
|
||||
betragNetto: $ust30,
|
||||
buchungsDatum: $buchungsDatum,
|
||||
buchungstext: '30% Vorsteuerkorrektur Eigenanteil',
|
||||
isTest: $isTest,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
private static function upsertKorrekturbuchung(
|
||||
int $belegId,
|
||||
int $kategorieId,
|
||||
int $zahlungsId,
|
||||
float $betragNetto,
|
||||
string $buchungsDatum,
|
||||
string $buchungstext,
|
||||
bool $isTest,
|
||||
): void {
|
||||
$existing = Database::fetchOne(
|
||||
'SELECT * FROM bookings
|
||||
WHERE beleg_id = ? AND kategorie_konto_id = ? AND zahlungskonto_id = ?
|
||||
AND is_reversal = 0 AND reversed_by IS NULL
|
||||
LIMIT 1',
|
||||
[$belegId, $kategorieId, $zahlungsId]
|
||||
);
|
||||
|
||||
if ($existing) {
|
||||
if ($isTest) {
|
||||
Database::execute(
|
||||
'UPDATE bookings
|
||||
SET betrag_netto = ?, betrag_ust = 0, betrag_brutto = ?,
|
||||
buchungs_datum = ?, buchungstext = ?
|
||||
WHERE id = ?',
|
||||
[$betragNetto, $betragNetto, $buchungsDatum, $buchungstext, $existing['id']]
|
||||
);
|
||||
} else {
|
||||
self::reverse((int)$existing['id'], 'Anpassung 30% Eigenanteil');
|
||||
self::create([
|
||||
'beleg_id' => $belegId,
|
||||
'buchungs_datum' => $buchungsDatum,
|
||||
'buchungstyp' => 'Ausgabe',
|
||||
'kategorie_konto_id' => $kategorieId,
|
||||
'zahlungskonto_id' => $zahlungsId,
|
||||
'betrag_netto' => $betragNetto,
|
||||
'ust_satz' => 0,
|
||||
'steuerart' => 'normal',
|
||||
'buchungstext' => $buchungstext,
|
||||
]);
|
||||
}
|
||||
} else {
|
||||
self::create([
|
||||
'beleg_id' => $belegId,
|
||||
'buchungs_datum' => $buchungsDatum,
|
||||
'buchungstyp' => 'Ausgabe',
|
||||
'kategorie_konto_id' => $kategorieId,
|
||||
'zahlungskonto_id' => $zahlungsId,
|
||||
'betrag_netto' => $betragNetto,
|
||||
'ust_satz' => 0,
|
||||
'steuerart' => 'normal',
|
||||
'buchungstext' => $buchungstext,
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
public static function generateBuchungsNr(): string
|
||||
{
|
||||
$year = date('Y');
|
||||
$result = Database::fetchOne(
|
||||
'SELECT COUNT(*) as cnt FROM bookings WHERE YEAR(buchungs_datum) = ?',
|
||||
[$year]
|
||||
"SELECT MAX(CAST(SUBSTRING_INDEX(buchungs_nr, '-', -1) AS UNSIGNED)) as maxnr
|
||||
FROM bookings WHERE buchungs_nr LIKE ?",
|
||||
["BU-{$year}-%"]
|
||||
);
|
||||
$next = (int)($result['cnt'] ?? 0) + 1;
|
||||
$next = (int)($result['maxnr'] ?? 0) + 1;
|
||||
return sprintf('BU-%s-%06d', $year, $next);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -8,8 +8,7 @@ use Steuerkram\Models\Setting;
|
|||
|
||||
/**
|
||||
* Erzeugt eine ELSTER-konforme XML-Datei für die UStVA.
|
||||
* Kompatibel mit dem Elster Online Portal (Dateiupload, keine ERiC-Signatur nötig).
|
||||
* Schema: ElsterOnline v11
|
||||
* Format: Anmeldungssteuern Upload-Schema v{year} (kein ERiC nötig).
|
||||
*/
|
||||
class ElsterXmlService
|
||||
{
|
||||
|
|
@ -18,203 +17,151 @@ class ElsterXmlService
|
|||
$settings = Setting::get();
|
||||
|
||||
$steuernummer = preg_replace('/[^0-9]/', '', $settings['ustva_steuernummer'] ?? '');
|
||||
$bundesland = strtoupper($settings['ustva_bundesland'] ?? 'DE');
|
||||
$erstelltAm = date('Y-m-d\TH:i:s');
|
||||
$zeitraum = sprintf('%04d%02d', $year, $month);
|
||||
$firma = htmlspecialchars($settings['firma'] ?? '', ENT_XML1);
|
||||
$email = htmlspecialchars($settings['email'] ?? '', ENT_XML1);
|
||||
$inhaber = $settings['inhaber'] ?? $settings['firma'] ?? '';
|
||||
$firma = $settings['firma'] ?? '';
|
||||
|
||||
// Build Kz fields XML
|
||||
$kzXml = '';
|
||||
$kzMap = [
|
||||
'09' => 'Kz09', // Steuernummer (wird im Header gesetzt)
|
||||
'10' => 'Kz10', // Voranmeldungszeitraum
|
||||
'22' => 'Kz22', // Berichtigungsvoranmeldung (0=nein)
|
||||
'81' => 'Kz81',
|
||||
'86' => 'Kz86',
|
||||
'35' => 'Kz35',
|
||||
'41' => 'Kz41',
|
||||
'44' => 'Kz44',
|
||||
'83' => 'Kz83',
|
||||
'84' => 'Kz84',
|
||||
'66' => 'Kz66',
|
||||
'61' => 'Kz61',
|
||||
'62' => 'Kz62',
|
||||
'69' => 'Kz69',
|
||||
];
|
||||
[$strasse, $hausnummer] = $this->splitStrasse($settings['strasse'] ?? '');
|
||||
[$vorname, $nachname] = $this->splitName($inhaber);
|
||||
|
||||
// Helper: format amount for ELSTER (integer cents, no decimals for Beträge; or in EUR with 2 dec)
|
||||
$fmtBetrag = fn($v) => $v !== null ? number_format((float)$v, 2, '.', '') : '0.00';
|
||||
$fmtSteuer = fn($v) => $v !== null ? number_format((float)$v, 2, '.', '') : '0.00';
|
||||
$x = fn(string $v): string => htmlspecialchars($v, ENT_XML1, 'UTF-8');
|
||||
|
||||
$fields = [];
|
||||
|
||||
// Steuernummer und Voranmeldungszeitraum
|
||||
$fields['09'] = $steuernummer;
|
||||
$fields['10'] = $zeitraum;
|
||||
$fields['22'] = '0'; // Keine Berichtigungsvoranmeldung
|
||||
|
||||
foreach ($kz as $nr => $data) {
|
||||
if ($data['betrag'] !== null && $data['betrag'] != 0) {
|
||||
$fields[$nr . '_betrag'] = $fmtBetrag($data['betrag']);
|
||||
}
|
||||
if ($data['steuer'] !== null && $data['steuer'] != 0) {
|
||||
$fields[$nr . '_steuer'] = $fmtSteuer($data['steuer']);
|
||||
}
|
||||
}
|
||||
|
||||
// Build XML
|
||||
$dom = new \DOMDocument('1.0', 'UTF-8');
|
||||
$dom->formatOutput = true;
|
||||
|
||||
$elster = $dom->createElementNS('http://www.elster.de/elsterxml/schema/v11', 'Elster');
|
||||
$dom->appendChild($elster);
|
||||
|
||||
// TransferHeader
|
||||
$th = $dom->createElement('TransferHeader');
|
||||
$th->appendChild($dom->createElement('Version', '11'));
|
||||
$th->appendChild($dom->createElement('TransferTicket', ''));
|
||||
$th->appendChild($dom->createElement('Testmerker', '700000004'));
|
||||
$th->appendChild($dom->createElement('HerstellerID', '74931'));
|
||||
$th->appendChild($dom->createElement('DatenArt', 'UStVA'));
|
||||
$th->appendChild($dom->createElement('Vorgang', 'send-Auth'));
|
||||
$elster->appendChild($th);
|
||||
|
||||
// DatenTeil
|
||||
$dt = $dom->createElement('DatenTeil');
|
||||
$elster->appendChild($dt);
|
||||
|
||||
$nb = $dom->createElement('Nutzdatenblock');
|
||||
$dt->appendChild($nb);
|
||||
|
||||
// NutzdatenHeader
|
||||
$ndh = $dom->createElement('NutzdatenHeader');
|
||||
$ndh->setAttribute('version', '11');
|
||||
$ndh->appendChild($dom->createElement('NutzdatenTicket', '1'));
|
||||
$empf = $dom->createElement('Empfaenger');
|
||||
$empf->setAttribute('id', 'F');
|
||||
$empf->nodeValue = $this->getFinanzamtCode($bundesland);
|
||||
$ndh->appendChild($empf);
|
||||
$hersteller = $dom->createElement('Hersteller');
|
||||
$hersteller->appendChild($dom->createElement('ProduktName', 'Steuerkram'));
|
||||
$hersteller->appendChild($dom->createElement('ProduktVersion', '1.0'));
|
||||
$ndh->appendChild($hersteller);
|
||||
$nb->appendChild($ndh);
|
||||
|
||||
// Nutzdaten
|
||||
$nd = $dom->createElement('Nutzdaten');
|
||||
$nb->appendChild($nd);
|
||||
|
||||
// Anmeldungssteuern
|
||||
$anm = $dom->createElement('Anmeldungssteuern');
|
||||
$anm->setAttribute('art', 'UStVA');
|
||||
$anm->setAttribute('version', sprintf('%04d%02d', $year, $month));
|
||||
$nd->appendChild($anm);
|
||||
$ns = sprintf('http://finkonsens.de/elster/elsteranmeldung/ustva/v%d', $year);
|
||||
$anm = $dom->createElementNS($ns, 'Anmeldungssteuern');
|
||||
$anm->setAttributeNS('http://www.w3.org/2000/xmlns/', 'xmlns', $ns);
|
||||
$anm->setAttribute('version', (string)$year);
|
||||
$dom->appendChild($anm);
|
||||
|
||||
// DatenLieferant
|
||||
$dl = $dom->createElement('DatenLieferant');
|
||||
$dl->appendChild($dom->createElement('Name', $firma));
|
||||
$dl->appendChild($dom->createElement('Strasse', htmlspecialchars($settings['strasse'] ?? '', ENT_XML1)));
|
||||
$dl->appendChild($dom->createElement('PLZ', htmlspecialchars($settings['plz'] ?? '', ENT_XML1)));
|
||||
$dl->appendChild($dom->createElement('Ort', htmlspecialchars($settings['ort'] ?? '', ENT_XML1)));
|
||||
if ($email) $dl->appendChild($dom->createElement('Email', $email));
|
||||
foreach ([
|
||||
'Name' => $x($inhaber),
|
||||
'Strasse' => $x($settings['strasse'] ?? ''),
|
||||
'PLZ' => $x($settings['plz'] ?? ''),
|
||||
'Ort' => $x($settings['ort'] ?? ''),
|
||||
'Telefon' => $x($settings['telefon'] ?? ''),
|
||||
'Email' => $x($settings['email'] ?? ''),
|
||||
] as $tag => $val) {
|
||||
$dl->appendChild($dom->createElement($tag, $val));
|
||||
}
|
||||
$anm->appendChild($dl);
|
||||
|
||||
// Erstellungsdatum
|
||||
$anm->appendChild($dom->createElement('Erstellungsdatum', date('Ymd')));
|
||||
|
||||
// Steuerfall
|
||||
$sf = $dom->createElement('Steuerfall');
|
||||
$anm->appendChild($sf);
|
||||
|
||||
// Berater (leer)
|
||||
$berater = $dom->createElement('Berater');
|
||||
foreach ([
|
||||
'Bezeichnung','Name','Vorname','Namensvorsatz','Namenszusatz',
|
||||
'Str','Hausnummer','HNrZusatz','AnschriftenZusatz','Ort','PLZ',
|
||||
'AuslandsPLZ','Land','PostfachOrt','Postfach','PostfachPLZ',
|
||||
'GKPLZ','Telefon','Email',
|
||||
] as $tag) {
|
||||
$berater->appendChild($dom->createElement($tag));
|
||||
}
|
||||
$sf->appendChild($berater);
|
||||
|
||||
// Mandant
|
||||
$mandant = $dom->createElement('Mandant');
|
||||
foreach ([
|
||||
'Name' => $x($nachname),
|
||||
'Vorname' => $x($vorname),
|
||||
'MandantenNr' => '',
|
||||
'Bearbeiterkennzeichen' => '',
|
||||
] as $tag => $val) {
|
||||
$mandant->appendChild($dom->createElement($tag, $val));
|
||||
}
|
||||
$sf->appendChild($mandant);
|
||||
|
||||
// Unternehmer
|
||||
$unternehmer = $dom->createElement('Unternehmer');
|
||||
foreach ([
|
||||
'Bezeichnung' => $x($firma),
|
||||
'Name' => $x($nachname),
|
||||
'Vorname' => $x($vorname),
|
||||
'Namensvorsatz' => '',
|
||||
'Namenszusatz' => '',
|
||||
'Str' => $x($strasse),
|
||||
'Hausnummer' => $x($hausnummer),
|
||||
'HNrZusatz' => '',
|
||||
'AnschriftenZusatz' => '',
|
||||
'Ort' => $x($settings['ort'] ?? ''),
|
||||
'PLZ' => $x($settings['plz'] ?? ''),
|
||||
'AuslandsPLZ' => '',
|
||||
'Land' => 'Deutschland',
|
||||
'PostfachOrt' => '',
|
||||
'Postfach' => '',
|
||||
'PostfachPLZ' => '',
|
||||
'GKPLZ' => '',
|
||||
'Telefon' => $x($settings['telefon'] ?? ''),
|
||||
'Email' => $x($settings['email'] ?? ''),
|
||||
] as $tag => $val) {
|
||||
$unternehmer->appendChild($dom->createElement($tag, $val));
|
||||
}
|
||||
$sf->appendChild($unternehmer);
|
||||
|
||||
// Umsatzsteuervoranmeldung
|
||||
$ustva = $dom->createElement('Umsatzsteuervoranmeldung');
|
||||
$sf->appendChild($ustva);
|
||||
|
||||
// Jahr, Zeitraum
|
||||
$ustva->appendChild($dom->createElement('Jahr', (string)$year));
|
||||
$ustva->appendChild($dom->createElement('Zeitraum', sprintf('%02d', $month)));
|
||||
|
||||
// Steuernummer
|
||||
$ustva->appendChild($dom->createElement('Steuernummer', $steuernummer));
|
||||
|
||||
// Kz-Felder
|
||||
// Kz81 (Umsatz 19%)
|
||||
if (!empty($kz['81']['betrag'])) {
|
||||
$kzEl = $dom->createElement('Kz81', (string)round($kz['81']['betrag']));
|
||||
$ustva->appendChild($kzEl);
|
||||
// Kz-Felder: Bemessungsgrundlagen ganzzahlig, Steuerbeträge mit 2 Dezimalstellen.
|
||||
// Kz83/Kz84 werden nicht übertragen (serverseitig aus Kz81/Kz86 abgeleitet).
|
||||
// Kz69 (Zahllast) wird als Kz39 übertragen.
|
||||
$kzBetrag = fn(float $v): string => (string)(int)round($v);
|
||||
$kzSteuer = fn(float $v): string => number_format($v, 2, '.', '');
|
||||
|
||||
$betragsfelder = ['81', '86', '35', '41', '44', '89', '93'];
|
||||
foreach ($betragsfelder as $nr) {
|
||||
$val = (float)($kz[$nr]['betrag'] ?? 0.0);
|
||||
if ($val != 0.0) {
|
||||
$ustva->appendChild($dom->createElement('Kz' . $nr, $kzBetrag($val)));
|
||||
}
|
||||
// Kz86 (Umsatz 7%)
|
||||
if (!empty($kz['86']['betrag'])) {
|
||||
$kzEl = $dom->createElement('Kz86', (string)round($kz['86']['betrag']));
|
||||
$ustva->appendChild($kzEl);
|
||||
}
|
||||
// Kz35 (steuerfreie Umsätze)
|
||||
if (!empty($kz['35']['betrag'])) {
|
||||
$kzEl = $dom->createElement('Kz35', (string)round($kz['35']['betrag']));
|
||||
$ustva->appendChild($kzEl);
|
||||
|
||||
$steuerfelder = ['61', '62', '67', '66'];
|
||||
foreach ($steuerfelder as $nr) {
|
||||
$val = (float)($kz[$nr]['steuer'] ?? 0.0);
|
||||
if ($val != 0.0) {
|
||||
$ustva->appendChild($dom->createElement('Kz' . $nr, $kzSteuer($val)));
|
||||
}
|
||||
// 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);
|
||||
|
||||
// Zahllast / Erstattung → Kz39
|
||||
$zahllast = (float)($kz['69']['steuer'] ?? 0.0);
|
||||
$ustva->appendChild($dom->createElement('Kz39', $kzSteuer($zahllast)));
|
||||
|
||||
return $dom->saveXML();
|
||||
}
|
||||
|
||||
private function getFinanzamtCode(string $bundesland): string
|
||||
/**
|
||||
* Trennt Straßenname und Hausnummer. "Bietigheimer Str. 67" → ["Bietigheimer Str.", "67"]
|
||||
*/
|
||||
private function splitStrasse(string $strasse): array
|
||||
{
|
||||
// Standard-Finanzamt-Code pro Bundesland für ELSTER
|
||||
$codes = [
|
||||
'BW' => '2800', 'BY' => '9200', 'BE' => '1100', 'BB' => '3000',
|
||||
'HB' => '2400', 'HH' => '2200', 'HE' => '2600', 'MV' => '4000',
|
||||
'NI' => '2300', 'NW' => '5100', 'RP' => '2700', 'SL' => '1001',
|
||||
'SN' => '3200', 'ST' => '3100', 'SH' => '2100', 'TH' => '4100',
|
||||
];
|
||||
return $codes[$bundesland] ?? '9999';
|
||||
if (preg_match('/^(.*?)\s+(\d+\w*)$/', trim($strasse), $m)) {
|
||||
return [$m[1], $m[2]];
|
||||
}
|
||||
return [$strasse, ''];
|
||||
}
|
||||
|
||||
/**
|
||||
* Trennt Vorname und Nachname. "Felix Schepmann" → ["Felix", "Schepmann"]
|
||||
*/
|
||||
private function splitName(string $name): array
|
||||
{
|
||||
$parts = explode(' ', trim($name), 2);
|
||||
if (count($parts) === 2) {
|
||||
return [$parts[0], $parts[1]];
|
||||
}
|
||||
return ['', $name];
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -9,137 +9,134 @@ use Steuerkram\Database;
|
|||
/**
|
||||
* Berechnet alle ELSTER-Kennzahlen (Kz) für die Umsatzsteuervoranmeldung.
|
||||
*
|
||||
* Maßgeblich sind buchungstyp, steuerart, ust_satz und betrag_ust der Buchung.
|
||||
* Maßgeblich ist ausschließlich das Feld accounts.ustva_kz des Kategorie-Kontos.
|
||||
*
|
||||
* Buchungslogik:
|
||||
* 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
|
||||
* Kontozuordnung:
|
||||
* ustva_kz = '81' → Kz 81 (betrag_netto), Kz 83 (betrag_ust)
|
||||
* ustva_kz = '86' → Kz 86 (betrag_netto), Kz 84 (betrag_ust)
|
||||
* ustva_kz = '35' → Kz 35 (betrag_netto)
|
||||
* ustva_kz = '41' → Kz 41 (betrag_netto)
|
||||
* ustva_kz = '44' → Kz 44 (betrag_netto)
|
||||
* ustva_kz = '61' → Kz 61 (betrag_ust) [Betriebsausgabe-Konten mit VSt19]
|
||||
* ustva_kz = '62' → Kz 62 (betrag_ust) [Betriebsausgabe-Konten mit VSt7]
|
||||
* ustva_kz = '89' → Kz 89 (betrag_netto), Kz 91 (betrag_ust) [ig. Erwerb 19 %]
|
||||
* ustva_kz = '93' → Kz 93 (betrag_netto), Kz 94 (betrag_ust) [ig. Erwerb 7 %]
|
||||
*
|
||||
* 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)
|
||||
*/
|
||||
class UstvaCalculator
|
||||
{
|
||||
public function calculate(int $year, int $month): array
|
||||
{
|
||||
$kz = [
|
||||
// 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],
|
||||
/**
|
||||
* Welche accounts.ustva_kz-Werte liefern Buchungen für die Drill-down-Ansicht einer Kennzahl.
|
||||
*/
|
||||
private const KZ_ACCOUNT_MAP = [
|
||||
'81' => ['81'],
|
||||
'83' => ['81'],
|
||||
'86' => ['86'],
|
||||
'84' => ['86'],
|
||||
'35' => ['35'],
|
||||
'41' => ['41'],
|
||||
'44' => ['44'],
|
||||
'61' => ['61'],
|
||||
'62' => ['62'],
|
||||
'89' => ['89'],
|
||||
'91' => ['89'],
|
||||
'93' => ['93'],
|
||||
'94' => ['93'],
|
||||
'67' => ['89', '93'],
|
||||
'66' => ['61', '62', '89', '93'],
|
||||
'69' => ['81', '86', '35', '41', '44', '61', '62', '89', '93'],
|
||||
];
|
||||
|
||||
$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'];
|
||||
|
||||
// ── 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ── 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ── Abgeleitete Kennzahlen ───────────────────────────────────────
|
||||
$kz['83']['steuer'] = round($kz['81']['betrag'] * 0.19, 2);
|
||||
$kz['84']['steuer'] = round($kz['86']['betrag'] * 0.07, 2);
|
||||
|
||||
$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);
|
||||
}
|
||||
|
||||
return $kz;
|
||||
}
|
||||
|
||||
private function fetchBookings(int $year, int $month): array
|
||||
public function calculate(int $year, int $month): array
|
||||
{
|
||||
return Database::fetchAll(
|
||||
'SELECT bk.buchungstyp, bk.steuerart, bk.ust_satz, bk.betrag_netto, bk.betrag_ust
|
||||
$rows = Database::fetchAll(
|
||||
'SELECT a.ustva_kz,
|
||||
SUM(bk.betrag_netto) AS summe_netto,
|
||||
SUM(bk.betrag_ust) AS summe_ust
|
||||
FROM bookings bk
|
||||
JOIN fiscal_years fy ON bk.fiscal_year_id = fy.id
|
||||
JOIN accounts a ON a.id = bk.kategorie_konto_id
|
||||
WHERE fy.year = ? AND MONTH(bk.buchungs_datum) = ?
|
||||
AND bk.is_reversal = 0',
|
||||
AND bk.is_reversal = 0
|
||||
AND a.ustva_kz IS NOT NULL AND a.ustva_kz != \'\'
|
||||
GROUP BY a.ustva_kz',
|
||||
[$year, $month]
|
||||
);
|
||||
|
||||
$byKz = [];
|
||||
foreach ($rows as $row) {
|
||||
$byKz[$row['ustva_kz']] = [
|
||||
'netto' => (float)$row['summe_netto'],
|
||||
'ust' => (float)$row['summe_ust'],
|
||||
];
|
||||
}
|
||||
|
||||
$n = fn(string $kz): float => abs($byKz[$kz]['netto'] ?? 0.0);
|
||||
$u = fn(string $kz): float => abs($byKz[$kz]['ust'] ?? 0.0);
|
||||
|
||||
$kz81 = $n('81'); $kz83 = $u('81');
|
||||
$kz86 = $n('86'); $kz84 = $u('86');
|
||||
$kz35 = $n('35');
|
||||
$kz41 = $n('41');
|
||||
$kz44 = $n('44');
|
||||
$kz89 = $n('89'); $kz91 = $u('89');
|
||||
$kz93 = $n('93'); $kz94 = $u('93');
|
||||
$kz61 = $u('61');
|
||||
$kz62 = $u('62');
|
||||
$kz67 = $kz91 + $kz94;
|
||||
$kz66 = $kz61 + $kz62 + $kz67;
|
||||
$kz69 = ($kz83 + $kz84 + $kz91 + $kz94) - $kz66;
|
||||
|
||||
return [
|
||||
'81' => ['name' => 'Steuerpfl. Umsätze 19 %', 'betrag' => round($kz81, 2), 'steuer' => null],
|
||||
'83' => ['name' => 'USt auf steuerpfl. Umsätze 19 %', 'betrag' => null, 'steuer' => round($kz83, 2)],
|
||||
'86' => ['name' => 'Steuerpfl. Umsätze 7 %', 'betrag' => round($kz86, 2), 'steuer' => null],
|
||||
'84' => ['name' => 'USt auf steuerpfl. Umsätze 7 %', 'betrag' => null, 'steuer' => round($kz84, 2)],
|
||||
'35' => ['name' => 'Steuerfreie Umsätze (ohne Vorsteuerabzug)', 'betrag' => round($kz35, 2), 'steuer' => null],
|
||||
'41' => ['name' => 'Steuerfreie ig. Lieferungen (§ 4 Nr. 1b)', 'betrag' => round($kz41, 2), 'steuer' => null],
|
||||
'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)],
|
||||
'66' => ['name' => 'Vorsteuerbeträge gesamt', 'betrag' => null, 'steuer' => round($kz66, 2)],
|
||||
'69' => ['name' => 'Verbleibende USt-Vorauszahlung/Erstattung', 'betrag' => null, 'steuer' => round($kz69, 2)],
|
||||
];
|
||||
}
|
||||
|
||||
public function fetchBookingsForKz(int $year, int $month, string $kz): array
|
||||
{
|
||||
$accountKzList = self::KZ_ACCOUNT_MAP[$kz] ?? null;
|
||||
if ($accountKzList === null) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$placeholders = implode(',', array_fill(0, count($accountKzList), '?'));
|
||||
$params = array_merge([$year, $month], $accountKzList);
|
||||
|
||||
return Database::fetchAll(
|
||||
'SELECT bk.id, bk.buchungs_datum, bk.buchungstyp, bk.steuerart, bk.ust_satz,
|
||||
bk.betrag_netto, bk.betrag_ust, bk.betrag_brutto, bk.buchungstext,
|
||||
bk.beleg_id, bl.beleg_nr,
|
||||
k.kontonummer AS kategorie_nr, k.bezeichnung AS kategorie_name,
|
||||
z.kontonummer AS zahlungs_nr, z.bezeichnung AS zahlungs_name
|
||||
FROM bookings bk
|
||||
JOIN fiscal_years fy ON bk.fiscal_year_id = fy.id
|
||||
LEFT JOIN belege bl ON bk.beleg_id = bl.id
|
||||
JOIN accounts k ON bk.kategorie_konto_id = k.id
|
||||
JOIN accounts z ON bk.zahlungskonto_id = z.id
|
||||
WHERE fy.year = ? AND MONTH(bk.buchungs_datum) = ?
|
||||
AND bk.is_reversal = 0
|
||||
AND k.ustva_kz IN (' . $placeholders . ')
|
||||
ORDER BY bk.buchungs_datum, bk.id',
|
||||
$params
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -391,7 +391,7 @@ $suppliers = $suppliers ?? [];
|
|||
<div class="form-grid form-grid-2">
|
||||
<div class="form-group">
|
||||
<label>Nettobetrag (€) *</label>
|
||||
<input type="number" name="betrag_netto" step="0.01" min="0.01" required
|
||||
<input type="number" name="betrag_netto" step="0.01" required
|
||||
placeholder="0.00"
|
||||
value="<?= $h($formData['betrag_netto'] ?? '') ?>"
|
||||
class="<?= isset($formErrors['betrag_netto']) ? 'input-error' : '' ?>">
|
||||
|
|
@ -410,6 +410,13 @@ $suppliers = $suppliers ?? [];
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group" style="margin-bottom:.75rem">
|
||||
<label style="display:flex;align-items:center;gap:.5rem;cursor:pointer">
|
||||
<input type="checkbox" name="eigenanteil_30" value="1">
|
||||
30% Eigenanteil buchen
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<button type="submit" class="btn btn-primary">Buchung erfassen</button>
|
||||
</form>
|
||||
</div>
|
||||
|
|
|
|||
97
src/Views/tax/ustva_kz_bookings.php
Normal file
97
src/Views/tax/ustva_kz_bookings.php
Normal file
|
|
@ -0,0 +1,97 @@
|
|||
<?php
|
||||
use Steuerkram\View;
|
||||
$h = fn($v) => View::h($v);
|
||||
$fmtKz = fn($v) => number_format((float)$v, 2, ',', '.') . ' €';
|
||||
|
||||
// Für Kz 69: vorzeichenbehafteter USt-Beitrag zur Zahllast.
|
||||
// Einnahmen mit USt = positiv, Ausgaben mit Vorsteuer = negativ,
|
||||
// ig_erwerb heben sich gegenseitig auf → 0.
|
||||
$ustBeitragKz69 = function(array $b): float {
|
||||
$steuerart = $b['steuerart'] ?? 'normal';
|
||||
$ustSatz = (float)$b['ust_satz'];
|
||||
$betragUst = (float)$b['betrag_ust'];
|
||||
if ($b['buchungstyp'] === 'Einnahme' && $steuerart === 'normal' && $ustSatz > 0) {
|
||||
return $betragUst; // Umsatzsteuer → positiv
|
||||
}
|
||||
if ($b['buchungstyp'] === 'Ausgabe' && $steuerart === 'normal' && $betragUst != 0) {
|
||||
return -$betragUst; // Vorsteuer → Vorzeichen umkehren
|
||||
}
|
||||
return 0.0; // steuerfreie Umsätze und ig_erwerb (KZ91-KZ94 = KZ67, Netto-Beitrag 0)
|
||||
};
|
||||
|
||||
// Ausgaben werden als negative Beträge angezeigt (Geld fließt ab).
|
||||
$signedBetrag = fn(array $b, string $field): float =>
|
||||
$b['buchungstyp'] === 'Ausgabe' ? -abs((float)$b[$field]) : abs((float)$b[$field]);
|
||||
?>
|
||||
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<div>
|
||||
<div class="card-title">Kz <?= $h($kz) ?> – <?= $h($kzName) ?></div>
|
||||
<div class="text-muted text-sm"><?= $h($months[$month-1]) ?> <?= $h($year) ?></div>
|
||||
</div>
|
||||
<a href="/tax/ustva/<?= $h($year) ?>/<?= $h($month) ?>" class="btn btn-secondary btn-sm">← Zurück zur UStVA</a>
|
||||
</div>
|
||||
|
||||
<?php if (empty($bookings)): ?>
|
||||
<div class="card-body">
|
||||
<p class="text-muted">Keine Buchungen für diesen Zeitraum.</p>
|
||||
</div>
|
||||
<?php else: ?>
|
||||
<div class="table-wrap">
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Datum</th>
|
||||
<th>Beleg</th>
|
||||
<th>Buchungstext</th>
|
||||
<th>Kategorie</th>
|
||||
<th>Zahlungskonto</th>
|
||||
<th class="td-right">Netto</th>
|
||||
<th class="td-right"><?= $kz === '69' ? 'USt-Beitrag' : 'USt' ?></th>
|
||||
<th class="td-right">Brutto</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<?php
|
||||
$sumNetto = 0.0;
|
||||
$sumUst = 0.0;
|
||||
$sumBrutto = 0.0;
|
||||
foreach ($bookings as $b):
|
||||
$ust = $kz === '69' ? $ustBeitragKz69($b) : $signedBetrag($b, 'betrag_ust');
|
||||
$netto = $signedBetrag($b, 'betrag_netto');
|
||||
$brutto = $signedBetrag($b, 'betrag_brutto');
|
||||
$sumNetto += $netto;
|
||||
$sumUst += $ust;
|
||||
$sumBrutto += $brutto;
|
||||
?>
|
||||
<tr>
|
||||
<td class="td-mono text-sm"><?= $h($b['buchungs_datum']) ?></td>
|
||||
<td>
|
||||
<?php if ($b['beleg_id']): ?>
|
||||
<a href="/belege/<?= (int)$b['beleg_id'] ?>"><?= $h($b['beleg_nr'] ?? '#' . $b['beleg_id']) ?></a>
|
||||
<?php else: ?>
|
||||
<span class="text-muted">–</span>
|
||||
<?php endif; ?>
|
||||
</td>
|
||||
<td><?= $h($b['buchungstext']) ?></td>
|
||||
<td class="text-sm text-muted"><?= $h($b['kategorie_nr']) ?> <?= $h($b['kategorie_name']) ?></td>
|
||||
<td class="text-sm text-muted"><?= $h($b['zahlungs_nr']) ?> <?= $h($b['zahlungs_name']) ?></td>
|
||||
<td class="td-right td-mono"><?= $fmtKz($netto) ?></td>
|
||||
<td class="td-right td-mono"><?= $fmtKz($ust) ?></td>
|
||||
<td class="td-right td-mono"><?= $fmtKz($brutto) ?></td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
</tbody>
|
||||
<tfoot>
|
||||
<tr style="font-weight:700;border-top:2px solid #000">
|
||||
<td colspan="5">Summe (<?= count($bookings) ?> Buchungen)</td>
|
||||
<td class="td-right td-mono"><?= $fmtKz($sumNetto) ?></td>
|
||||
<td class="td-right td-mono"><?= $fmtKz($sumUst) ?></td>
|
||||
<td class="td-right td-mono"><?= $fmtKz($sumBrutto) ?></td>
|
||||
</tr>
|
||||
</tfoot>
|
||||
</table>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
|
|
@ -53,10 +53,18 @@ $fmtKz = fn($v) => $v !== null ? number_format((float)$v, 2, ',', '.') . ' €'
|
|||
<td class="td-mono" style="color:var(--color-muted);width:50px"><?= $h($nr) ?></td>
|
||||
<td><?= $h($field['name']) ?></td>
|
||||
<td class="td-right td-mono">
|
||||
<?= $field['betrag'] !== null ? $fmtKz($field['betrag']) : '–' ?>
|
||||
<?php if ($field['betrag'] !== null): ?>
|
||||
<a href="/tax/ustva/<?= $year ?>/<?= $month ?>/kz/<?= $nr ?>" style="text-decoration:none;color:inherit"><?= $fmtKz($field['betrag']) ?></a>
|
||||
<?php else: ?>
|
||||
<?= $isResult ? '' : '–' ?>
|
||||
<?php endif; ?>
|
||||
</td>
|
||||
<td class="td-right td-mono" style="<?= $isNegative ? 'color:var(--color-success)' : ($isResult ? 'color:var(--color-danger)' : '') ?>">
|
||||
<?= $field['steuer'] !== null ? $fmtKz($field['steuer']) : '–' ?>
|
||||
<?php if ($field['steuer'] !== null): ?>
|
||||
<a href="/tax/ustva/<?= $year ?>/<?= $month ?>/kz/<?= $nr ?>" style="text-decoration:none;color:inherit"><?= $fmtKz($field['steuer']) ?></a>
|
||||
<?php else: ?>
|
||||
–
|
||||
<?php endif; ?>
|
||||
</td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue