Fix #2: Ausgangsrechnung automatisch buchen beim Finalisieren

Beim Finalisieren einer Rechnung wurde bisher kein Beleg und keine
Buchungszeilen erstellt – der Nutzer musste danach manuell "Buchen"
klicken, was nicht offensichtlich war.

- finalize() erstellt jetzt atomar Beleg + Buchungszeilen (via
  createBelegAndBookings) innerhalb derselben Transaktion
- Buchungslogik aus book() in private Methode extrahiert (DRY),
  book()-Route bleibt für bereits finalisierte, noch ungebuchte
  Rechnungen erhalten
- "Buchen"-Button in der Show-View entfernt (nicht mehr nötig)
This commit is contained in:
fds2610 2026-03-25 21:11:30 +01:00
parent 2942b663ec
commit 08b99e3992
2 changed files with 79 additions and 73 deletions

View file

@ -169,12 +169,18 @@ class InvoiceController
{
$id = (int)$params['id'];
try {
\Steuerkram\Database::beginTransaction();
$nr = Invoice::finalize($id);
View::redirect("/invoices/{$id}", "Rechnung {$nr} wurde finalisiert.");
$invoice = Invoice::find($id);
$belegNr = $this->createBelegAndBookings($invoice);
\Steuerkram\Database::commit();
View::redirect("/invoices/{$id}", "Rechnung {$nr} wurde finalisiert und gebucht (Beleg {$belegNr}).");
} catch (\PDOException $e) {
\Steuerkram\Database::rollBack();
error_log('[Steuerkram] PDO-Fehler in InvoiceController::finalize(): ' . $e->getMessage());
View::redirect("/invoices/{$id}", 'Datenbankfehler beim Finalisieren der Rechnung.', 'error');
} catch (\Exception $e) {
\Steuerkram\Database::rollBack();
View::redirect("/invoices/{$id}", $e->getMessage(), 'error');
}
}
@ -208,72 +214,7 @@ class InvoiceController
try {
\Steuerkram\Database::beginTransaction();
// Create Beleg for this invoice
$belegNr = Beleg::generateBelegNr();
$belegId = Beleg::create([
'beleg_nr' => $belegNr,
'filename' => $invoice['rechnungs_nr'] . '.pdf',
'storage_path' => '', // no physical file for outgoing invoices
'mime_type' => 'application/pdf',
'file_hash' => hash('sha256', $invoice['rechnungs_nr']),
'beleg_datum' => $invoice['rechnungs_datum'],
'eingangs_datum' => $invoice['rechnungs_datum'],
'rechnungs_nr' => $invoice['rechnungs_nr'],
'betrag_brutto' => $invoice['betrag_brutto'],
'belegtyp' => 'Ausgangsrechnung',
'notes' => 'Automatisch erstellt aus Rechnung ' . $invoice['rechnungs_nr'],
]);
$debitorKonto = \Steuerkram\Database::fetchOne(
'SELECT id FROM accounts WHERE kontonummer = ?', ['1400']
);
// Erlöse-Konten nach USt-Satz: 8000 (19%), 8200 (7%), 8400 (0%/steuerfrei)
$erloesKonten = [];
foreach (\Steuerkram\Database::fetchAll(
"SELECT id, kontonummer FROM accounts WHERE kontonummer IN ('8000','8200','8400')"
) as $row) {
$erloesKonten[$row['kontonummer']] = $row['id'];
}
// Buchungen pro USt-Satz aus den Rechnungszeilen
$lines = Invoice::lines($id);
$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) {
$habenId = $erloesKonten['8000'] ?? null;
} elseif (abs($satz - 7) < 0.01) {
$habenId = $erloesKonten['8200'] ?? null;
} else {
$habenId = $erloesKonten['8400'] ?? null;
}
if (!$habenId) {
throw new \RuntimeException("Erlöskonto für USt-Satz {$satz}% nicht gefunden.");
}
Booking::create([
'beleg_id' => $belegId,
'buchungs_datum' => $invoice['rechnungs_datum'],
'soll_konto_id' => $debitorKonto['id'],
'haben_konto_id' => $habenId,
'betrag_netto' => round($netto, 2),
'ust_satz' => $satz,
'buchungstext' => 'Ausgangsrechnung ' . $invoice['rechnungs_nr'],
]);
}
// Link beleg to invoice
\Steuerkram\Database::execute(
'UPDATE invoices SET beleg_id=? WHERE id=?', [$belegId, $id]
);
$belegNr = $this->createBelegAndBookings($invoice);
\Steuerkram\Database::commit();
View::redirect("/invoices/{$id}", 'Rechnung erfolgreich gebucht (Beleg ' . $belegNr . ').');
} catch (\PDOException $e) {
@ -286,6 +227,77 @@ class InvoiceController
}
}
private function createBelegAndBookings(array $invoice): string
{
$id = (int)$invoice['id'];
$belegNr = Beleg::generateBelegNr();
$belegId = Beleg::create([
'beleg_nr' => $belegNr,
'filename' => $invoice['rechnungs_nr'] . '.pdf',
'storage_path' => '', // no physical file for outgoing invoices
'mime_type' => 'application/pdf',
'file_hash' => hash('sha256', $invoice['rechnungs_nr']),
'beleg_datum' => $invoice['rechnungs_datum'],
'eingangs_datum' => $invoice['rechnungs_datum'],
'rechnungs_nr' => $invoice['rechnungs_nr'],
'betrag_brutto' => $invoice['betrag_brutto'],
'belegtyp' => 'Ausgangsrechnung',
'notes' => 'Automatisch erstellt aus Rechnung ' . $invoice['rechnungs_nr'],
]);
$debitorKonto = \Steuerkram\Database::fetchOne(
'SELECT id FROM accounts WHERE kontonummer = ?', ['1400']
);
if (!$debitorKonto) {
throw new \RuntimeException('Debitorenkonto 1400 nicht gefunden.');
}
$erloesKonten = [];
foreach (\Steuerkram\Database::fetchAll(
"SELECT id, kontonummer FROM accounts WHERE kontonummer IN ('8000','8200','8400')"
) as $row) {
$erloesKonten[$row['kontonummer']] = $row['id'];
}
$lines = Invoice::lines($id);
$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) {
$habenId = $erloesKonten['8000'] ?? null;
} elseif (abs($satz - 7) < 0.01) {
$habenId = $erloesKonten['8200'] ?? null;
} else {
$habenId = $erloesKonten['8400'] ?? null;
}
if (!$habenId) {
throw new \RuntimeException("Erlöskonto für USt-Satz {$satz}% nicht gefunden.");
}
Booking::create([
'beleg_id' => $belegId,
'buchungs_datum' => $invoice['rechnungs_datum'],
'soll_konto_id' => $debitorKonto['id'],
'haben_konto_id' => $habenId,
'betrag_netto' => round($netto, 2),
'ust_satz' => $satz,
'buchungstext' => 'Ausgangsrechnung ' . $invoice['rechnungs_nr'],
]);
}
\Steuerkram\Database::execute(
'UPDATE invoices SET beleg_id=? WHERE id=?', [$belegId, $id]
);
return $belegNr;
}
public function markPaid(array $params): void
{
Invoice::markPaid((int)$params['id']);

View file

@ -39,12 +39,6 @@ $badgeClass = match($invoice['status']) {
<?php if ($invoice['status'] !== 'Entwurf'): ?>
<a href="/invoices/<?= $h($invoice['id']) ?>/pdf" class="btn btn-primary">PDF herunterladen</a>
<?php endif; ?>
<?php if ($invoice['status'] === 'Gesendet' && empty($invoice['beleg_id'])): ?>
<form method="post" action="/invoices/<?= $h($invoice['id']) ?>/book"
data-confirm="Rechnung jetzt buchen?">
<button type="submit" class="btn btn-success">Buchen</button>
</form>
<?php endif; ?>
<?php if ($invoice['status'] === 'Gesendet'): ?>
<form method="post" action="/invoices/<?= $h($invoice['id']) ?>/mark-paid">
<button type="submit" class="btn btn-success">Als bezahlt markieren</button>