Finanzmanager/src/Views/wertpapiere/stammdaten.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

123 lines
5.5 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
use Finanzmanager\Models\Wertpapier;
use Finanzmanager\Services\Kursabruf;
use Finanzmanager\View;
/**
* @var list<array<string,mixed>> $wertpapiere
* @var string|null $anbieter
* @var bool $schluessel
* @var string $csrfToken
*/
include __DIR__ . '/../layout/kopf.php';
?>
<div class="tableiste">
<a href="/wertpapiere" class="tab">Bestände</a>
<a href="/wertpapiere/buchungen" class="tab">Depotbuchungen</a>
<span class="tab aktiv">Wertpapiere</span>
<a href="/wertpapiere/auswertung?art=veraeusserungen" class="tab">Veräußerungen</a>
<a href="/wertpapiere/auswertung?art=ertraege" class="tab">Erträge</a>
</div>
<div class="seitenaktionen">
<?php if ($schluessel): ?>
<form method="post" action="/wertpapiere/kurse" class="inline">
<input type="hidden" name="csrf_token" value="<?= View::h($csrfToken) ?>">
<button type="submit" class="knopf primaer">Kurse jetzt abrufen</button>
</form>
<span class="nebentext">
über <?= View::h(Kursabruf::ANBIETER[$anbieter] ?? (string)$anbieter) ?>
</span>
<?php else: ?>
<a href="/einstellungen?bereich=wertpapiere" class="knopf">Kursdienst einrichten</a>
<span class="nebentext">Ohne API-Schlüssel bleibt nur die Eingabe von Hand.</span>
<?php endif; ?>
</div>
<?php if ($wertpapiere === []): ?>
<p class="platzhalter">
Noch keine Wertpapiere. Sie entstehen beim QIF-Import oder beim Erfassen der ersten
Depotbuchung.
</p>
<?php else: ?>
<?php foreach ($wertpapiere as $papier): ?>
<form id="wp-<?= (int)$papier['id'] ?>" method="post"
action="/wertpapiere/stammdaten/<?= (int)$papier['id'] ?>"></form>
<form id="kurs-<?= (int)$papier['id'] ?>" method="post"
action="/wertpapiere/stammdaten/<?= (int)$papier['id'] ?>/kurs"></form>
<?php endforeach; ?>
<table class="liste">
<thead>
<tr>
<th>Name</th><th>ISIN</th><th>WKN</th><th>Art</th><th>Kürzel</th>
<th>Letzter Kurs</th><th>Kurs eintragen</th><th>Aktiv</th><th class="rechts">Aktion</th>
</tr>
</thead>
<tbody>
<?php foreach ($wertpapiere as $papier): ?>
<?php $f = 'wp-' . (int)$papier['id']; $k = 'kurs-' . (int)$papier['id']; ?>
<tr<?= (int)$papier['aktiv'] === 1 ? '' : ' class="inaktiv"' ?>>
<td>
<input type="hidden" form="<?= $f ?>" name="csrf_token" value="<?= View::h($csrfToken) ?>">
<input type="text" form="<?= $f ?>" name="name" value="<?= View::h($papier['name']) ?>" required>
</td>
<td><input type="text" form="<?= $f ?>" name="isin" class="schmal"
value="<?= View::h($papier['isin']) ?>"></td>
<td><input type="text" form="<?= $f ?>" name="wkn" class="schmal"
value="<?= View::h($papier['wkn']) ?>"></td>
<td>
<select form="<?= $f ?>" name="art">
<?php foreach (Wertpapier::ARTEN as $schluesselArt => $name): ?>
<option value="<?= View::h($schluesselArt) ?>"
<?= $papier['art'] === $schluesselArt ? 'selected' : '' ?>>
<?= View::h($name) ?>
</option>
<?php endforeach; ?>
</select>
</td>
<td>
<input type="text" form="<?= $f ?>" name="symbol" class="schmal"
value="<?= View::h($papier['symbol']) ?>"
title="Kürzel beim Kursdienst – wird beim ersten Abruf selbst ermittelt">
</td>
<td class="rechts">
<?php if ($papier['kurs'] === null): ?>
<span class="nebentext">–</span>
<?php else: ?>
<?= View::h(number_format((float)$papier['kurs'], 2, ',', '.')) ?>
<span class="nebentext"><?= View::h(View::datum($papier['kurs_datum'])) ?></span>
<?php endif; ?>
</td>
<td class="nowrap">
<input type="hidden" form="<?= $k ?>" name="csrf_token" value="<?= View::h($csrfToken) ?>">
<input type="hidden" form="<?= $k ?>" name="datum" value="<?= View::h(date('Y-m-d')) ?>">
<input type="text" form="<?= $k ?>" name="kurs" class="schmal" inputmode="decimal"
placeholder="Kurs">
<button type="submit" form="<?= $k ?>" class="knopf klein">Setzen</button>
</td>
<td>
<label class="ankreuzfeld">
<input type="checkbox" form="<?= $f ?>" name="aktiv" value="1"
<?= (int)$papier['aktiv'] === 1 ? 'checked' : '' ?>>
</label>
</td>
<td class="rechts">
<button type="submit" form="<?= $f ?>" class="knopf klein">Speichern</button>
</td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
<p class="hinweis klein">
Das <strong>Kürzel</strong> ist der Name des Papiers beim Kursdienst. Es wird beim
ersten Abruf aus der ISIN ermittelt und gemerkt – das spart bei jedem weiteren Abruf
eine Anfrage, und das Freikontingent ist knapp.
</p>
<?php endif; ?>
<?php include __DIR__ . '/../layout/fuss.php'; ?>