Fix #8: Ausgangsrechnungen erscheinen jetzt in Dashboard, EÜR und UStVA

This commit is contained in:
fds2610 2026-03-27 07:21:05 +01:00
parent 29295989aa
commit 1950b3c892
3 changed files with 633 additions and 0 deletions

View file

@ -2,6 +2,19 @@
---
## 2026-03-27 – Ausgangsrechnungen in Dashboard/EÜR/UStVA (#8)
### Behoben
- **Seed-Skript**: `scripts/seed-testdata.php` rief `Invoice::finalize()` auf, ohne anschließend Beleg und Buchungen anzulegen – Ausgangsrechnungen fehlten daher in allen Auswertungen
- **Ursache**: `Invoice::finalize()` (Model) setzt nur Status und Rechnungsnummer; die Buchungserstellung liegt in `InvoiceController::createBelegAndBookings()` und wurde im Seed nicht aufgerufen
- **Fix**: Seed-Skript legt nach `Invoice::finalize()` nun Beleg und zugehörige Buchungen an (analog zur Controller-Logik), sodass Ausgangsrechnungen korrekt in EÜR, UStVA und Dashboard erscheinen
### Betrifft
- Skript: `scripts/seed-testdata.php`
- Test: `tests/09_InvoiceToReportsTest.php` (neu – End-to-End-Test Ausgangsrechnung → EÜR/UStVA/Dashboard)
---
## 2026-03-26 – Lieferantenmanagement (#7)
### Neu

480
scripts/seed-testdata.php Executable file
View file

