- Depot der Bank wird einem Depot im Finanzmanager zugeordnet (Migration 022) - Bestand einbuchen: Ein-/Auslieferungen aus dem Bankbestand, Stichtag per Dialog - Zuordnungsliste: Depots nur noch als Wertpapiergeschäft Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
574 lines
21 KiB
PHP
574 lines
21 KiB
PHP
<?php
|
||
|
||
declare(strict_types=1);
|
||
|
||
namespace Finanzmanager\Controllers;
|
||
|
||
use Finanzmanager\Models\Bankzugang;
|
||
use Finanzmanager\Models\Depotbuchung;
|
||
use Finanzmanager\Models\Einstellung;
|
||
use Finanzmanager\Models\Konto;
|
||
use Finanzmanager\Models\Wertpapier;
|
||
use Finanzmanager\Services\Depot;
|
||
use Finanzmanager\Services\DepotbestandImport;
|
||
use Finanzmanager\Services\Depotabgleich;
|
||
use Finanzmanager\Services\Kursabruf;
|
||
use Finanzmanager\Services\Zeitraum;
|
||
use Finanzmanager\View;
|
||
|
||
/**
|
||
* Depots: Bestände, Buchungen, Kurse und die beiden Auswertungen
|
||
* *Veräußerungsgeschäfte* und *Erträge*.
|
||
*/
|
||
class WertpapiereController extends BaseController
|
||
{
|
||
public function index(): void
|
||
{
|
||
$this->verlangeAnmeldung();
|
||
|
||
$depots = Depot::depots();
|
||
$kontoId = (int)$this->query('depot');
|
||
$kontoId = $kontoId > 0 ? $kontoId : null;
|
||
|
||
$this->render('wertpapiere/index.php', [
|
||
'titel' => 'Wertpapiere',
|
||
'depots' => $depots,
|
||
'depot' => $kontoId,
|
||
'bestand' => Depot::bestand($kontoId),
|
||
'entwicklung' => Depot::entwicklung($kontoId),
|
||
'kursabruf' => Einstellung::get('wertpapiere_api_key', '') !== '',
|
||
]);
|
||
}
|
||
|
||
/**
|
||
* Die Detailseite eines Wertpapiers: Stammdaten, Kurs, Bestand je Depot,
|
||
* die Wertentwicklung – und unten die Buchungen eines Zeitraums.
|
||
*
|
||
* Die Kennzahlen oben gelten für die ganze Historie: Wer wissen will, was
|
||
* ein Papier gebracht hat, meint alles seit dem ersten Kauf. Nur die Liste
|
||
* unten folgt dem gewählten Zeitraum, voreingestellt das laufende Jahr.
|
||
*
|
||
* @param array<string,string> $params
|
||
*/
|
||
public function papier(array $params): void
|
||
{
|
||
$this->verlangeAnmeldung();
|
||
|
||
$id = (int)$params['id'];
|
||
$wertpapier = Wertpapier::find($id);
|
||
|
||
if ($wertpapier === null) {
|
||
View::redirect('/wertpapiere/stammdaten', 'Dieses Wertpapier gibt es nicht.', 'fehler');
|
||
return;
|
||
}
|
||
|
||
$grenzen = Depotbuchung::datumsgrenzen($id);
|
||
$zeitraum = Zeitraum::berechne(
|
||
$this->query('zeitraum', 'aktuelles_jahr'),
|
||
$this->query('von'),
|
||
$this->query('bis'),
|
||
null,
|
||
$grenzen
|
||
);
|
||
|
||
$this->render('wertpapiere/papier.php', [
|
||
'titel' => (string)$wertpapier['name'],
|
||
'wertpapier' => $wertpapier,
|
||
'bestand' => Depot::bestand(null, null, $id),
|
||
'kurs' => Wertpapier::letzterKurs($id),
|
||
'eckwerte' => Wertpapier::kurseckwerte($id, date('Y-m-d', strtotime('-1 year'))),
|
||
'entwicklung' => Depot::entwicklung(null, 12, null, $id),
|
||
'grenzen' => $grenzen,
|
||
'zeitraum' => $zeitraum,
|
||
'buchungen' => Depotbuchung::liste([
|
||
'wertpapier_id' => $id,
|
||
'von' => $zeitraum['von'],
|
||
'bis' => $zeitraum['bis'],
|
||
]),
|
||
]);
|
||
}
|
||
|
||
public function buchungen(): void
|
||
{
|
||
$this->verlangeAnmeldung();
|
||
|
||
$filter = [
|
||
'konto_id' => (int)$this->query('depot') ?: null,
|
||
'wertpapier_id' => (int)$this->query('wertpapier') ?: null,
|
||
'vorgang' => $this->query('vorgang'),
|
||
];
|
||
|
||
$this->render('wertpapiere/buchungen.php', [
|
||
'titel' => 'Depotbuchungen',
|
||
'buchungen' => Depotbuchung::liste(array_filter($filter)),
|
||
'depots' => Depot::depots(),
|
||
'wertpapiere' => Wertpapier::alle(),
|
||
'konten' => Konto::alle(true),
|
||
'filter' => $filter,
|
||
]);
|
||
}
|
||
|
||
public function neu(): void
|
||
{
|
||
$this->verlangeAnmeldung();
|
||
|
||
$this->render('wertpapiere/formular.php', [
|
||
'titel' => 'Depotbuchung erfassen',
|
||
'depots' => Depot::depots(true),
|
||
'wertpapiere' => Wertpapier::alle(true),
|
||
'konten' => Konto::alle(true),
|
||
]);
|
||
}
|
||
|
||
public function anlegen(): void
|
||
{
|
||
$this->verlangeAnmeldung();
|
||
|
||
try {
|
||
$wertpapierId = (int)$this->post('wertpapier_id');
|
||
|
||
// Ein neues Papier lässt sich im selben Formular anlegen – sonst
|
||
// müsste man für jeden Erstkauf erst in die Stammdaten wechseln.
|
||
if ($wertpapierId === 0) {
|
||
$wertpapierId = Wertpapier::findenOderAnlegen([
|
||
'name' => $this->post('wertpapier_name'),
|
||
'isin' => $this->post('isin'),
|
||
'wkn' => $this->post('wkn'),
|
||
'art' => $this->post('art', 'aktie'),
|
||
'waehrung' => $this->post('waehrung', 'EUR'),
|
||
]);
|
||
}
|
||
|
||
$stueck = $this->zahl($this->post('stueck'));
|
||
$kurs = $this->zahl($this->post('kurs'));
|
||
$betrag = $this->zahl($this->post('betrag'));
|
||
|
||
// Zwei von drei Angaben genügen: Der dritte Wert ergibt sich.
|
||
if ($betrag <= 0.0 && $stueck > 0.0 && $kurs > 0.0) {
|
||
$betrag = round($stueck * $kurs, 2);
|
||
}
|
||
|
||
Depotbuchung::anlegen([
|
||
'konto_id' => (int)$this->post('konto_id'),
|
||
'wertpapier_id' => $wertpapierId,
|
||
'datum' => $this->post('datum'),
|
||
'valuta' => $this->post('valuta'),
|
||
'vorgang' => $this->post('vorgang', 'kauf'),
|
||
'stueck' => $stueck,
|
||
'kurs' => $kurs > 0.0 ? $kurs : null,
|
||
'betrag' => $betrag,
|
||
'gebuehren' => $this->zahl($this->post('gebuehren')),
|
||
'steuern' => $this->zahl($this->post('steuern')),
|
||
'verrechnungskonto_id' => $this->post('verrechnungskonto_id'),
|
||
'notiz' => $this->post('notiz'),
|
||
'wertpapiername' => $this->post('wertpapier_name') ?: null,
|
||
]);
|
||
} catch (\Throwable $e) {
|
||
View::redirect('/wertpapiere/buchungen/neu', $e->getMessage(), 'fehler');
|
||
return;
|
||
}
|
||
|
||
View::redirect('/wertpapiere/buchungen', 'Depotbuchung erfasst.');
|
||
}
|
||
|
||
/** @param array<string,string> $params */
|
||
public function loeschen(array $params): void
|
||
{
|
||
$this->verlangeAnmeldung();
|
||
|
||
Depotbuchung::loeschen((int)$params['id']);
|
||
|
||
View::redirect($this->buchungenPfad(), 'Depotbuchung gelöscht – samt ihrer Geldbuchung.');
|
||
}
|
||
|
||
/**
|
||
* Sammellöschen aus der Liste der Depotbuchungen – wie im Kontoblatt.
|
||
*
|
||
* Der Bestand wird bei jedem Aufruf aus den Depotbuchungen gerechnet; er
|
||
* gibt die Löschung deshalb von selbst wieder her, ebenso Veräußerungen und
|
||
* Erträge. Fortgeschrieben wird nichts.
|
||
*/
|
||
public function auswahlLoeschen(): void
|
||
{
|
||
$this->verlangeAnmeldung();
|
||
|
||
$ziel = $this->buchungenPfad();
|
||
$ids = $this->postIds('buchungen');
|
||
if ($ids === []) {
|
||
View::redirect($ziel, 'Keine Depotbuchung ausgewählt.', 'warnung');
|
||
return;
|
||
}
|
||
|
||
$anzahl = 0;
|
||
foreach ($ids as $id) {
|
||
// Fremde oder längst gelöschte Nummern still übergehen – der Rest
|
||
// der Auswahl soll trotzdem verschwinden.
|
||
if (Depotbuchung::find($id) === null) {
|
||
continue;
|
||
}
|
||
|
||
try {
|
||
Depotbuchung::loeschen($id);
|
||
} catch (\Throwable $e) {
|
||
View::redirect($ziel, $e->getMessage(), 'fehler');
|
||
return;
|
||
}
|
||
$anzahl++;
|
||
}
|
||
|
||
View::redirect($ziel, $anzahl === 1
|
||
? 'Depotbuchung gelöscht – samt ihrer Geldbuchung.'
|
||
: $anzahl . ' Depotbuchungen gelöscht – samt ihrer Geldbuchungen.');
|
||
}
|
||
|
||
/**
|
||
* Zurück in die Liste, mit demselben Filter.
|
||
*
|
||
* Die Werte kommen aus der Abfragezeichenfolge des Formularziels und werden
|
||
* hier neu zusammengesetzt statt übernommen: So kann kein fremdes Ziel
|
||
* eingeschleust werden.
|
||
*/
|
||
private function buchungenPfad(): string
|
||
{
|
||
$query = array_filter([
|
||
'depot' => $this->query('depot'),
|
||
'wertpapier' => $this->query('wertpapier'),
|
||
'vorgang' => $this->query('vorgang'),
|
||
], static fn(string $wert): bool => $wert !== '');
|
||
|
||
return '/wertpapiere/buchungen' . ($query === [] ? '' : '?' . http_build_query($query));
|
||
}
|
||
|
||
// ── Bestand laut Bank ────────────────────────────────────────────────────
|
||
|
||
/**
|
||
* Der Depotbestand, wie ihn die Bank per FinTS meldet, neben dem eigenen
|
||
* Bestand. Der Abruf selbst läuft über den Banking-Bereich (TAN-Maske).
|
||
*/
|
||
public function bankbestand(): void
|
||
{
|
||
$this->verlangeAnmeldung();
|
||
|
||
$depots = [];
|
||
foreach (Depot::depots(true) as $depot) {
|
||
$depots[] = $depot + [
|
||
'vergleich' => Depotabgleich::vergleich((int)$depot['id']),
|
||
'abgerufen_am' => Depotabgleich::abgerufenAm((int)$depot['id']),
|
||
];
|
||
}
|
||
|
||
$this->render('wertpapiere/bank.php', [
|
||
'titel' => 'Depotbestand laut Bank',
|
||
'depots' => $depots,
|
||
'zugaenge' => array_values(array_filter(
|
||
Bankzugang::alle(true),
|
||
static fn(array $zugang): bool => (string)$zugang['typ'] === 'fints'
|
||
)),
|
||
]);
|
||
}
|
||
|
||
/** Bucht die Unterschiede zwischen Bank- und eigenem Bestand ein. */
|
||
public function bankbestandEinbuchen(): void
|
||
{
|
||
$this->verlangeAnmeldung();
|
||
|
||
$depotId = (int)$this->post('konto_id');
|
||
if (Depot::depots(true) === [] || Konto::find($depotId) === null) {
|
||
View::redirect('/wertpapiere/bank', 'Dieses Depot gibt es nicht.', 'fehler');
|
||
return;
|
||
}
|
||
|
||
$stichtag = trim((string)$this->post('stichtag'));
|
||
if ($stichtag !== '') {
|
||
$datum = \DateTimeImmutable::createFromFormat('!Y-m-d', $stichtag);
|
||
if ($datum === false || $datum->format('Y-m-d') !== $stichtag) {
|
||
View::redirect('/wertpapiere/bank', 'Der Stichtag ist kein gültiges Datum.', 'fehler');
|
||
return;
|
||
}
|
||
}
|
||
|
||
try {
|
||
$e = Depotabgleich::einbuchen($depotId, $stichtag !== '' ? $stichtag : null);
|
||
} catch (\Throwable $ex) {
|
||
View::redirect('/wertpapiere/bank', 'Einbuchen fehlgeschlagen: ' . $ex->getMessage(), 'fehler');
|
||
return;
|
||
}
|
||
|
||
View::redirect('/wertpapiere/bank', sprintf(
|
||
'%d Einlieferung(en), %d Auslieferung(en) gebucht, %d Wertpapier(e) neu angelegt.',
|
||
$e['eingeliefert'],
|
||
$e['ausgeliefert'],
|
||
$e['angelegt']
|
||
));
|
||
}
|
||
|
||
// ── Bestandsimport aus einer CSV-Datei ───────────────────────────────────
|
||
|
||
public function importFormular(): void
|
||
{
|
||
$this->verlangeAnmeldung();
|
||
|
||
$this->render('wertpapiere/import.php', [
|
||
'titel' => 'Depotbestand importieren',
|
||
'depots' => Depot::depots(true),
|
||
'geldkonten' => $this->geldkonten(),
|
||
'ergebnis' => null,
|
||
'datei' => '',
|
||
'depot' => (int)$this->query('depot'),
|
||
'verrechnungskonto' => 0,
|
||
'datum' => '',
|
||
]);
|
||
}
|
||
|
||
/**
|
||
* Vorschau und Übernahme laufen durch dieselbe Tür: Der erste Aufruf bringt
|
||
* die hochgeladene Datei mit, der zweite nur noch ihren Namen.
|
||
*/
|
||
public function importAusfuehren(): void
|
||
{
|
||
$this->verlangeAnmeldung();
|
||
|
||
try {
|
||
$datei = $this->importDatei();
|
||
} catch (\Throwable $e) {
|
||
View::redirect('/wertpapiere/import', $e->getMessage(), 'fehler');
|
||
return;
|
||
}
|
||
|
||
$trockenlauf = $this->post('modus') !== 'import';
|
||
$depotId = (int)$this->post('konto_id');
|
||
$verrechnung = (int)$this->post('verrechnungskonto_id');
|
||
|
||
try {
|
||
$import = DepotbestandImport::ausDatei($this->importVerzeichnis() . '/' . $datei, [
|
||
'konto_id' => $depotId,
|
||
'verrechnungskonto_id' => $verrechnung,
|
||
'datum' => $this->post('datum'),
|
||
]);
|
||
|
||
// Ohne ausdrückliche Wahl entscheidet die Depotnummer aus der Datei.
|
||
if ($depotId === 0) {
|
||
$passend = DepotbestandImport::passendesDepot($import->depotnummer());
|
||
$depotId = (int)($passend['id'] ?? 0);
|
||
$import->depot($depotId);
|
||
}
|
||
|
||
$ergebnis = $import->ausfuehren($trockenlauf);
|
||
} catch (\Throwable $e) {
|
||
View::redirect('/wertpapiere/import', 'Import fehlgeschlagen: ' . $e->getMessage(), 'fehler');
|
||
return;
|
||
}
|
||
|
||
if (!$trockenlauf) {
|
||
View::redirect('/wertpapiere?depot=' . $depotId, sprintf(
|
||
'%d Positionen übernommen, %d waren schon da%s.',
|
||
$ergebnis['zaehler']['neu'],
|
||
$ergebnis['zaehler']['vorhanden'],
|
||
$ergebnis['zaehler']['fehler'] > 0
|
||
? ', ' . $ergebnis['zaehler']['fehler'] . ' fehlgeschlagen' : ''
|
||
), $ergebnis['zaehler']['fehler'] > 0 ? 'fehler' : 'erfolg');
|
||
return;
|
||
}
|
||
|
||
$this->render('wertpapiere/import.php', [
|
||
'titel' => 'Depotbestand importieren',
|
||
'depots' => Depot::depots(true),
|
||
'geldkonten' => $this->geldkonten(),
|
||
'ergebnis' => $ergebnis,
|
||
'datei' => $datei,
|
||
'depot' => $depotId,
|
||
'verrechnungskonto' => $verrechnung,
|
||
'datum' => $ergebnis['datum'],
|
||
]);
|
||
}
|
||
|
||
/**
|
||
* Nimmt die hochgeladene Datei entgegen und legt sie ab – oder greift auf
|
||
* die aus der Vorschau zurück.
|
||
*/
|
||
private function importDatei(): string
|
||
{
|
||
$hochgeladen = $_FILES['csv'] ?? null;
|
||
|
||
if (is_array($hochgeladen) && ($hochgeladen['error'] ?? UPLOAD_ERR_NO_FILE) === UPLOAD_ERR_OK) {
|
||
$name = self::sichererCsvName((string)$hochgeladen['name']);
|
||
if ($name === null) {
|
||
throw new \RuntimeException('Nur .CSV-Dateien können hochgeladen werden.');
|
||
}
|
||
|
||
$ziel = $this->importVerzeichnis() . '/' . $name;
|
||
if (
|
||
!is_uploaded_file((string)$hochgeladen['tmp_name'])
|
||
|| !move_uploaded_file((string)$hochgeladen['tmp_name'], $ziel)
|
||
) {
|
||
throw new \RuntimeException('Die Datei konnte nicht abgelegt werden.');
|
||
}
|
||
|
||
return $name;
|
||
}
|
||
|
||
$name = self::sichererCsvName($this->post('datei'));
|
||
if ($name === null || !is_file($this->importVerzeichnis() . '/' . $name)) {
|
||
throw new \RuntimeException('Es wurde keine CSV-Datei ausgewählt.');
|
||
}
|
||
|
||
return $name;
|
||
}
|
||
|
||
/** Wie beim QIF-Import: kein Pfadanteil, keine fremde Endung. */
|
||
public static function sichererCsvName(string $eingabe): ?string
|
||
{
|
||
$name = basename(trim($eingabe));
|
||
if ($name === '' || $name === '.' || $name === '..' || str_contains($name, "\0")) {
|
||
return null;
|
||
}
|
||
|
||
if (strtolower(pathinfo($name, PATHINFO_EXTENSION)) !== 'csv') {
|
||
return null;
|
||
}
|
||
|
||
$name = preg_replace('/[^A-Za-z0-9._-]/', '_', $name) ?? '';
|
||
|
||
return $name === '' ? null : $name;
|
||
}
|
||
|
||
/** Dasselbe Verzeichnis wie beim QIF-Import – es ist das Importverzeichnis. */
|
||
private function importVerzeichnis(): string
|
||
{
|
||
$relativ = (string)Einstellung::get('import_qif_verzeichnis', 'data/import');
|
||
$pfad = dirname(__DIR__, 2) . '/' . trim($relativ, '/');
|
||
|
||
if (!is_dir($pfad)) {
|
||
mkdir($pfad, 0o775, true);
|
||
}
|
||
|
||
return $pfad;
|
||
}
|
||
|
||
/** Konten, die als Verrechnungskonto in Frage kommen – alles außer Depots. */
|
||
private function geldkonten(): array
|
||
{
|
||
return array_values(array_filter(
|
||
Konto::alle(true),
|
||
static fn(array $konto): bool => (string)$konto['kontoart'] !== 'depot'
|
||
));
|
||
}
|
||
|
||
// ── Stammdaten und Kurse ─────────────────────────────────────────────────
|
||
|
||
public function stammdaten(): void
|
||
{
|
||
$this->verlangeAnmeldung();
|
||
|
||
$kurse = Wertpapier::letzteKurse();
|
||
$liste = [];
|
||
foreach (Wertpapier::alle() as $wertpapier) {
|
||
$wertpapier['kurs'] = $kurse[(int)$wertpapier['id']]['kurs'] ?? null;
|
||
$wertpapier['kurs_datum'] = $kurse[(int)$wertpapier['id']]['datum'] ?? null;
|
||
$liste[] = $wertpapier;
|
||
}
|
||
|
||
$this->render('wertpapiere/stammdaten.php', [
|
||
'titel' => 'Wertpapiere verwalten',
|
||
'wertpapiere' => $liste,
|
||
'anbieter' => Einstellung::get('wertpapiere_kursdienst', 'leeway'),
|
||
'schluessel' => Einstellung::get('wertpapiere_api_key', '') !== '',
|
||
]);
|
||
}
|
||
|
||
/** @param array<string,string> $params */
|
||
public function speichern(array $params): void
|
||
{
|
||
$this->verlangeAnmeldung();
|
||
|
||
try {
|
||
Wertpapier::speichern((int)$params['id'], [
|
||
'name' => $this->post('name'),
|
||
'wkn' => $this->post('wkn'),
|
||
'isin' => $this->post('isin'),
|
||
'art' => $this->post('art'),
|
||
'waehrung' => $this->post('waehrung', 'EUR'),
|
||
'symbol' => $this->post('symbol'),
|
||
'aktiv' => isset($_POST['aktiv']) ? 1 : 0,
|
||
]);
|
||
} catch (\Throwable $e) {
|
||
View::redirect('/wertpapiere/stammdaten', $e->getMessage(), 'fehler');
|
||
return;
|
||
}
|
||
|
||
View::redirect('/wertpapiere/stammdaten', 'Wertpapier gespeichert.');
|
||
}
|
||
|
||
/** @param array<string,string> $params */
|
||
public function kursSetzen(array $params): void
|
||
{
|
||
$this->verlangeAnmeldung();
|
||
|
||
$kurs = $this->zahl($this->post('kurs'));
|
||
$datum = $this->post('datum') ?: date('Y-m-d');
|
||
|
||
if ($kurs <= 0.0) {
|
||
View::redirect('/wertpapiere/stammdaten', 'Der Kurs muss größer als null sein.', 'fehler');
|
||
return;
|
||
}
|
||
|
||
Wertpapier::kursSetzen((int)$params['id'], $datum, $kurs, 'manuell');
|
||
|
||
View::redirect('/wertpapiere/stammdaten', 'Kurs eingetragen.');
|
||
}
|
||
|
||
public function kurseAbrufen(): void
|
||
{
|
||
$this->verlangeAnmeldung();
|
||
|
||
$ergebnis = Kursabruf::laufen();
|
||
|
||
$meldung = sprintf(
|
||
'%d Kurse abgerufen, %d übersprungen, %d fehlgeschlagen.',
|
||
$ergebnis['abgerufen'],
|
||
$ergebnis['uebersprungen'],
|
||
$ergebnis['fehler']
|
||
);
|
||
|
||
if ($ergebnis['fehler'] > 0) {
|
||
$meldung .= ' ' . implode(' · ', array_slice($ergebnis['protokoll'], 0, 3));
|
||
}
|
||
|
||
View::redirect(
|
||
'/wertpapiere/stammdaten',
|
||
$meldung,
|
||
$ergebnis['fehler'] > 0 && $ergebnis['abgerufen'] === 0 ? 'fehler' : 'erfolg'
|
||
);
|
||
}
|
||
|
||
// ── Auswertungen ─────────────────────────────────────────────────────────
|
||
|
||
public function auswertung(): void
|
||
{
|
||
$this->verlangeAnmeldung();
|
||
|
||
$art = $this->query('art', 'veraeusserungen');
|
||
$vorgabe = $this->query('zeitraum', 'aktuelles_jahr');
|
||
$zeitraum = Zeitraum::berechne($vorgabe, $this->query('von'), $this->query('bis'));
|
||
$kontoId = (int)$this->query('depot') ?: null;
|
||
|
||
$this->render('wertpapiere/auswertung.php', [
|
||
'titel' => $art === 'ertraege' ? 'Erträge' : 'Veräußerungsgeschäfte',
|
||
'art' => $art === 'ertraege' ? 'ertraege' : 'veraeusserungen',
|
||
'zeitraum' => $zeitraum,
|
||
'depots' => Depot::depots(),
|
||
'depot' => $kontoId,
|
||
'daten' => $art === 'ertraege'
|
||
? Depot::ertraege($zeitraum['von'], $zeitraum['bis'], $kontoId)
|
||
: Depot::veraeusserungen($zeitraum['von'], $zeitraum['bis'], $kontoId),
|
||
]);
|
||
}
|
||
|
||
/** Beträge kommen mit Komma aus dem Formular. */
|
||
private function zahl(string $wert): float
|
||
{
|
||
$wert = trim(str_replace(['.', ' '], '', $wert));
|
||
|
||
return $wert === '' ? 0.0 : (float)str_replace(',', '.', $wert);
|
||
}
|
||
}
|