diff --git a/public/index.php b/public/index.php index 2e302da..8a1f8e9 100644 --- a/public/index.php +++ b/public/index.php @@ -86,7 +86,8 @@ $router->get('/invoices/{id}/edit', fn($p) => (new InvoiceController())->edit($p $router->post('/invoices/{id}', fn($p) => (new InvoiceController())->update($p)); $router->post('/invoices/{id}/finalize', fn($p) => (new InvoiceController())->finalize($p)); $router->get('/invoices/{id}/pdf', fn($p) => (new InvoiceController())->pdf($p)); -$router->post('/invoices/{id}/book', fn($p) => (new InvoiceController())->book($p)); +$router->get('/invoices/{id}/book', fn($p) => (new InvoiceController())->book($p)); +$router->post('/invoices/{id}/bookings', fn($p) => (new InvoiceController())->storeBooking($p)); $router->post('/invoices/{id}/mark-paid', fn($p) => (new InvoiceController())->markPaid($p)); $router->post('/invoices/{id}/cancel', fn($p) => (new InvoiceController())->cancel($p)); $router->post('/invoices/{id}/lines', fn($p) => (new InvoiceController())->addLine($p)); diff --git a/src/Controllers/InvoiceController.php b/src/Controllers/InvoiceController.php index 5a0f26f..311942e 100644 --- a/src/Controllers/InvoiceController.php +++ b/src/Controllers/InvoiceController.php @@ -8,6 +8,7 @@ use Steuerkram\Models\Invoice; use Steuerkram\Models\Customer; use Steuerkram\Models\Beleg; use Steuerkram\Models\Booking; +use Steuerkram\Models\Account; use Steuerkram\Models\Setting; use Steuerkram\Services\PdfInvoiceService; use Steuerkram\View; @@ -172,9 +173,9 @@ class InvoiceController \Steuerkram\Database::beginTransaction(); $nr = Invoice::finalize($id); $invoice = Invoice::find($id); - $belegNr = $this->createBelegAndBookings($invoice); + $belegNr = $this->createBeleg($invoice); \Steuerkram\Database::commit(); - View::redirect("/invoices/{$id}", "Rechnung {$nr} wurde finalisiert und gebucht (Beleg {$belegNr})."); + View::redirect("/invoices/{$id}", "Rechnung {$nr} wurde finalisiert (Beleg {$belegNr}). Bitte Zahlungseingang erfassen."); } catch (\PDOException $e) { \Steuerkram\Database::rollBack(); error_log('[Steuerkram] PDO-Fehler in InvoiceController::finalize(): ' . $e->getMessage()); @@ -207,27 +208,110 @@ class InvoiceController View::redirect("/invoices/{$id}", 'Rechnung muss zuerst finalisiert werden.', 'error'); return; } - if (!empty($invoice['beleg_id'])) { - View::redirect("/invoices/{$id}", 'Rechnung wurde bereits gebucht.', 'warning'); + if (empty($invoice['beleg_id'])) { + View::redirect("/invoices/{$id}", 'Kein Beleg zugeordnet.', 'error'); + return; + } + + $bookings = Booking::forBeleg((int)$invoice['beleg_id']); + $accounts = Account::all(['aktiv' => 1]); + $formErrors = $_GET['_error'] ?? []; + $formData = []; + + $view = new View(); + $view->render('layout/base.php', [ + 'pageTitle' => 'Zahlungseingang – ' . ($invoice['rechnungs_nr'] ?? 'Rechnung'), + 'content' => $this->renderView('invoices/book', [ + 'invoice' => $invoice, + 'bookings' => $bookings, + 'accounts' => $accounts, + 'formErrors' => $formErrors, + 'formData' => $formData, + ]), + ]); + } + + public function storeBooking(array $params): void + { + $id = (int)$params['id']; + $invoice = Invoice::find($id); + if (!$invoice) { http_response_code(404); return; } + + $belegId = (int)($invoice['beleg_id'] ?? 0); + if (!$belegId) { + View::redirect("/invoices/{$id}", 'Kein Beleg zugeordnet.', 'error'); + return; + } + + $data = $_POST; + $errors = []; + + if (empty($data['kategorie_konto_id'])) $errors['kategorie_konto_id'] = 'Pflichtfeld'; + if (empty($data['zahlungskonto_id'])) $errors['zahlungskonto_id'] = 'Pflichtfeld'; + if (empty($data['buchungs_datum'])) $errors['buchungs_datum'] = 'Pflichtfeld'; + if (!isset($data['betrag_netto']) || $data['betrag_netto'] === '') + $errors['betrag_netto'] = 'Pflichtfeld'; + if (empty($data['buchungstext'])) $errors['buchungstext'] = 'Pflichtfeld'; + + if (empty($errors) && $data['kategorie_konto_id'] === $data['zahlungskonto_id']) { + $errors['zahlungskonto_id'] = 'Kategorie- und Zahlungskonto dürfen nicht identisch sein.'; + } + + if ($errors) { + $bookings = Booking::forBeleg($belegId); + $accounts = Account::all(['aktiv' => 1]); + $view = new View(); + $view->render('layout/base.php', [ + 'pageTitle' => 'Zahlungseingang – ' . ($invoice['rechnungs_nr'] ?? 'Rechnung'), + 'content' => $this->renderView('invoices/book', [ + 'invoice' => $invoice, + 'bookings' => $bookings, + 'accounts' => $accounts, + 'formErrors' => $errors, + 'formData' => $data, + ]), + ]); return; } try { - \Steuerkram\Database::beginTransaction(); - $belegNr = $this->createBelegAndBookings($invoice); - \Steuerkram\Database::commit(); - View::redirect("/invoices/{$id}", 'Rechnung erfolgreich gebucht (Beleg ' . $belegNr . ').'); + $data['beleg_id'] = $belegId; + $data['buchungstyp'] = 'Einnahme'; + Booking::create($data); + View::redirect("/invoices/{$id}/book", 'Buchung erfasst.'); } catch (\PDOException $e) { - \Steuerkram\Database::rollBack(); - error_log('[Steuerkram] PDO-Fehler in InvoiceController::book(): ' . $e->getMessage()); - View::redirect("/invoices/{$id}", 'Datenbankfehler beim Buchen der Rechnung.', 'error'); + error_log('[Steuerkram] PDO-Fehler in InvoiceController::storeBooking(): ' . $e->getMessage()); + $bookings = Booking::forBeleg($belegId); + $accounts = Account::all(['aktiv' => 1]); + $view = new View(); + $view->render('layout/base.php', [ + 'pageTitle' => 'Zahlungseingang – ' . ($invoice['rechnungs_nr'] ?? 'Rechnung'), + 'content' => $this->renderView('invoices/book', [ + 'invoice' => $invoice, + 'bookings' => $bookings, + 'accounts' => $accounts, + 'formErrors' => ['_exception' => 'Datenbankfehler beim Speichern der Buchung.'], + 'formData' => $data, + ]), + ]); } catch (\Exception $e) { - \Steuerkram\Database::rollBack(); - View::redirect("/invoices/{$id}", $e->getMessage(), 'error'); + $bookings = Booking::forBeleg($belegId); + $accounts = Account::all(['aktiv' => 1]); + $view = new View(); + $view->render('layout/base.php', [ + 'pageTitle' => 'Zahlungseingang – ' . ($invoice['rechnungs_nr'] ?? 'Rechnung'), + 'content' => $this->renderView('invoices/book', [ + 'invoice' => $invoice, + 'bookings' => $bookings, + 'accounts' => $accounts, + 'formErrors' => ['_exception' => $e->getMessage()], + 'formData' => $data, + ]), + ]); } } - private function createBelegAndBookings(array $invoice): string + private function createBeleg(array $invoice): string { $id = (int)$invoice['id']; @@ -235,7 +319,7 @@ class InvoiceController $belegId = Beleg::create([ 'beleg_nr' => $belegNr, 'filename' => $invoice['rechnungs_nr'] . '.pdf', - 'storage_path' => '', // no physical file for outgoing invoices + 'storage_path' => '', 'mime_type' => 'application/pdf', 'file_hash' => hash('sha256', $invoice['rechnungs_nr']), 'beleg_datum' => $invoice['rechnungs_datum'], @@ -246,52 +330,6 @@ class InvoiceController 'notes' => 'Automatisch erstellt aus Rechnung ' . $invoice['rechnungs_nr'], ]); - $zahlungskonto = \Steuerkram\Database::fetchOne( - 'SELECT id FROM accounts WHERE kontonummer = ?', ['1400'] - ); - if (!$zahlungskonto) { - throw new \RuntimeException('Forderungskonto 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) { - $kategorieId = $erloesKonten['8000'] ?? null; - } elseif (abs($satz - 7) < 0.01) { - $kategorieId = $erloesKonten['8200'] ?? null; - } else { - $kategorieId = $erloesKonten['8400'] ?? null; - } - if (!$kategorieId) { - throw new \RuntimeException("Erlöskonto für USt-Satz {$satz}% nicht gefunden."); - } - - Booking::create([ - 'beleg_id' => $belegId, - 'buchungs_datum' => $invoice['rechnungs_datum'], - 'buchungstyp' => 'Einnahme', - 'kategorie_konto_id' => $kategorieId, - 'zahlungskonto_id' => $zahlungskonto['id'], - '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] ); diff --git a/src/Views/belege/detail.php b/src/Views/belege/detail.php index c9f6a7d..ddaf798 100644 --- a/src/Views/belege/detail.php +++ b/src/Views/belege/detail.php @@ -301,7 +301,7 @@ $suppliers = $suppliers ?? [];
| Buchungsnr. | +Zahldatum | +Kategorie | +Zahlungskonto | +Netto | +USt | +Brutto | +Text | +
|---|---|---|---|---|---|---|---|
| = $h($bk['buchungs_nr']) ?> | += View::formatDate($bk['buchungs_datum']) ?> | += $h($bk['kategorie_nr']) ?> = $h($bk['kategorie_name']) ?> |
+ = $h($bk['zahlungs_nr']) ?> = $h($bk['zahlungs_name']) ?> |
+ = $fmt($bk['betrag_netto']) ?> | += number_format((float)$bk['ust_satz'], 0) ?> % | += $fmt($bk['betrag_brutto']) ?> | ++ = $h($bk['buchungstext']) ?> + + Storno + + Storniert + + | +
| Noch keine Buchungen. | |||||||
Alle Zahlungseingänge erfasst? Dann die Rechnung als bezahlt markieren.
+ +