@ -0,0 +1,480 @@
#!/usr/bin/env php
<?php
declare(strict_types=1);
/**
* Testdaten-Seed für Steuerkram
*
* Erzeugt:
* - 1 Kunde (TechVentures GmbH)
* - 30 Eingangsbelege mit je 1–2 Buchungszeilen (Jan–Mär 2026)
* Schwerpunkt: Reisekosten und Geschäftsessen (freiberuflicher Unternehmensberater)
* - 3 Ausgangsrechnungen (Januar, Februar, März 2026)
*
* Verwendung:
* php scripts/seed-testdata.php
*/
$projectRoot = dirname(__DIR__);
if (!file_exists($projectRoot . '/vendor/autoload.php')) {
fwrite(STDERR, "Fehler: vendor/autoload.php nicht gefunden. Bitte 'composer install' ausführen.\n");
exit(1);
}
require $projectRoot . '/vendor/autoload.php';
\Steuerkram\Config::load();
use Steuerkram\Database;
use Steuerkram\Models\Beleg;
use Steuerkram\Models\Booking;
use Steuerkram\Models\Customer;
use Steuerkram\Models\Invoice;
/** Konto-ID anhand der Kontonummer ermitteln */
function kontoId(string $nr): int
{
$row = Database::fetchOne('SELECT id FROM accounts WHERE kontonummer = ?', [$nr]);
if (!$row) {
throw new \RuntimeException("Konto {$nr} nicht in der Datenbank gefunden.");
}
return (int)$row['id'];
}
// ── Konten laden ─────────────────────────────────────────────────────────────
$bank = kontoId('1200'); // Bank (Zahlungskonto)
$reise = kontoId('4460'); // Reisekosten Arbeitnehmer
$bewirt = kontoId('4540'); // Bewirtungskosten
$telefon = kontoId('4630'); // Telefon
$inet = kontoId('4640'); // Internetkosten
$buerob = kontoId('4600'); // Bürobedarf
$edv = kontoId('4660'); // EDV-Kosten (Software, Lizenzen)
$fortb = kontoId('4670'); // Betriebliche Fortbildungskosten
$fachlit = kontoId('4680'); // Fachliteratur
echo "Konten geladen.\n";
// ── Kunde anlegen ─────────────────────────────────────────────────────────────
$customerId = Customer::create([
'firma' => 'TechVentures GmbH',
'anrede' => 'Herr',
'vorname' => 'Michael',
'nachname' => 'Schmidt',
'strasse' => 'Goethestraße 15',
'plz' => '60313',
'ort' => 'Frankfurt am Main',
'land' => 'DE',
'email' => 'm.schmidt@techventures.de',
'telefon' => '+49 69 123456-0',
'ust_id' => 'DE234567890',
'payment_days' => 30,
'notes' => 'Hauptkunde – IT-Beratung und Digitalisierungsprojekte',
]);
echo "Kunde angelegt: TechVentures GmbH (ID {$customerId})\n";
// ── Eingangsbelege ────────────────────────────────────────────────────────────
// Schema je Beleg:
// [datum, lieferant, ext_rechnungs_nr, belegtyp, notes, [
// [konto_id, betrag_netto, ust_satz, buchungstext],
// ...
// ]]
$belegDaten = [
// ══════════════════════════════════════
// Januar 2026
// ══════════════════════════════════════
[
'2026-01-08', 'Deutsche Bahn AG', 'DB-2026-010891',
'Eingangsrechnung', 'Geschäftsreise Frankfurt–Hamburg',
[
[$reise, 89.90, 19, 'Zugticket Frankfurt–Hamburg (Hinfahrt, 1. Klasse Flexpreis)'],
[$reise, 84.50, 19, 'Zugticket Hamburg–Frankfurt (Rückfahrt, 1. Klasse Flexpreis)'],
],
],
[
'2026-01-09', 'Best Western Hotel Hamburg', 'BWH-2026-0342',
'Eingangsrechnung', '2 Nächte inkl. Frühstück, Kundentermin Hamburg',
[
[$reise, 189.00, 19, 'Übernachtung Best Western Hamburg – 2 Nächte (EZ)'],
[$bewirt, 28.00, 19, 'Frühstück Best Western Hamburg (2 Tage)'],
],
],
[
'2026-01-09', 'Hafen Grill Hamburg', 'HG-001-2026',
'Quittung', 'Geschäftsessen mit Kunden TechVentures (3 Personen)',
[
[$bewirt, 67.50, 19, 'Geschäftsessen Hafen Grill Hamburg – Projektbesprechung'],
],
],
[
'2026-01-09', 'FREE NOW GmbH', 'FN-2026-884231',
'Quittung', 'Taxi Hamburg Hafen nach Hotel',
[
[$reise, 22.30, 19, 'Taxi Hamburg – Hafen Grill zum Best Western Hotel'],
],
],
[
'2026-01-20', 'Deutsche Telekom AG', 'TEL-2026-01-774883',
'Eingangsrechnung', 'Mobilfunk Januar 2026',
[
[$telefon, 39.50, 19, 'Mobilfunkvertrag Telekom Business – Januar 2026'],
],
],
[
'2026-01-01', 'Microsoft Ireland Operations Ltd.', 'MSFT-2026-01-00123',
'Eingangsrechnung', 'Microsoft 365 Business – Monatslizenz',
[
[$edv, 10.90, 19, 'Microsoft 365 Business Premium – Monatsgebühr Januar 2026'],
],
],
[
'2026-01-15', 'Amazon Business', 'AMZ-BIZ-2026-0089123',
'Eingangsrechnung', 'Bürobedarf Januar',
[
[$buerob, 45.38, 19, 'Bürobedarf: Tonerkartusche HP, Druckpapier A4 (2×500 Blatt)'],
],
],
[
'2026-01-22', 'Deutsche Bahn AG', 'DB-2026-022745',
'Eingangsrechnung', 'Dienstreise Stuttgart–Berlin',
[
[$reise, 145.00, 19, 'ICE Stuttgart–Berlin (Hinfahrt, 1. Klasse Flexpreis)'],
[$reise, 4.50, 19, 'Sitzplatzreservierung ICE Stuttgart–Berlin'],
],
],
[
'2026-01-23', 'Restaurant Unter den Linden GmbH', 'UDL-2026-0018',
'Quittung', 'Strategiemeeting Berlin (4 Personen)',
[
[$bewirt, 89.20, 19, 'Geschäftsessen Unter den Linden Berlin – Strategiemeeting mit Projektpartnern'],
],
],
[
'2026-01-23', 'Starbucks Coffee Germany GmbH', 'SB-23012026',
'Quittung', 'Arbeitsgespräch Berlin',
[
[$bewirt, 18.40, 19, 'Starbucks Berlin – Arbeitsgespräch mit Projektleiter TechVentures'],
],
],
// ══════════════════════════════════════
// Februar 2026
// ══════════════════════════════════════
[
'2026-02-11', 'Sixt SE', 'SIXT-2026-0229812',
'Eingangsrechnung', 'Mietwagen Kundenbesuch München (3 Tage)',
[
[$reise, 195.00, 19, 'Mietwagen Sixt VW Passat – 3 Tage München'],
[$reise, 52.00, 19, 'Kraftstoff Mietwagen Sixt München'],
],
],
[
'2026-02-11', 'Hilton Frankfurt City Centre', 'HLT-2026-0452',
'Eingangsrechnung', 'Übernachtung für Kundentermin Frankfurt',
[
[$reise, 215.00, 19, 'Übernachtung Hilton Frankfurt City Centre – 2 Nächte (EZ)'],
],
],
[
'2026-02-12', 'Restaurant Metropol Frankfurt', 'METRO-2026-013',
'Quittung', 'Geschäftsessen Projektplanung Q1 (3 Personen)',
[
[$bewirt, 112.50, 19, 'Geschäftsessen Restaurant Metropol – Projektplanung Q1 mit Auftraggeber'],
],
],
[
'2026-02-18', 'Deutsche Lufthansa AG', 'LH-2026-0748221',
'Eingangsrechnung', 'Flug Stuttgart–Hamburg Hin/Rück (Kundenpräsentation)',
[
[$reise, 189.00, 19, 'Lufthansa Flug STR–HAM Hin/Rück – Economy Flex'],
],
],
[
'2026-02-20', '1&1 Versatel GmbH', '1U1-2026-02-883421',
'Eingangsrechnung', 'DSL-Anschluss Büro Februar 2026',
[
[$inet, 34.95, 19, 'DSL Business-Anschluss Büro – Februar 2026'],
],
],
[
'2026-02-01', 'Slack Technologies, Inc.', 'SLK-2026-02-00091',
'Eingangsrechnung', 'Slack Pro Monatsgebühr',
[
[$edv, 7.25, 19, 'Slack Pro Workspace – Monatsgebühr Februar 2026'],
],
],
[
'2026-02-01', 'Deutsche Bahn AG', 'DB-BC100-2026-02',
'Eingangsrechnung', 'BahnCard 100 erste Klasse – Monatsanteil',
[
[$reise, 308.40, 19, 'BahnCard 100 erste Klasse – Monatsanteil Februar 2026'],
],
],
[
'2026-02-25', 'IHK Frankfurt am Main', 'IHK-SEM-2026-0881',
'Eingangsrechnung', 'Fortbildung „Agiles Projektmanagement" (1 Tag)',
[
[$fortb, 295.00, 19, 'Seminar „Agiles Projektmanagement in der Beratungspraxis" – IHK Frankfurt'],
[$fachlit, 24.30, 7, 'Seminarunterlagen und Skript zur Fortbildung'],
],
],
[
'2026-02-26', 'La Dolce Vita GmbH', 'LDV-2026-0197',
'Quittung', 'Projektabschlussdinner mit Kunden (5 Personen)',
[
[$bewirt, 95.80, 19, 'Geschäftsessen La Dolce Vita – Projektabschlussdinner TechVentures'],
],
],
[
'2026-02-10', 'Staples Deutschland GmbH', 'STP-2026-0228817',
'Eingangsrechnung', 'Büromaterial Februar',
[
[$buerob, 38.49, 19, 'Büromaterial: Ordner, Hefter, Schreibutensilien'],
],
],
// ══════════════════════════════════════
// März 2026
// ══════════════════════════════════════
[
'2026-03-04', 'Deutsche Bahn AG', 'DB-2026-030428',
'Eingangsrechnung', 'Dienstreise Frankfurt–Berlin (Kundentermin)',
[
[$reise, 155.00, 19, 'ICE Frankfurt–Berlin Hin/Rück – 1. Klasse Flexpreis'],
],
],
[
'2026-03-05', 'Scandic Hotels Deutschland GmbH', 'SCA-BER-2026-0041',
'Eingangsrechnung', 'Übernachtung Berlin Kundentermin (2 Nächte)',
[
[$reise, 142.00, 19, 'Übernachtung Scandic Hotel Berlin – 2 Nächte (EZ)'],
[$reise, 18.00, 19, 'Parkhaus Scandic Hotel Berlin – 2 Tage'],
],
],
[
'2026-03-05', 'Restaurant Grüne Pause Berlin', 'GP-2026-0067',
'Quittung', 'Q1-Strategiereview Berlin (3 Personen)',
[
[$bewirt, 72.40, 19, 'Geschäftsessen Grüne Pause Berlin – Q1-Strategiereview mit Führungskräften'],
],
],
[
'2026-03-05', 'Uber Germany GmbH', 'UBR-2026-03050001',
'Quittung', 'Fahrt zum Kunden Berlin',
[
[$reise, 18.90, 19, 'Uber – Fahrt Scandic Hotel Berlin zum Kunden (TechVentures Berlin Office)'],
],
],
[
'2026-03-20', 'Vodafone GmbH', 'VOD-2026-03-449871',
'Eingangsrechnung', 'Mobilfunk März 2026',
[
[$telefon, 41.18, 19, 'Mobilfunkvertrag Vodafone Business – März 2026'],
],
],
[
'2026-03-01', 'Zoom Video Communications, Inc.', 'ZM-2026-03-0012',
'Eingangsrechnung', 'Zoom Pro – Jahresabo März-Anteil',
[
[$edv, 12.49, 19, 'Zoom Pro Meetings – Monatsanteil März 2026 (Jahresabonnement)'],
],
],
[
'2026-03-17', 'Deutsche Bahn AG', 'DB-2026-031744',
'Eingangsrechnung', 'Dienstreise Stuttgart–München (Workshop)',
[
[$reise, 89.00, 19, 'ICE Stuttgart–München (Hinfahrt, 1. Klasse Flexpreis)'],
[$reise, 82.00, 19, 'ICE München–Stuttgart (Rückfahrt, 1. Klasse Flexpreis)'],
],
],
[
'2026-03-12', 'Springer Fachmedien Wiesbaden GmbH', 'SFM-2026-0041229',
'Eingangsrechnung', 'Fachliteratur Unternehmensberatung',
[
[$fachlit, 34.90, 7, 'Fachbuch „Unternehmensberatung digital – Strategien und Methoden" (5. Aufl.)'],
],
],
[
'2026-03-20', 'Restaurant Seeblick Frankfurt GmbH', 'SEE-2026-0038',
'Quittung', 'Quartals-Review Q1 2026 mit Kunden (4 Personen)',
[
[$bewirt, 95.00, 19, 'Geschäftsessen Restaurant Seeblick – Q1-Quartals-Review TechVentures'],
[$bewirt, 23.50, 19, 'Getränke und Weinbegleitung – Quartals-Review TechVentures'],
],
],
[
'2026-03-25', 'Staples Deutschland GmbH', 'STP-2026-0332541',
'Eingangsrechnung', 'Büromaterial März',
[
[$buerob, 42.15, 19, 'Büromaterial: Kopierpapier, Druckerpatronen, Briefumschläge'],
],
],
];
// ── Belege und Buchungen anlegen ──────────────────────────────────────────────
$belegCount = 0;
$buchungCount = 0;
foreach ($belegDaten as $i => $b) {
[$datum, $lieferant, $extNr, $belegtyp, $notes, $zeilen] = $b;
$belegNr = Beleg::generateBelegNr();
$belegId = Beleg::create([
'beleg_nr' => $belegNr,
'filename' => str_replace(['/', ' '], ['-', '_'], $lieferant) . '_' . $extNr . '.pdf',
'storage_path' => '',
'mime_type' => 'application/pdf',
'file_hash' => hash('sha256', $belegNr . $extNr . $lieferant),
'beleg_datum' => $datum,
'eingangs_datum' => $datum,
'rechnungs_nr' => $extNr,
'betrag_brutto' => null, // wird aus Buchungszeilen berechnet
'belegtyp' => $belegtyp,
'notes' => $notes,
]);
foreach ($zeilen as $zeile) {
[$kontoId, $netto, $ust, $buchungstext] = $zeile;
Booking::create([
'beleg_id' => $belegId,
'buchungs_datum' => $datum,
'buchungstyp' => 'Ausgabe',
'kategorie_konto_id' => $kontoId,
'zahlungskonto_id' => $bank,
'betrag_netto' => $netto,
'ust_satz' => $ust,
'steuerart' => 'normal',
'buchungstext' => $buchungstext,
]);
$buchungCount++;
}
$belegCount++;
$zeilenAnz = count($zeilen);
$zeilenTxt = $zeilenAnz === 1 ? '1 Zeile' : "{$zeilenAnz} Zeilen";
echo sprintf(" [%02d] %s – %s (%s, %s)\n", $belegCount, $belegNr, $lieferant, $datum, $zeilenTxt);
}
echo "\n{$belegCount} Eingangsbelege mit {$buchungCount} Buchungszeilen angelegt.\n\n";
// ── Erlöskonten für Ausgangsrechnungs-Buchungen ───────────────────────────────
$forderungen = kontoId('1400'); // Forderungen aus Lieferungen und Leistungen
$erloes19 = kontoId('8000'); // Erlöse 19 % USt
$erloes7 = kontoId('8200'); // Erlöse 7 % (8200 entspricht Mapping im Controller)
$erloes0 = kontoId('8400'); // Erlöse steuerfrei/0 %
// ── Ausgangsrechnungen ────────────────────────────────────────────────────────
$rechnungen = [
[
'datum' => '2026-01-31',
'von' => '2025-12-01',
'bis' => '2025-12-31',
'betreff' => 'Unternehmensberatung Dezember 2025 – Strategieentwicklung',
'status' => 'Bezahlt',
'notes' => 'Projektnummer: TV-2025-Q4-STRAT',
'lines' => [
['bezeichnung' => 'Strategieberatung und Konzeptentwicklung', 'menge' => 15, 'einheit' => 'h', 'einzelpreis' => 150.00, 'ust_satz' => 19],
['bezeichnung' => 'Workshop-Moderation (ganztägig)', 'menge' => 2, 'einheit' => 'Tag', 'einzelpreis' => 1200.00, 'ust_satz' => 19],
['bezeichnung' => 'Ergebnispräsentation und Abschlussbericht', 'menge' => 4, 'einheit' => 'h', 'einzelpreis' => 150.00, 'ust_satz' => 19],
],
],
[
'datum' => '2026-02-28',
'von' => '2026-01-01',
'bis' => '2026-01-31',
'betreff' => 'Unternehmensberatung Januar 2026 – Change Management',
'status' => 'Bezahlt',
'notes' => 'Projektnummer: TV-2026-Q1-CHANGE',
'lines' => [
['bezeichnung' => 'Projektbegleitung Change Management', 'menge' => 20, 'einheit' => 'h', 'einzelpreis' => 150.00, 'ust_satz' => 19],
['bezeichnung' => 'Stakeholder-Interviews und Analyse', 'menge' => 8, 'einheit' => 'h', 'einzelpreis' => 150.00, 'ust_satz' => 19],
['bezeichnung' => 'Reisekostenpauschale (weiterberechnet)', 'menge' => 1, 'einheit' => 'Psch.', 'einzelpreis' => 380.00, 'ust_satz' => 19],
],
],
[
'datum' => '2026-03-25',
'von' => '2026-02-01',
'bis' => '2026-02-28',
'betreff' => 'Unternehmensberatung Februar 2026 – Digitalisierungsstrategie',
'status' => 'Gesendet',
'notes' => 'Projektnummer: TV-2026-Q1-DIGIT; Zahlungsziel 30 Tage',
'lines' => [
['bezeichnung' => 'Digitalisierungsberatung und Roadmap-Entwicklung', 'menge' => 25, 'einheit' => 'h', 'einzelpreis' => 150.00, 'ust_satz' => 19],
['bezeichnung' => 'Kundenpräsentation und Management Summary', 'menge' => 5, 'einheit' => 'h', 'einzelpreis' => 150.00, 'ust_satz' => 19],
],
],
];
foreach ($rechnungen as $r) {
$invId = Invoice::create([
'customer_id' => $customerId,
'betreff' => $r['betreff'],
'leistungszeitraum_von' => $r['von'],
'leistungszeitraum_bis' => $r['bis'],
'rechnungs_datum' => $r['datum'],
'notes' => $r['notes'],
'lines' => $r['lines'],
]);
$rechnungsNr = Invoice::finalize($invId);
$inv = Invoice::find($invId);
// Beleg und Buchungen anlegen (analog InvoiceController::createBelegAndBookings)
$belegNr = Beleg::generateBelegNr();
$belegId = Beleg::create([
'beleg_nr' => $belegNr,
'filename' => $rechnungsNr . '.pdf',
'storage_path' => '',
'mime_type' => 'application/pdf',
'file_hash' => hash('sha256', $rechnungsNr),
'beleg_datum' => $inv['rechnungs_datum'],
'eingangs_datum' => $inv['rechnungs_datum'],
'rechnungs_nr' => $rechnungsNr,
'betrag_brutto' => $inv['betrag_brutto'],
'belegtyp' => 'Ausgangsrechnung',
'notes' => 'Automatisch erstellt aus Rechnung ' . $rechnungsNr,
]);
$lines = Invoice::lines($invId);
$gruppenNetto = [];
foreach ($lines as $line) {
$satz = (float)$line['ust_satz'];
$netto = round((float)$line['menge'] * (float)$line['einzelpreis'], 2);
$gruppenNetto[$satz] = ($gruppenNetto[$satz] ?? 0.0) + $netto;
}
foreach ($gruppenNetto as $satz => $netto) {
if (abs($satz - 19) < 0.01) $kategorieId = $erloes19;
elseif (abs($satz - 7) < 0.01) $kategorieId = $erloes7;
else $kategorieId = $erloes0;
Booking::create([
'beleg_id' => $belegId,
'buchungs_datum' => $inv['rechnungs_datum'],
'buchungstyp' => 'Einnahme',
'kategorie_konto_id' => $kategorieId,
'zahlungskonto_id' => $forderungen,
'betrag_netto' => round($netto, 2),
'ust_satz' => $satz,
'buchungstext' => 'Ausgangsrechnung ' . $rechnungsNr,
]);
}
Database::execute('UPDATE invoices SET beleg_id=? WHERE id=?', [$belegId, $invId]);
if ($r['status'] === 'Bezahlt') {
Invoice::markPaid($invId);
}
$status = $inv['status'] ?? $r['status'];
if ($r['status'] === 'Bezahlt') $status = 'Bezahlt';
echo sprintf(
" %s | %s | Netto: %s € | Status: %s | Beleg: %s\n",
$rechnungsNr,
$r['datum'],
number_format((float)($inv['betrag_netto'] ?? 0), 2, ',', '.'),
$status,
$belegNr
);
}
echo "\n3 Ausgangsrechnungen für Kunde TechVentures GmbH angelegt.\n";
echo "\nTestdaten erfolgreich erstellt.\n";

