Steuerkram/src/Controllers/InvoiceController.php

407 lines
15 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<?php
declare(strict_types=1);
namespace Steuerkram\Controllers;
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;
class InvoiceController
{
public function index(array $params): void
{
$filter = $_GET;
$page = max(1, (int)($_GET['page'] ?? 1));
$result = Invoice::all($filter, $page);
$settings = Setting::get();
$view = new View();
$view->render('layout/base.php', [
'pageTitle' => 'Ausgangsrechnungen',
'content' => $this->renderView('invoices/list', [
'rows' => $result['rows'],
'total' => $result['total'],
'pages' => $result['pages'],
'page' => $page,
'filter' => $filter,
'settings' => $settings,
]),
'topbarActions' => '<a href="/invoices/new" class="btn btn-primary">+ Rechnung erstellen</a>',
]);
}
public function create(array $params): void
{
$customers = Customer::all();
$settings = Setting::get();
$invoiceDefaults = [
'rechnungs_datum' => date('Y-m-d'),
];
$preCustomerId = (int)($_GET['customer_id'] ?? 0);
if ($preCustomerId > 0) {
$customer = Customer::find($preCustomerId);
if ($customer) {
$invoiceDefaults['customer_id'] = $preCustomerId;
$invoiceDefaults['payment_days'] = $customer['payment_days'];
}
}
$view = new View();
$view->render('layout/base.php', [
'pageTitle' => 'Neue Rechnung',
'content' => $this->renderView('invoices/form', [
'invoice' => $invoiceDefaults,
'lines' => [],
'customers' => $customers,
'errors' => [],
'action' => '/invoices',
'settings' => $settings,
]),
]);
}
public function store(array $params): void
{
$data = $_POST;
$errors = $this->validate($data);
if ($errors) {
$customers = Customer::all();
$view = new View();
$view->render('layout/base.php', [
'pageTitle' => 'Neue Rechnung',
'content' => $this->renderView('invoices/form', [
'invoice' => $data,
'lines' => $data['lines'] ?? [],
'customers' => $customers,
'errors' => $errors,
'action' => '/invoices',
'settings' => Setting::get(),
]),
]);
return;
}
$id = Invoice::create($data);
View::redirect("/invoices/{$id}", 'Rechnung als Entwurf gespeichert.');
}
public function show(array $params): void
{
$invoice = Invoice::find((int)$params['id']);
if (!$invoice) { http_response_code(404); return; }
$lines = Invoice::lines((int)$params['id']);
$settings = Setting::get();
$view = new View();
$view->render('layout/base.php', [
'pageTitle' => 'Rechnung ' . ($invoice['rechnungs_nr'] ?? 'Entwurf'),
'content' => $this->renderView('invoices/show', [
'invoice' => $invoice,
'lines' => $lines,
'settings' => $settings,
]),
]);
}
public function edit(array $params): void
{
$invoice = Invoice::find((int)$params['id']);
if (!$invoice) { http_response_code(404); return; }
if ($invoice['status'] !== 'Entwurf') {
View::redirect("/invoices/{$invoice['id']}", 'Nur Entwürfe können bearbeitet werden.', 'error');
return;
}
$lines = Invoice::lines((int)$params['id']);
$customers = Customer::all();
$view = new View();
$view->render('layout/base.php', [
'pageTitle' => 'Rechnung bearbeiten',
'content' => $this->renderView('invoices/form', [
'invoice' => $invoice,
'lines' => $lines,
'customers' => $customers,
'errors' => [],
'action' => '/invoices/' . $invoice['id'],
'settings' => Setting::get(),
]),
]);
}
public function update(array $params): void
{
$id = (int)$params['id'];
$invoice = Invoice::find($id);
if (!$invoice) { http_response_code(404); return; }
$data = $_POST;
$errors = $this->validate($data);
if ($errors) {
$customers = Customer::all();
$view = new View();
$view->render('layout/base.php', [
'pageTitle' => 'Rechnung bearbeiten',
'content' => $this->renderView('invoices/form', [
'invoice' => array_merge($invoice, $data),
'lines' => $data['lines'] ?? [],
'customers' => $customers,
'errors' => $errors,
'action' => '/invoices/' . $id,
'settings' => Setting::get(),
]),
]);
return;
}
try {
Invoice::update($id, $data);
View::redirect("/invoices/{$id}", 'Rechnung gespeichert.');
} catch (\PDOException $e) {
error_log('[Steuerkram] PDO-Fehler in InvoiceController::update(): ' . $e->getMessage());
View::redirect("/invoices/{$id}", 'Datenbankfehler beim Speichern der Rechnung.', 'error');
} catch (\Exception $e) {
View::redirect("/invoices/{$id}", $e->getMessage(), 'error');
}
}
public function finalize(array $params): void
{
$id = (int)$params['id'];
try {
\Steuerkram\Database::beginTransaction();
$nr = Invoice::finalize($id);
$invoice = Invoice::find($id);
$belegNr = $this->createBeleg($invoice);
\Steuerkram\Database::commit();
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());
View::redirect("/invoices/{$id}", 'Datenbankfehler beim Finalisieren der Rechnung.', 'error');
} catch (\Exception $e) {
\Steuerkram\Database::rollBack();
View::redirect("/invoices/{$id}", $e->getMessage(), 'error');
}
}
public function pdf(array $params): void
{
$invoice = Invoice::find((int)$params['id']);
if (!$invoice) { http_response_code(404); return; }
$lines = Invoice::lines((int)$params['id']);
$settings = Setting::get();
$svc = new PdfInvoiceService();
$svc->render($invoice, $lines, $settings);
}
public function book(array $params): void
{
$id = (int)$params['id'];
$invoice = Invoice::find($id);
if (!$invoice) { http_response_code(404); return; }
if ($invoice['status'] === 'Entwurf') {
View::redirect("/invoices/{$id}", 'Rechnung muss zuerst finalisiert werden.', 'error');
return;
}
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 {
$data['beleg_id'] = $belegId;
$data['buchungstyp'] = 'Einnahme';
Booking::create($data);
View::redirect("/invoices/{$id}/book", 'Buchung erfasst.');
} catch (\PDOException $e) {
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) {
$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 createBeleg(array $invoice): string
{
$id = (int)$invoice['id'];
$belegNr = Beleg::generateBelegNr();
$belegId = Beleg::create([
'beleg_nr' => $belegNr,
'filename' => $invoice['rechnungs_nr'] . '.pdf',
'storage_path' => '',
'mime_type' => 'application/pdf',
'file_hash' => hash('sha256', $invoice['rechnungs_nr']),
'beleg_datum' => $invoice['rechnungs_datum'],
'rechnungs_nr' => $invoice['rechnungs_nr'],
'betrag_brutto' => $invoice['betrag_brutto'],
'belegtyp' => 'Ausgangsrechnung',
'notes' => 'Automatisch erstellt aus Rechnung ' . $invoice['rechnungs_nr'],
]);
\Steuerkram\Database::execute(
'UPDATE invoices SET beleg_id=? WHERE id=?', [$belegId, $id]
);
return $belegNr;
}
public function destroy(array $params): void
{
if (!Setting::isTestMode()) {
View::redirect('/invoices', 'Löschen ist nur im Testmodus erlaubt.', 'error');
return;
}
$id = (int)$params['id'];
Invoice::delete($id);
View::redirect('/invoices', 'Rechnung gelöscht.');
}
public function markPaid(array $params): void
{
Invoice::markPaid((int)$params['id']);
View::redirect('/invoices/' . $params['id'], 'Rechnung als bezahlt markiert.');
}
public function cancel(array $params): void
{
Invoice::cancel((int)$params['id']);
View::redirect('/invoices/' . $params['id'], 'Rechnung storniert.');
}
public function addLine(array $params): void
{
// This is handled client-side via JS; redirect back
View::redirect('/invoices/' . $params['id'] . '/edit');
}
public function deleteLine(array $params): void
{
\Steuerkram\Database::execute(
'DELETE FROM invoice_lines WHERE id=? AND invoice_id=?',
[(int)$params['line_id'], (int)$params['id']]
);
View::redirect('/invoices/' . $params['id'] . '/edit', 'Position gelöscht.');
}
private function validate(array $data): array
{
$errors = [];
if (empty($data['customer_id'])) $errors['customer_id'] = 'Pflichtfeld';
if (empty($data['betreff'])) $errors['betreff'] = 'Pflichtfeld';
if (empty($data['rechnungs_datum'])) $errors['rechnungs_datum'] = 'Pflichtfeld';
if (empty($data['lines'])) {
$errors['lines'] = 'Mindestens eine Position erforderlich.';
}
return $errors;
}
private function renderView(string $template, array $data): string
{
extract($data, EXTR_SKIP);
ob_start();
include dirname(__DIR__) . '/Views/' . $template . '.php';
return ob_get_clean();
}
}