407 lines
15 KiB
PHP
407 lines
15 KiB
PHP
<?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();
|
||
}
|
||
}
|