View file

@ -0,0 +1,140 @@
<?php
declare(strict_types=1);
/**
* Test 9: Ausgangsrechnung → Dashboard / EÜR / UStVA.
*
* Prüft den vollständigen Pfad: Ausgangsrechnung anlegen, finalisieren,
* Buchung erzeugen und sicherstellen, dass die Werte im Dashboard,
* in der EÜR und in der UStVA erscheinen.
*/
class InvoiceToReportsTest extends TestCase
{
protected string $testName = 'Test 9: Ausgangsrechnung → Dashboard/EÜR/UStVA';
public function run(): void
{
$year = (int)date('Y');
$month = (int)date('n');
$datum = date('Y-m-d');
// ── Konten prüfen ────────────────────────────────────────────────────
$zahlungsId = $this->findAccountId('1400'); // Forderungen
$erloes19Id = $this->findAccountId('8000'); // Erlöse 19 %
if (!$zahlungsId || !$erloes19Id) {
$this->assert(false, 'Benötigte Konten (1400, 8000) nicht gefunden – SKR03-Seed vorhanden?');
return;
}
$this->assert(true, 'Konten 1400 und 8000 vorhanden');
// ── Kunde und Rechnung anlegen ────────────────────────────────────────
$customerId = \Steuerkram\Models\Customer::create([
'nachname' => 'ReportTest',
'firma' => 'Report GmbH',
'strasse' => 'Musterstraße 1',
'plz' => '10115',
'ort' => 'Berlin',
'payment_days' => 14,
]);
$invoiceId = \Steuerkram\Models\Invoice::create([
'customer_id' => $customerId,
'betreff' => 'Testrechnung für Report-Integration',
'rechnungs_datum' => $datum,
'leistungszeitraum_von' => date('Y-m-01'),
'leistungszeitraum_bis' => date('Y-m-t'),
'lines' => [
[
'bezeichnung' => 'Beratungsleistung',
'menge' => 10,
'einheit' => 'h',
'einzelpreis' => 100.00,
'ust_satz' => 19.0,
],
],
]);
$this->assertGreaterThan(0, $invoiceId, 'Rechnung angelegt');
// ── Finalisieren (Modell-Methode, wie in InvoiceController::finalize) ──
$rechnungsNr = \Steuerkram\Models\Invoice::finalize($invoiceId);
$this->assertNotEmpty($rechnungsNr, 'Rechnungsnummer nach Finalisierung vorhanden');
$invoice = \Steuerkram\Models\Invoice::find($invoiceId);
$this->assertEquals('Gesendet', $invoice['status'] ?? null, 'Status nach Finalisierung: Gesendet');
// ── Beleg und Buchungen anlegen (wie InvoiceController::createBelegAndBookings) ──
$belegNr = \Steuerkram\Models\Beleg::generateBelegNr();
$belegId = \Steuerkram\Models\Beleg::create([
'beleg_nr' => $belegNr,
'filename' => $rechnungsNr . '.pdf',
'storage_path' => '',
'mime_type' => 'application/pdf',
'file_hash' => hash('sha256', $rechnungsNr),
'beleg_datum' => $invoice['rechnungs_datum'],
'eingangs_datum' => $invoice['rechnungs_datum'],
'rechnungs_nr' => $rechnungsNr,
'betrag_brutto' => $invoice['betrag_brutto'],
'belegtyp' => 'Ausgangsrechnung',
'notes' => 'Test Ausgangsrechnung',
]);
$this->assertGreaterThan(0, $belegId, 'Beleg angelegt');
$lines = \Steuerkram\Models\Invoice::lines($invoiceId);
$this->assertGreaterThan(0, count($lines), 'Rechnungspositionen vorhanden');
$gruppenNetto = [];
foreach ($lines as $line) {
$satz = (float)$line['ust_satz'];
$netto = round((float)$line['menge'] * (float)$line['einzelpreis'], 2);
$gruppenNetto[$satz] = ($gruppenNetto[$satz] ?? 0.0) + $netto;
}
foreach ($gruppenNetto as $satz => $netto) {
$kategorieId = $erloes19Id; // vereinfacht für Test mit 19 %
\Steuerkram\Models\Booking::create([
'beleg_id' => $belegId,
'buchungs_datum' => $invoice['rechnungs_datum'],
'buchungstyp' => 'Einnahme',
'kategorie_konto_id' => $kategorieId,
'zahlungskonto_id' => $zahlungsId,
'betrag_netto' => round($netto, 2),
'ust_satz' => $satz,
'buchungstext' => 'Ausgangsrechnung ' . $rechnungsNr,
]);
}
\Steuerkram\Database::execute(
'UPDATE invoices SET beleg_id=? WHERE id=?', [$belegId, $invoiceId]
);
$this->assert(true, 'Buchungen und Beleg-Verknüpfung erstellt');
// ── EÜR prüfen ───────────────────────────────────────────────────────
$euer = (new \Steuerkram\Services\EuerCalculator())->calculate($year);
$this->assert(
(float)$euer['total_einnahmen'] >= 1000.00,
'EÜR: Einnahmen ≥ 1000 € (Ausgangsrechnung eingeflossen)'
);
// ── UStVA prüfen ─────────────────────────────────────────────────────
$kz = (new \Steuerkram\Services\UstvaCalculator())->calculate($year, $month);
$this->assert(
(float)$kz['81']['betrag'] >= 1000.00,
'UStVA Kz 81 (Umsätze 19 %): Betrag ≥ 1000 € (Ausgangsrechnung eingeflossen)'
);
// ── Dashboard-Abfrage prüfen ─────────────────────────────────────────
$umsatz = \Steuerkram\Database::fetchOne(
'SELECT COALESCE(SUM(bk.betrag_netto),0) as total
FROM bookings bk
JOIN fiscal_years fy ON bk.fiscal_year_id = fy.id
WHERE bk.buchungstyp = \'Einnahme\' AND bk.is_reversal = 0 AND fy.year = ?',
[$year]
);
$this->assert(
(float)($umsatz['total'] ?? 0) >= 1000.00,
'Dashboard-Abfrage: Umsatz ≥ 1000 € (Ausgangsrechnung eingeflossen)'
);
}
}