diff --git a/public/index.php b/public/index.php index dc3b910..422201f 100644 --- a/public/index.php +++ b/public/index.php @@ -104,6 +104,7 @@ $router->post('/invoices/{id}/delete', fn($p) => (new InvoiceController())->dest $router->post('/invoices/{id}/lines', fn($p) => (new InvoiceController())->addLine($p)); $router->post('/invoices/{id}/lines/{line_id}/delete', fn($p) => (new InvoiceController())->deleteLine($p)); $router->post('/invoices/{id}/send', fn($p) => (new InvoiceController())->send($p)); +$router->post('/invoices/{id}/quick-book', fn($p) => (new InvoiceController())->quickBook($p)); // ── Anlagevermögen ─────────────────────────────────────────── $router->get('/assets', fn($p) => (new AssetController())->index($p)); diff --git a/src/Controllers/InvoiceController.php b/src/Controllers/InvoiceController.php index 890674f..81045c2 100644 --- a/src/Controllers/InvoiceController.php +++ b/src/Controllers/InvoiceController.php @@ -119,6 +119,14 @@ class InvoiceController ); } + // Buchungen + Konten für das Zahlungs-Modal laden (nur wenn Beleg vorhanden) + $bookings = []; + $accounts = []; + if (!empty($invoice['beleg_id'])) { + $bookings = Booking::forBeleg((int)$invoice['beleg_id']); + $accounts = Account::all(['aktiv' => 1]); + } + $view = new View(); $view->render('layout/base.php', [ 'pageTitle' => 'Rechnung ' . ($invoice['rechnungs_nr'] ?? 'Entwurf'), @@ -128,6 +136,8 @@ class InvoiceController 'settings' => $settings, 'stornorechnung' => $stornorechnung ?: null, 'originalrechnung' => $originalrechnung, + 'bookings' => $bookings, + 'accounts' => $accounts, ]), ]); } @@ -340,6 +350,10 @@ class InvoiceController } if ($errors) { + if (!empty($data['_from_modal'])) { + View::redirect("/invoices/{$id}", implode(' ', $errors), 'error'); + return; + } $bookings = Booking::forBeleg($belegId); $accounts = Account::all(['aktiv' => 1]); $view = new View(); @@ -360,7 +374,8 @@ class InvoiceController $data['beleg_id'] = $belegId; $data['buchungstyp'] = 'Einnahme'; Booking::create($data); - View::redirect("/invoices/{$id}/book", 'Buchung erfasst.'); + $returnUrl = !empty($data['_from_modal']) ? "/invoices/{$id}" : "/invoices/{$id}/book"; + View::redirect($returnUrl, 'Buchung erfasst.'); } catch (\PDOException $e) { error_log('[Steuerkram] PDO-Fehler in InvoiceController::storeBooking(): ' . $e->getMessage()); $bookings = Booking::forBeleg($belegId); @@ -393,6 +408,30 @@ class InvoiceController } } + public function quickBook(array $params): void + { + $id = (int)$params['id']; + $invoice = Invoice::find($id); + if (!$invoice) { http_response_code(404); return; } + + $data = $_POST; + if (empty($data['kategorie_konto_id']) || empty($data['zahlungskonto_id']) || empty($data['buchungs_datum'])) { + View::redirect("/invoices/{$id}", 'Bitte Konto und Datum angeben.', 'error'); + return; + } + try { + $cnt = count(Invoice::createBookingsFromLines( + $id, + (int)$data['kategorie_konto_id'], + (int)$data['zahlungskonto_id'], + $data['buchungs_datum'] + )); + View::redirect("/invoices/{$id}", "{$cnt} Buchung(en) automatisch erfasst."); + } catch (\RuntimeException $e) { + View::redirect("/invoices/{$id}", $e->getMessage(), 'error'); + } + } + private function createBeleg(array $invoice): string { $id = (int)$invoice['id']; diff --git a/src/Models/Invoice.php b/src/Models/Invoice.php index 50c9358..a82cc4b 100644 --- a/src/Models/Invoice.php +++ b/src/Models/Invoice.php @@ -162,6 +162,39 @@ class Invoice Database::execute('UPDATE invoices SET status=\'Bezahlt\', updated_at=NOW() WHERE id=?', [$id]); } + public static function createBookingsFromLines( + int $invoiceId, + int $kategorieKontoId, + int $zahlungskontoId, + string $buchungsDatum + ): array { + $inv = self::find($invoiceId); + if (!$inv) throw new \RuntimeException('Rechnung nicht gefunden.'); + if (empty($inv['beleg_id'])) throw new \RuntimeException('Rechnung hat keinen Beleg (noch nicht finalisiert?).'); + + $gruppen = []; + foreach (self::lines($invoiceId) as $line) { + if (($line['type'] ?? 'position') !== 'position') continue; + $satz = (float)$line['ust_satz']; + $gruppen[$satz] = ($gruppen[$satz] ?? 0.0) + round((float)$line['betrag_netto'], 2); + } + + $ids = []; + foreach ($gruppen as $satz => $netto) { + $ids[] = Booking::create([ + 'beleg_id' => (int)$inv['beleg_id'], + 'buchungs_datum' => $buchungsDatum, + 'buchungstyp' => 'Einnahme', + 'kategorie_konto_id' => $kategorieKontoId, + 'zahlungskonto_id' => $zahlungskontoId, + 'betrag_netto' => round($netto, 2), + 'ust_satz' => $satz, + 'buchungstext' => 'Ausgangsrechnung ' . $inv['rechnungs_nr'], + ]); + } + return $ids; + } + public static function cancel(int $id): void { $invoice = self::find($id); diff --git a/src/Views/invoices/show.php b/src/Views/invoices/show.php index 5719302..a2340c2 100644 --- a/src/Views/invoices/show.php +++ b/src/Views/invoices/show.php @@ -50,8 +50,9 @@ $badgeClass = match($invoice['status']) { - - Zahlungseingang erfassen + +
+ + + + +
+ + diff --git a/tests/27_StornorechnungTest.php b/tests/27_StornorechnungTest.php index d534951..73bceaa 100644 --- a/tests/27_StornorechnungTest.php +++ b/tests/27_StornorechnungTest.php @@ -86,16 +86,11 @@ class StornorechnungTest extends TestCase 'UPDATE invoices SET beleg_id=? WHERE id=?', [$belegId, $originalId] ); - $origBookingId = \Steuerkram\Models\Booking::create([ - 'beleg_id' => $belegId, - 'buchungs_datum' => $datum, - 'buchungstyp' => 'Einnahme', - 'kategorie_konto_id' => $kategorieId, - 'zahlungskonto_id' => $zahlungsId, - 'betrag_netto' => 250.00, - 'ust_satz' => 19.0, - 'buchungstext' => 'Test Originalrechnung', - ]); + $bookingIds = \Steuerkram\Models\Invoice::createBookingsFromLines( + $originalId, $kategorieId, $zahlungsId, $datum + ); + $this->assertGreaterThan(0, count($bookingIds), 'createBookingsFromLines erzeugt Buchungen'); + $origBookingId = $bookingIds[0]; $origBooking = \Steuerkram\Models\Booking::find($origBookingId); $this->assertEquals(250.00, round((float)$origBooking['betrag_netto'], 2), 'Original-Buchung Netto = 250'); $this->assertEquals(47.50, round((float)$origBooking['betrag_ust'], 2), 'Original-Buchung USt = 47,50');