Finanzmanager/src/Controllers/VorlagenController.php
fds2610-bot f388d04c05 Phasen 4 bis 7: Cockpit, Online-Banking, Wertpapiere, Nebenmodule
Damit ist der geplante Funktionsumfang beisammen.

Phase 4 abgeschlossen
- Konfigurierbare Cockpit-Kacheln; abgewählte werden nicht berechnet
- Einnahmen/Ausgaben je Monat als Balkenpaare mit Saldolinie, aufgesetzt auf
  den Summenbericht statt auf eine eigene Abfrage
- Meine Ausgaben als Ring auf der obersten Kategorieebene
- Offene Vorgänge aus allen Modulen

Phase 5 – Online-Banking
- nemiah/php-fints als Fork auf Codeberg, psr/log direkt eingebaut: die
  Anwendung hängt an keinem fremden Update-Zyklus
- PIN AES-256-GCM-verschlüsselt; Schlüssel aus APP_KEY oder aus
  data/schluessel/app_key, den der Entrypoint einmalig erzeugt
- TAN-Dialog über mehrere Requests (FinTs::persist plus serialisierte Aktion
  in der Sitzung) – ein wartender Prozess wäre mit php-fpm der falsche Weg
- Abgleich statt Direktbuchung: verbinden, übernehmen oder verwerfen
- Regeln mit vier Vergleichsarten, Betragsgrenzen und Split-Schema

Phase 6 – Wertpapiere
- Depotbuchung und Geldbuchung als zwei Seiten eines Geschäfts; betrag ist
  der Bruttowert, Nebenkosten stehen daneben
- Einstand nach gleitendem Durchschnitt, realisierte Gewinne, Erträge
- Kursabruf über Leeway oder FMP, täglicher Lauf per Cron
- QIF-Import für !Type:Invst

Dabei behoben: Der Import buchte den Geldfluss jedes Wertpapiergeschäfts als
Abgang – auch bei Verkäufen und Dividenden; und er erkannte das
Verrechnungskonto nicht, wenn dem Konto im L-Feld eine Kategorie vorangeht
(bei Verkäufen also immer).

Phase 7 – Nebenmodule und Zahlungsverkehr
- Buchungsvorlagen inklusive QIF-Import, Splits werden mitskaliert
- Regelmäßige Buchungen mit Fälligkeitslauf, Finanzkalender, Erinnerungen
- Adressen mit IBAN-Prüfung, Verträge mit Kündigungsfrist, Sparziele
- Kredite mit gerechnetem Tilgungsplan
- Überweisungen über FinTS; die Buchung entsteht erst nach Annahme durch die
  Bank. Die pain.001-Nachricht erzeugt SepaXml selbst

Nebenbei: .dockerignore ergänzt (ohne sie brach der Build an data/mariadb ab),
PlatzhalterController entfernt, zwei Inline-Handler beseitigt, die die
Content-Security-Policy ohnehin blockierte.

888 PHP-Prüfungen und 45 E2E-Tests grün.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-12 06:59:24 +02:00

125 lines
3.7 KiB
PHP

<?php
declare(strict_types=1);
namespace Finanzmanager\Controllers;
use Finanzmanager\Models\Buchungsvorlage;
use Finanzmanager\Models\Kategorie;
use Finanzmanager\Models\Klasse;
use Finanzmanager\Models\Konto;
use Finanzmanager\View;
/**
* Buchungsvorlagen: suchen, buchen, pflegen.
*/
class VorlagenController extends BaseController
{
public function index(): void
{
$this->verlangeAnmeldung();
$suche = $this->query('suche');
$this->render('vorlagen/index.php', [
'titel' => 'Buchungsvorlagen',
'vorlagen' => Buchungsvorlage::alle($suche),
'suche' => $suche,
'anzahl' => Buchungsvorlage::anzahl(),
'konten' => Konto::alle(true),
'kategorien' => Kategorie::baumFlach(null, true),
'klassen' => Klasse::alle(),
]);
}
public function anlegen(): void
{
$this->verlangeAnmeldung();
try {
Buchungsvorlage::anlegen($this->formulardaten());
} catch (\Throwable $e) {
View::redirect('/vorlagen', $e->getMessage(), 'fehler');
return;
}
View::redirect('/vorlagen', 'Vorlage angelegt.');
}
/** @param array<string,string> $params */
public function speichern(array $params): void
{
$this->verlangeAnmeldung();
try {
Buchungsvorlage::speichern((int)$params['id'], $this->formulardaten());
} catch (\Throwable $e) {
View::redirect('/vorlagen', $e->getMessage(), 'fehler');
return;
}
View::redirect('/vorlagen', 'Vorlage gespeichert.');
}
/** @param array<string,string> $params */
public function loeschen(array $params): void
{
$this->verlangeAnmeldung();
Buchungsvorlage::loeschen((int)$params['id']);
View::redirect('/vorlagen', 'Vorlage gelöscht.');
}
/** @param array<string,string> $params */
public function buchen(array $params): void
{
$this->verlangeAnmeldung();
try {
$buchungId = Buchungsvorlage::buchen((int)$params['id'], [
'datum' => $this->post('datum') ?: date('Y-m-d'),
'betrag' => $this->post('betrag'),
'konto_id' => $this->post('konto_id'),
]);
} catch (\Throwable $e) {
View::redirect('/vorlagen', $e->getMessage(), 'fehler');
return;
}
View::redirect('/buchungen/' . $buchungId, 'Buchung aus der Vorlage angelegt.');
}
/** Aus einer vorhandenen Buchung eine Vorlage machen. */
public function ausBuchung(): void
{
$this->verlangeAnmeldung();
$buchungId = (int)$this->post('buchung_id');
try {
Buchungsvorlage::ausBuchung($buchungId, $this->post('name'));
} catch (\Throwable $e) {
View::redirect('/buchungen/' . $buchungId, $e->getMessage(), 'fehler');
return;
}
View::redirect('/buchungen/' . $buchungId, 'Vorlage aus dieser Buchung angelegt.');
}
/** @return array<string,mixed> */
private function formulardaten(): array
{
return [
'name' => $this->post('name'),
'konto_id' => $this->post('konto_id'),
'betrag' => str_replace(',', '.', $this->post('betrag')),
'empfaenger' => $this->post('empfaenger'),
'buchungstext' => $this->post('buchungstext'),
'nummer' => $this->post('nummer'),
'kategorie_id' => $this->post('kategorie_id'),
'klasse_id' => $this->post('klasse_id'),
'gegenkonto_id' => $this->post('gegenkonto_id'),
];
}
}