1650 lines
103 KiB
Markdown
1650 lines
103 KiB
Markdown
# Änderungen
|
||
|
||
## Regeln lassen sich auf Buchungen ohne Kategorie anwenden (27.09.2026)
|
||
|
||
Der Knopf unter Verwaltung → Regeln hieß „Gegen offene Umsätze prüfen“ und zählte nur,
|
||
wie viele noch nicht abgeglichene Umsätze eine Regel träfe – das beantwortet weder, wo im
|
||
Kontoblatt schon Lücken liegen, noch schließt es sie.
|
||
|
||
- Neu: `Buchung::ohneKategorie()` liefert Buchungen ganz ohne Kategorie – weder direkt
|
||
(`kategorie_id`) noch in einer Splitzeile –, ausgenommen Umbuchungen (die haben per
|
||
Definition ein Gegenkonto statt einer Kategorie).
|
||
- Neu: `Bankabgleich::regelnAufBuchungenOhneKategorieAnwenden()` trägt bei jeder Buchung
|
||
ohne Kategorie die erste passende Regel nach (Kategorie, Klasse, Gegenkonto oder
|
||
Split) – ohne Treffer bleibt die Buchung unverändert, ein abgeschlossener Monat oder
|
||
sonstiger Fehler betrifft nur die eine Buchung und bricht den Lauf nicht ab.
|
||
- Der Knopf „Regeln auf Buchungen ohne Kategorie anwenden“ (mit Rückfrage) ersetzt den
|
||
reinen Testlauf; die Regel-Trefferzähler zählen mit.
|
||
- Test in `tests/16_BankingTest.php` (`buchungOhneKategorieWirdGefunden`,
|
||
`regelnKategorisierenBuchungenOhneKategorie`).
|
||
|
||
## `update.sh` zieht aus dem Fork (25.09.2026)
|
||
|
||
- Das Update-Skript holt den Code ausdrücklich per `git pull --ff-only origin main`. Vorher
|
||
brach es ohne Tracking-Branch mit „Es gibt keine Tracking-Informationen“ ab.
|
||
- Quelle ist der Fork, weil es derzeit nur die Entwicklungsumgebung gibt; der Upstream ist
|
||
Backup und Veröffentlichung. Für eine spätere Produktion: `UPDATE_REMOTE=upstream`.
|
||
- Das Skript bricht bei unverändertem Code nicht mehr ab: Container starten und Migrationen
|
||
laufen immer, damit ein von Hand gezogener Stand nicht ohne Migration bleibt.
|
||
- Der Pull läuft mit HTTP/1.1 (Codeberg liefert bei HTTP/2 sonst 504).
|
||
|
||
## Echtzeitüberweisung im Zahlungsverkehr (25.09.2026)
|
||
|
||
Der Überweisungsdialog bietet jetzt das Häkchen **Echtzeitüberweisung**: Das Geld ist binnen
|
||
Sekunden beim Empfänger. SEPA behandelt die Echtzeitüberweisung als eigenes Verfahren
|
||
(SCT Inst), der Finanzmanager deshalb auch.
|
||
|
||
- Über FinTS geht der Auftrag als Geschäftsvorfall HKIPZ hinaus (`SendSEPARealtimeTransfer`
|
||
aus php-fints) statt als HKCCS. In der pain.001-Nachricht steht die Zahlungsart
|
||
`LclInstrm/Cd = INST`; das gibt es nur im ISO-Format `pain.001.001.03`.
|
||
- Eine Echtzeitüberweisung hat keinen Ausführungstermin: Der Dialog sperrt das Feld, ein
|
||
Termin in der Zukunft wird beim Speichern abgewiesen.
|
||
- Bietet die Bank keine Echtzeitüberweisung an, scheitert der Auftrag mit einer klaren
|
||
Meldung und bleibt stehen. Eine stille Umwandlung in eine gewöhnliche Überweisung durch
|
||
die Bank ist abgeschaltet.
|
||
- Die Übersicht kennzeichnet Echtzeitüberweisungen in der Spalte „Ausführung“, das
|
||
Übermittlungsprotokoll nennt sie ausdrücklich.
|
||
- Neue Spalte `zahlungsauftraege.echtzeit` (Migration `019`).
|
||
- Test 43 (`tests/43_EchtzeitueberweisungTest.php`) und ein E2E-Test für das Sperren des
|
||
Termins.
|
||
|
||
## Übermittlungsprotokoll für Überweisungen und Bankkommunikation (21.09.2026)
|
||
|
||
Als Nachweis gegenüber der Bank, dass eine Überweisung fristgerecht und korrekt übermittelt
|
||
wurde, speichert der Finanzmanager jetzt den gesamten Dialog – gesendete und empfangene
|
||
Meldungen im Wortlaut, im Erfolgs- wie im Fehlerfall. PIN und Benutzerkennung sind maskiert
|
||
(das erledigt die FinTS-Bibliothek, bevor die Zeilen ankommen).
|
||
|
||
- **Zahlungsverkehr:** Ein Klick auf den Status einer Überweisung öffnet ihr
|
||
Übermittlungsprotokoll in einem modalen Dialog. Eine Überweisung mit TAN-Freigabe hat
|
||
mehrere Durchläufe; alle stehen darin, der älteste zuerst. Ohne JavaScript führt der
|
||
Status auf dieselbe Ansicht als eigene Seite.
|
||
- **Verwaltung › Online-Banking › Protokoll** (`/banking/protokoll`): das Gesamtprotokoll
|
||
über jede Kommunikation mit den Banken – Überweisungen, Umsatzabrufe und die Abfrage der
|
||
TAN-Verfahren. Filterbar nach Bank, Zeitraum und Freitext.
|
||
- Neue Tabelle `bank_protokolle` (Migration `018`), Modell `Bankprotokoll`, Mitschnitt über
|
||
`Services\BankProtokollLogger`. Das Protokoll bleibt stehen, auch wenn die Überweisung
|
||
gelöscht wird; es wird nicht automatisch aufgeräumt.
|
||
- Test 42 (`tests/42_UebermittlungsprotokollTest.php`).
|
||
|
||
## Letzter roter E2E-Test aus dem Typeahead-Umbau behoben (04.09.2026)
|
||
|
||
Ein Test blieb nach der Anpassung an den Typeahead (siehe unten) noch rot:
|
||
„Bargeld-Tool › Eine vorgemerkte Kategorie wird beim Zuordnen geprüft“ – und zwar
|
||
zuverlässig, nicht nur gelegentlich. Ursache war kein Rest des Umbaus mehr, sondern ein zu
|
||
knapper Timeout: Die Schnellzuordnung im Kontoblatt prüft dabei gegen eine absichtlich
|
||
ungültige Adresse (`kasse.beispiel.invalid`), und bis der serverseitige Abruf daran
|
||
scheitert, vergehen durch `BargeldTool::ZEITGRENZE` (8 s Verbindungsaufbau) plus die
|
||
Namensauflösung der ungültigen Adresse über 15 Sekunden – mehr, als Playwrights
|
||
Standard-Timeout von 5 s für `toContainText()` hergibt. Die betroffenen Erwartungen tragen
|
||
jetzt einen eigenen Timeout von 25 s, der Test insgesamt 90 s statt 60 s.
|
||
|
||
## E2E-Datenbestand wird vor jedem Lauf aufgeräumt (04.09.2026)
|
||
|
||
Die Browser-Tests legten ihre Konten, Kategorien und Buchungen bei jedem Lauf neu an,
|
||
weggeräumt hat sie niemand: Zuletzt standen über dreitausend Zeilen Testdaten in der
|
||
Datenbank – vierzig „E2E Bankprovision …“ hintereinander im Kategoriefilter des
|
||
Kontoblatts, und kein Bildschirmfoto eines fehlgeschlagenen Tests war mehr zu gebrauchen.
|
||
|
||
`scripts/e2e-aufraeumen.php` räumt den Bestand weg; `tests/e2e/run.sh` ruft es vor jedem
|
||
Lauf auf. Vor dem Lauf und nicht danach, weil ein abgebrochener Lauf sonst einen Bestand
|
||
hinterlässt, an den niemand mehr denkt – und weil die Daten des letzten Laufs so bis zum
|
||
nächsten für die Fehlersuche stehen bleiben. `E2E_KEIN_AUFRAEUMEN=1` lässt sie liegen,
|
||
`--probe` zählt nur, was wegfiele.
|
||
|
||
Gelöscht wird, was am Testnutzer hängt – nicht, was nach Testdaten aussieht. Ein
|
||
Namensmuster übersähe die Buchung „Supermarkt“ und träfe irgendwann etwas Echtes; die
|
||
Fremdschlüssel dagegen sagen genau, was zu diesem Nutzer gehört, und nehmen über ihre
|
||
Kaskaden auch die Zeilen ohne eigene `benutzer_id` mit (Splitzeilen, Budgetpositionen,
|
||
Bankkonten, Kurse). Der Nutzer selbst bleibt stehen, sonst käme der nächste Lauf nicht
|
||
durch die Anmeldung.
|
||
|
||
Als Schranke gegen den Fehlgriff muss er den Anzeigenamen tragen, den
|
||
`scripts/e2e-benutzer.php` setzt: Wer `E2E_USER` versehentlich auf seinen eigenen Zugang
|
||
stellt, bekommt einen Abbruch statt eines leeren Datenbestands.
|
||
|
||
Das Aufräumen deckte auf, dass sieben Tests auf den Resten früherer Läufe standen: Die
|
||
Auswertungen prüfen, dass ein Bericht eine Tabelle statt eines Platzhalters zeigt, das
|
||
Cockpit, dass „Übrige Konten“ eine Ebene tiefer führt – Daten dafür legte keiner von ihnen
|
||
an. Auf einer frisch aufgeräumten Instanz fielen sie aus; vorher hing ihr Ergebnis davon
|
||
ab, wer zuletzt was hatte laufen lassen. `scripts/e2e-grundbestand.php` stellt deshalb nach
|
||
dem Aufräumen einen festgelegten Bestand her, dessen Zuschnitte an genau diesen Tests
|
||
hängen: mehr Konten als `Vermoegensentwicklung::SERIEN`, Buchungen in jedem Monat des
|
||
laufenden Jahres, ein Vorjahr für den Vergleichsbericht, ein Depot mit Wertpapiergeschäft.
|
||
Die Suite prüft damit dasselbe wie vorher, aber auf einem Bestand, den sie kennt.
|
||
|
||
## E2E-Tests an den Typeahead im Kontoblatt angepasst (04.09.2026)
|
||
|
||
Zwölf E2E-Tests standen seit dem Typeahead-Umbau (Fix #41) auf Rot – nicht, weil die
|
||
Anwendung etwas falsch machte, sondern weil die Specs den alten Stand prüften. Vier
|
||
Ursachen lagen darunter:
|
||
|
||
- **Die Kombobox versteckt ihr `<select>`.** `kombobox.js` lässt das ursprüngliche Feld
|
||
zwar im Formular stehen, setzt es aber auf `hidden`; `selectOption()` wartet darauf
|
||
vergeblich, bis der Test nach 30 Sekunden abbricht. Der neue Helfer
|
||
`tests/e2e/specs/hilfen.js` bedient stattdessen das Eingabefeld: Er tippt die
|
||
Beschriftung ein – das ist zugleich die Probe auf die Filterung – und klickt den genau
|
||
passenden Treffer an. Exakt muss der Vergleich sein, weil `[Depot]` sonst auch das
|
||
`[Depot] …` des Wertpapiergeschäfts träfe.
|
||
- **Zwei Erfolgsmeldungen übereinander.** Seit dem Schnellspeichern ohne Neuladen
|
||
(Fix #40) bleibt die Meldung des Anlegens stehen und die neue kommt darunter. Die Tests
|
||
meinen die neue und greifen jetzt zu `.last()`.
|
||
- **Die Adressliste ist eine Bearbeitungstabelle.** Seit Fix #51 stehen die Namen in
|
||
Eingabefeldern, wo `toContainText` nicht hinsieht. Geprüft wird nun der Feldwert – die
|
||
Adresse selbst wurde die ganze Zeit korrekt gespeichert.
|
||
- **Zwei Folgefehler.** Playwright startet nach jedem Fehlschlag einen frischen Worker;
|
||
damit wird das Spec-Modul neu geladen, seine Marke neu gezogen und `beforeAll` legt neue,
|
||
leere Konten an. Die Tests „Kontoblatt lässt sich durchsuchen“ und „Löschen fragt nach“
|
||
suchten deshalb eine Buchung, die auf dem Konto des vorigen Workers lag. Sie brauchten
|
||
keine Änderung.
|
||
|
||
Die Suite läuft wieder vollständig durch: 87 Tests grün, und ohne die Timeouts in 2,7
|
||
statt 7,1 Minuten.
|
||
|
||
## FinTS-Bibliothek auf den aktuellen Upstream-Stand gehoben (04.09.2026)
|
||
|
||
Der Fork `nemiah/php-fints` steht jetzt auf `v4.1.4` und damit auf dem Upstream-Stand vom
|
||
03.09.2026. Der eigene Patch für den beim Close Match gemeldeten Kontoinhaber
|
||
(`getDifferingPayeeName()`) ist dort **gemergt** und entfällt als eigener Commit; übrig
|
||
bleiben im Fork nur noch das eingebaute psr/log und der VOP-Stepwise-Patch, der als
|
||
[PR #584](https://github.com/nemiah/phpFinTS/pull/584) offen ist. Dieser wurde auf den neuen
|
||
Upstream-Stand rebased – der Konflikt entstand allein daraus, dass beide Patches ihre Tests
|
||
an dieselbe Stelle der Testdatei setzten.
|
||
|
||
Mitgenommen sind damit die fremden Verbesserungen seit Juli: Typsicherheit in den
|
||
Action-Klassen, vollständiges Auslesen mehrzeiliger Verwendungszwecke aus camt, die
|
||
Gegenparteien aus camt, Kreditkartenumsätze der DKB sowie ein behobener Typfehler beim
|
||
Lesen der minimalen Vorlaufzeit für SEPA-Lastschriften.
|
||
|
||
## Zahlungsverkehr: Auftragskonto über die Kontenzuordnung finden (01.09.2026)
|
||
|
||
Beim Senden suchte `FinTsUeberweisung` unter den Konten, die die Bank meldet, nach der IBAN
|
||
aus dem Konto selbst. Die ist in der Kontenverwaltung aber als optional ausgewiesen und
|
||
bleibt meist leer – dann scheiterte die Überweisung an „Die Bank führt kein Konto mit der
|
||
IBAN des Auftragskontos“, obwohl der Umsatzabruf für dasselbe Konto längst lief.
|
||
|
||
Maßgeblich ist jetzt dieselbe Zuordnung wie beim Abruf: die Zeile in `bank_konten`, die der
|
||
Nutzer dem Konto zugewiesen hat. Dort steht die IBAN, die die Bank selbst gemeldet hat, und
|
||
die Kontonummer für die Häuser, die keine IBAN liefern. Der bisherige Weg bleibt bestehen:
|
||
Wer die IBAN am Konto gepflegt hat, kommt auch ohne Zuordnung zum Ziel, und führt die Bank
|
||
nur ein einziges Konto, ist die Sache ohnehin eindeutig.
|
||
|
||
## Zahlungsverkehr: Namensabgleich vor der Überweisung (01.09.2026)
|
||
|
||
Seit Oktober 2025 gleicht die Bank vor der Ausführung den Empfängernamen mit dem
|
||
Kontoinhaber ab (Verification of Payee). Der Finanzmanager kannte diese Stufe nicht, die
|
||
Überweisung blieb stehen.
|
||
|
||
`FinTsUeberweisung` behandelt den Ablauf jetzt vollständig: Auf das Ergebnis wird gewartet,
|
||
solange die Bank es braucht; **stimmt der Name überein, bestätigt der Finanzmanager selbst**
|
||
und geht direkt zur TAN. Weicht der Name ab, ist er nur nahezu gleich oder konnte die Bank
|
||
nicht prüfen, kommt die neue Maske `zahlungsverkehr/vop.php` – mit dem Ergebnis in einem
|
||
deutschen Satz, dem Aufklärungstext der Bank als Klartext (ihr HTML wird abgeräumt) und der
|
||
Wahl zwischen *Trotzdem senden* und *Abbrechen*. Beim Abbrechen bleibt der Auftrag Entwurf.
|
||
|
||
Stimmt der Name nur **nahezu** überein, zeigt die Maske neben dem angegebenen Empfänger
|
||
auch den bei der Bank hinterlegten Kontoinhaber – die Angabe, die die Rückfrage überhaupt
|
||
beantwortbar macht. Weicht der Name ganz ab, nennt die Bank ihn nicht; das steht dann als
|
||
Erklärung in der Maske.
|
||
|
||
Dazu gehören zwei Eingriffe im FinTS-Fork (`v4.1.3`): Meldet eine Bank die schrittweise
|
||
Lieferung des Prüfberichts (`Art der Lieferung Payment Status Report` = `S`, etwa
|
||
comdirect), lehnte php-fints jede Überweisung ab. Der Parameter beschreibt laut
|
||
Spezifikation aber nur, wie die Bank den Bericht aufteilt, *wenn* sie ihn über mehrere
|
||
Runden liefert – bei einer Einzelüberweisung passiert das nie. Der Fork prüft ihn deshalb
|
||
erst, wenn tatsächlich eine Teillieferung eintrifft. Und der beim Close Match gemeldete
|
||
Kontoinhaber wurde zwar eingelesen, aber nicht an die Anwendung durchgereicht; dafür gibt
|
||
es jetzt `VopConfirmationRequest::getDifferingPayeeName()`. Beide Commits liegen zusätzlich
|
||
auf eigenen Branches (`vop-stepwise`, `vop-close-match-name`), damit daraus PRs an das
|
||
Upstream-Projekt werden können.
|
||
|
||
## Zahlungsverkehr: SEPA-Format der Bank (01.09.2026)
|
||
|
||
Die Überweisung wurde bisher immer als `pain.001.001.03` eingereicht. Banken, die nur
|
||
das ältere DK-Format `pain.001.003.03` führen, weisen sie damit ab, noch bevor eine TAN
|
||
angefordert wird ("The bank does not support the XML schema …").
|
||
|
||
`FinTsUeberweisung` liest die unterstützten Formate jetzt nach der Anmeldung aus den
|
||
Bankparameterdaten (Segment HISPAS) und `SepaXml::schemaWaehlen()` bestimmt daraus den
|
||
Namensraum der Nachricht – bevorzugt das ISO-Format, sonst das DK-Format. Der Zusatz
|
||
`_GBIC_x`, den manche Banken anhängen, stört den Vergleich nicht. Meldet die Bank
|
||
ausschließlich Formate, die der Finanzmanager nicht erzeugt, nennt die Fehlermeldung sie
|
||
jetzt beim Namen, statt die Bibliothek durchschlagen zu lassen.
|
||
|
||
Für die Einzelüberweisung sind beide Fassungen im Aufbau gleich; es genügt der andere
|
||
Namensraum. `tests/schemata/pain.001.003.03.xsd` (DFÜ-Abkommen Anlage 3, Version 2.7) liegt
|
||
der Testsuite bei, damit die erzeugte Nachricht wirklich gegen das Schema geprüft wird.
|
||
|
||
## Adressverwaltung in der Verwaltung, Typeahead im Zahlungsverkehr (31.08.2026)
|
||
|
||
Das Adressbuch (bisher nur über die Verträge-Kachel erreichbar) bekommt in der Verwaltung
|
||
eine eigene Kachel „Adressverwaltung“ (Feature #51) – es speist auch den Zahlungsverkehr,
|
||
der mit Verträgen nichts zu tun hat. Die Adressliste unter `/vertraege/adressen` bleibt die
|
||
gleiche Seite; zu jeder Adresse klappt jetzt ein `<details>`-Element ihre letzten Buchungen
|
||
auf. Da die Buchung kein Fremdschlüsselfeld auf die Adresse führt, sucht
|
||
`Buchung::letzteFuerEmpfaenger()` kontenübergreifend nach dem exakten Empfängernamen – die
|
||
eindeutige Kombination aus `benutzer_id` und `name` in `adressen` (Migration 008) macht das
|
||
zuverlässig.
|
||
|
||
Die Empfängerauswahl im Zahlungsverkehr-Formular ist jetzt ein Typeahead: `kombobox.js`
|
||
(schon für die Kategorie im Kontoblatt im Einsatz) baut das `<select>` in ein Eingabefeld mit
|
||
gefilterter Liste um, ohne dass App- oder Formular-Code etwas davon merken – Wert und
|
||
Ändern-Ereignis bleiben identisch. Geprüft wurde außerdem, dass „Empfänger ins Adressbuch
|
||
übernehmen“ beim Erfassen einer Überweisung tatsächlich eine Adresse anlegt
|
||
(`ZahlungsverkehrController::anlegen()` → `Adresse::anlegen()`).
|
||
|
||
## Zahlungsverkehr: automatische Bankauswahl (31.08.2026)
|
||
|
||
Beim Erfassen einer Überweisung musste bisher neben dem Auftragskonto auch die
|
||
Bankverbindung von Hand gewählt werden – obwohl sich diese meist schon aus dem Konto ergibt
|
||
(Feature #50). Das Formular fragt jetzt nur noch das Auftragskonto ab; die passende Bank
|
||
ermittelt `Bankzugang::zugangJeKonto()` anhand der Kontenzuordnung (`bank_konten.konto_id`)
|
||
und trägt sie beim Speichern automatisch in `zahlungsauftraege.zugang_id` ein.
|
||
|
||
Konten ohne konfigurierte Bankverbindung stehen als Auftragskonto gar nicht erst zur
|
||
Auswahl – ohne Bank ließe sich ohnehin nichts senden. Ein bereits erfasster Auftrag behält
|
||
sein Auftragskonto auch dann, wenn dessen Bank nachträglich entfernt wurde: Es bleibt in der
|
||
Auswahl, statt beim nächsten Speichern unbemerkt zu wechseln.
|
||
|
||
## Einstellungen: Reiter "Profil" zum Ändern des eigenen Passworts (20.08.2026)
|
||
|
||
Bislang ließ sich das eigene Passwort nur direkt über den Datenbestand ändern (Feature #46).
|
||
Der Einstellungsdialog bekommt dafür einen neuen Reiter `Profil`, der den angemeldeten
|
||
Benutzernamen anzeigt und ein eigenes Formular für die Passwortänderung enthält – getrennt
|
||
vom generischen Speichern-Formular der übrigen Reiter, weil hier kein Einstellungswert,
|
||
sondern der Anmeldedatensatz geschrieben wird.
|
||
|
||
`EinstellungenController::passwortAendern()` (Route `POST /einstellungen/passwort`) verlangt
|
||
das aktuelle Passwort (geprüft über `Benutzer::pruefeAnmeldung()`), ein neues mit mindestens
|
||
10 Zeichen – dieselbe Grenze wie bei der Ersteinrichtung – und dessen Wiederholung. Bei Erfolg
|
||
schreibt `Benutzer::passwortSetzen()` den neuen Hash; ein Eintrag landet wie bei An- und
|
||
Abmeldung im Debug-Log.
|
||
|
||
## CONTRIBUTE.md und Setup-Skript für Mitwirkende (19.08.2026)
|
||
|
||
`CONTRIBUTE.md` beschreibt, wie Beiträge ankommen (Issue, PR, E-Mail), welche
|
||
Unterstützung gesucht wird und die drei bindenden Leitlinien des Projekts: FOSS,
|
||
Datensparsamkeit und Security mit direktem Kontakt an fds2610@sonnengetrock.net
|
||
für Sicherheitslücken. Vorangestellt ist ein Disclaimer, dass VIBE Coding
|
||
ausdrücklich erlaubt ist – das Projekt selbst ist mit intensivem LLM-Einsatz
|
||
entstanden (Feature #43).
|
||
|
||
Neu ist außerdem `dev-setup.sh` im Repo-Root: richtet Docker (optional),
|
||
Checkout mit Fork- und Upstream-Remote, lokale Container und optional die
|
||
E2E-Testinfrastruktur ein. Zielverzeichnis, E2E-Setup und Docker-Installation
|
||
lassen sich über Flags oder Umgebungsvariablen abweichend vom Default setzen.
|
||
|
||
## Kontoblatt: Typeahead für die Kategorie (19.08.2026)
|
||
|
||
Die Kategorie ließ sich im Kontoblatt nur über eine lange Auswahlliste finden – bei vielen
|
||
Kategorien ein langes Blättern (Feature #41). Ein neues `kombobox.js` baut ein markiertes
|
||
`<select data-kombobox>` beim Laden der Seite in ein Eingabefeld mit darunter aufklappender,
|
||
gefilterter Liste um; Gruppen (Optgroups) und die Reihenfolge der Optionen bleiben erhalten.
|
||
Das ursprüngliche `<select>` bleibt unsichtbar im Formular stehen, damit Wert, Name und das
|
||
Ändern-Ereignis für vorhandenen Code unverändert bleiben.
|
||
|
||
Betroffen sind der Kategorie-Filter oben im Kontoblatt und die Schnellzuordnung je Buchung
|
||
(`kontoblatt.js` öffnet dort die Kombobox statt `showPicker()`, sobald ein `<select>`
|
||
umgebaut wurde). Die Klassen-Auswahl bleibt bewusst ein gewöhnliches `<select>` – ihre Liste
|
||
ist kurz. Ohne JavaScript bleibt es überall beim normalen `<select>`.
|
||
|
||
## Kontoblatt: Schnellzuordnung ohne Neuladen (19.08.2026)
|
||
|
||
Das Ändern von Kategorie oder Klasse im Kontoblatt lud bisher die ganze Seite neu, um die
|
||
Zeile zu aktualisieren – dabei sprang der Bildschirm nach ganz oben. Bei einer älteren
|
||
Buchung weit unten in der Liste musste man nach jeder Änderung wieder dorthin zurückscrollen
|
||
(Fehler #40).
|
||
|
||
kontoblatt.js schickt die Auswahl jetzt im Hintergrund per `fetch`; Kategorie- bzw.
|
||
Klassenspalte und – falls eingerichtet – das Symbol der Bargeld-Abgleichspalte aktualisieren
|
||
sich an Ort und Stelle, die Bildlaufposition bleibt stehen. Die Meldung „Zuordnung
|
||
gespeichert.“ erscheint wie gewohnt, jetzt über `window.FmMeldungen` (app.js) ohne
|
||
Seitenwechsel.
|
||
|
||
Serverseitig unterscheiden `BuchungenController::zuordnung()` und `::klasse()` per
|
||
`X-Requested-With: XMLHttpRequest`, ob eine JSON-Antwort erwartet wird oder die bisherige
|
||
Weiterleitung mit Flash-Meldung – Letztere bleibt der einzige Weg ohne JavaScript. Schlägt
|
||
die Hintergrundanfrage fehl (Netzwerk, abgelaufene Anmeldung), übernimmt wie bisher eine
|
||
echte Formularübermittlung.
|
||
|
||
## Bargeld-Tool: Adresse mit `/api/v1` (19.08.2026)
|
||
|
||
Der Verbindungstest scheiterte mit „Das Bargeld-Tool kennt die Schnittstelle nicht
|
||
(HTTP 404)“, wenn die hinterlegte Adresse den Schnittstellenpfad schon enthielt – der
|
||
Finanzmanager hängt ihn selbst an und fragte dann unter `/api/v1/api/v1/status` an. Ein
|
||
abschließendes `/api` bzw. `/api/v1` kürzt er jetzt beim Speichern weg; bereits
|
||
gespeicherte Adressen räumt Migration 017 auf.
|
||
|
||
## Abgleich mit dem Bargeld-Tool (19.08.2026)
|
||
|
||
Bargeldausgaben werden in einem zweiten Programm erfasst; im Finanzmanager kommen sie als
|
||
Kartenumsatz oder Abhebung an. **Beim Zuweisen einer Kategorie fragt der Finanzmanager
|
||
jetzt dort nach**, ob es zum Betrag schon eine Buchung gibt – und zeigt die Antwort im
|
||
Kontoblatt.
|
||
|
||
Eingerichtet wird das unter **Verwaltung → Bargeld-Tool**, einer neuen Kachel: Adresse,
|
||
Benutzerkennung, Passwort (AES-verschlüsselt wie die FinTS-PIN), Suchzeitraum in Tagen und
|
||
die Auswahl der Kategorien, die überhaupt abgeglichen werden. Die Vormerkung steht als
|
||
`bargeld_abgleich` an der Kategorie selbst und nicht als Liste von Nummern in den
|
||
Einstellungen – so verschwindet sie mit der Kategorie, statt als tote Nummer liegenzubleiben.
|
||
Ein Knopf *Verbindung testen* sagt, ob Adresse und Zugangsdaten stimmen, ohne dass dafür
|
||
eine Buchung herhalten muss.
|
||
|
||
Geprüft wird nur, was geprüft werden soll: eine Kategorie an der Buchung (keine Umbuchung,
|
||
keine Splitbuchung), diese Kategorie vorgemerkt, eine Adresse konfiguriert. Fällt eines weg,
|
||
verschwindet der Status – auch beim Umkategorisieren und beim Abwählen einer Kategorie in der
|
||
Verwaltung, sonst bliebe ein alter Haken an einer Buchung stehen, die niemand mehr abgleicht.
|
||
Gesucht wird rückwärts ab dem Buchungsdatum (Vorgabe zehn Tage): Bargeld wird ausgegeben,
|
||
bevor der Umsatz auf dem Konto ankommt, nie danach.
|
||
|
||
Im Kontoblatt steht das Ergebnis in einer schmalen Spalte **zwischen Klasse und Betrag**:
|
||
grauer Kreis (wird nicht abgeglichen), grüner Haken (gefunden), rotes X (nicht gefunden),
|
||
Ausrufezeichen (Abruf gescheitert). Das Ausrufezeichen behält ausdrücklich die Textfarbe des
|
||
Farbschemas – der Fehler liegt beim Abruf, nicht bei der Buchung; rot oder grün wäre eine
|
||
Aussage über die Buchung, die niemand treffen kann. Der Tooltip nennt den Treffer bzw. die
|
||
Meldung der Gegenstelle. Ohne hinterlegte Adresse entfällt die Spalte ganz; sie zeigte sonst
|
||
in jeder Zeile denselben grauen Kreis. Der Status steht an der Buchung, das Kontoblatt geht
|
||
also nicht bei jedem Blättern erneut übers Netz.
|
||
|
||
Ein Fehlschlag ist ein Ergebnis, kein Abbruch: Weder ein abgeschaltetes Bargeld-Tool noch ein
|
||
Netzfehler verhindert, dass die Kategorie gespeichert wird. Der Abruf bricht nach acht
|
||
Sekunden ab. Null Treffer sind kein Fehler – ein 404 dagegen schon, denn er kann ebenso gut
|
||
heißen, dass es den Endpunkt nicht gibt.
|
||
|
||
Die Anforderung an die Gegenstelle (zwei lesende Aufrufe, Basic Auth, JSON) steht im Ticket
|
||
und in `docs/buchungen.md`. Nebenbei: Test 02 verlangte bisher für jeden
|
||
Einstellungsbereich einen Reiter im Dialog. Die Bargeld-Einstellungen haben stattdessen eine
|
||
eigene Seite – deshalb gibt es jetzt `Einstellung::EIGENE_SEITEN`, und der Test prüft die
|
||
Regel, die eigentlich gemeint war: dass jeder Bereich erreichbar bleibt.
|
||
|
||
Der E2E-Test geht denselben Weg im Browser – Kachel, Formular, Zuordnung im Kontoblatt –
|
||
gegen eine Adresse, die es nicht gibt: Genau daran zeigt sich, dass ein gescheiterter Abruf
|
||
die Zuordnung nicht aufhält, sondern nur das Ausrufezeichen setzt.
|
||
|
||
Test 34 prüft den Abruf gegen einen echten HTTP-Server (der eingebaute Server von PHP mit
|
||
einem Router, der sich wie das Bargeld-Tool verhält). Nachgebildete Antworten allein würden
|
||
genau das übergehen, woran ein Abruf scheitert: Basic Auth, HTTP-Status, ein Rumpf, der kein
|
||
JSON ist.
|
||
|
||
## Berichte: Tabellen so breit wie nötig, Zahlen rechtsbündig (18.08.2026)
|
||
|
||
Ein Bericht mit zwei oder vier Spalten wurde bisher über die gesamte Fensterbreite gezogen –
|
||
zwischen Bezeichnung und Betrag klaffte ein leeres Feld, und das Auge musste die Zeile
|
||
zweimal überqueren. Die **Berichtstabellen nehmen jetzt nur den Platz, den ihre Spalten
|
||
brauchen** (`width: auto`) und stehen dabei mittig unter der Überschrift; erst wenn der
|
||
Inhalt mehr verlangt, wachsen sie bis zur vollen Breite. Im Scrollrahmen `.breittabelle` (zwölf Monatsspalten) bleibt es beim
|
||
bisherigen Verhalten: Die Tabelle darf hinausragen und waagerecht scrollen, statt umzubrechen.
|
||
|
||
Dabei fiel auf, dass die Zahlenspalten **gar nicht rechtsbündig** standen: `.rechts` und
|
||
`.betrag` sind nur einfach spezifisch, `table.liste th, table.liste td { text-align: left }`
|
||
weiter oben im Stilblatt ist spezifischer und gewann. Solange die Spalten über den Bildschirm
|
||
gezogen waren, fiel es kaum auf – in der schmalen Tabelle sofort. Die Ausrichtung steht jetzt
|
||
für **alle Listen** ausdrücklich (`table.liste th.rechts, table.liste td.rechts`): Beträge,
|
||
Salden, Stückzahlen und Aktionsspalten stehen überall rechts, wo `.rechts` gesetzt ist –
|
||
Kontenübersicht, Kontoblatt, Wertpapiere, Depotbuchungen, Kategorien und die Berichte
|
||
gleichermaßen. Die Klasse tut damit endlich das, was ihr Name sagt.
|
||
|
||
Der neue E2E-Test misst alles drei: dass der Summenbericht deutlich schmaler bleibt als sein
|
||
Rahmen und links wie rechts gleich viel Luft behält, dass die letzte Spalte
|
||
`text-align: right` hat und dass die Monatsansicht weiterhin breiter wird.
|
||
|
||
Nachgezogen: Der E2E-Test der eingeklappten Seitenleiste erwartete `toBeHidden()` für die
|
||
Beschriftungen. Die sind aber mit Absicht nur aus dem Bild geschoben (`clip-path`, 1×1 Pixel)
|
||
statt entfernt, damit Vorleseprogramme die Namen behalten – für Playwright bleibt so ein
|
||
Element „sichtbar“. Geprüft wird jetzt, dass die Beschriftung keinen Platz mehr einnimmt und
|
||
der Verweis trotzdem seinen zugänglichen Namen behält.
|
||
|
||
## Seitenleiste: Marke mit Münzstapel, ein Klick schiebt sie auf Symbolbreite (18.08.2026)
|
||
|
||
Oben in der Seitenleiste steht jetzt eine richtige **Marke**: das Bild `Coinstack.png` aus den
|
||
Altdaten als Symbol (`public/static/coinstack.png`, auf 128 px gerechnet) und daneben der
|
||
zweifarbige Schriftzug **Finanz**manager – dieselbe Machart wie die `sidebar-brand` im
|
||
Schwesterprojekt Steuerkram.
|
||
|
||
Die Marke ist zugleich der **Schalter**: Ein Klick schiebt die Seitenleiste von 232 px auf
|
||
64 px zusammen, dann stehen dort nur noch das Markensymbol und die Symbole der Menüpunkte –
|
||
mit unveränderten Zielen; der gewonnene Platz geht an den Hauptbereich. Ein Klick auf das
|
||
Symbol ganz oben klappt sie wieder auf ihre alte Breite auf.
|
||
|
||
Zwei Feinheiten: Der Zustand steht in einem **Cookie**, nicht im `localStorage` – so rendert
|
||
der Server die Leiste schon in der richtigen Breite, statt sie bei jedem Seitenwechsel kurz
|
||
breit aufblitzen zu lassen. Und die Beschriftungen werden eingeklappt nur **aus dem Bild
|
||
geschoben** (`clip-path`), nicht mit `display:none` entfernt: Sonst verlören die Verweise
|
||
ihren Namen für Vorleseprogramme. In schmalen Fenstern, wo die Navigation ohnehin waagerecht
|
||
liegt, bleibt alles wie gehabt.
|
||
|
||
Nebenbei behoben: Die Marke der Seitenleiste hieß bisher schlicht `.marke` – dieselbe Klasse
|
||
wie die kleine Statusmarke weiter unten im Stilblatt, deren Regeln sie überschrieben. Sie
|
||
heißt jetzt `.seitenleisten-marke`.
|
||
|
||
## Bankleitzahlen-Verzeichnis: die FinTS-Adresse nachschlagen statt suchen (18.08.2026)
|
||
|
||
Beim Anlegen einer FinTS-Verbindung fehlt in aller Regel genau eine Angabe: der
|
||
**PIN/TAN-Zugangspunkt**. Die Bankleitzahl kennt jeder auswendig, die Adresse steht tief in
|
||
der Hilfe zum Online-Banking und lautet oft ganz anders als der Name der Bank vermuten lässt
|
||
(1822direkt: `https://fints.1822direkt.com/fints/hbci`).
|
||
|
||
Neu ist deshalb unter *Verwaltung → Bankleitzahlen* (`/verwaltung/bankinstitute`) das
|
||
**Verzeichnis der FinTS-Institute**. Es nimmt die CSV-Liste der Deutschen Kreditwirtschaft
|
||
(`hbci-zka.de`) auf – dieselbe Datei, aus der auch der Lexware FinanzManager seine
|
||
Zugangsdaten zieht – und beantwortet damit die eine Frage, die das Formular stellt.
|
||
|
||
**Einlesen** heißt hier ersetzen: Ein Import wirft den bisherigen Bestand weg, statt ihn zu
|
||
ergänzen; sonst blieben Institute stehen, die es nicht mehr gibt. *Nur prüfen* und
|
||
*Übernehmen* sind zwei Knöpfe an derselben Datei statt zweier Schritte an einer abgelegten:
|
||
Der Inhalt landet vollständig in der Tabelle, ein zweites Exemplar im Importverzeichnis wäre
|
||
nur Ballast zwischen den Kontoauszügen. Der Prüfbericht nennt Zeilen, Bankleitzahlen,
|
||
Zugangspunkte und den Stand der Daten und zeigt die ersten Zeilen als Sichtprobe – stimmen
|
||
dort Umlaute und Spalten, stimmt der Rest auch.
|
||
|
||
Die Datei hat es in sich, und `Services\FintsInstituteImport` nimmt ihr das ab: **ISO-8859-1**
|
||
statt UTF-8, ein Institutsname über **zwei Zeilen** in Anführungszeichen, dieselbe
|
||
**Bankleitzahl vielfach** (einmal je Filiale, mit identischem Zugangspunkt), am Ende
|
||
**Hunderte leerer Zeilen** mit laufender Nummer, und wo es keinen PIN/TAN-Zugang gibt, steht
|
||
Text statt einer Adresse. Angesprochen werden die Spalten über ihre Überschrift, nicht über
|
||
ihre Position; fehlen `BLZ` und `Institut`, bricht der Import ab, statt lauter leere Zeilen
|
||
zu schreiben. Die Spalten der Chipkartenverfahren bleiben liegen – der Finanzmanager spricht
|
||
ausschließlich PIN/TAN.
|
||
|
||
**Gesucht** wird über Bankleitzahl, BIC, Name und Ort in einem Zug. Eine Ziffernfolge nur
|
||
deshalb als Bankleitzahl zu lesen, weil sie aus Ziffern besteht, verschwiege dem Suchenden
|
||
die Bank, die er meint: „1822“ steht bei der 1822direkt im Namen und bei der Frankfurter
|
||
Sparkasse im BIC – in keiner der beiden Bankleitzahlen. Die Bankleitzahl wird von vorn
|
||
verglichen, alles Übrige als Wortbestandteil; Treffer in der Bankleitzahl stehen oben.
|
||
|
||
Aus der Trefferliste führt **Verbindung anlegen** direkt ins FinTS-Formular, das mit Bank,
|
||
Bankleitzahl und Adresse gefüllt dasteht. Umgekehrt gilt: Die **FinTS-Adresse ist kein
|
||
Pflichtfeld** mehr – bleibt sie leer, wird sie zur Bankleitzahl nachgeschlagen. Findet auch
|
||
das nichts, weist `Bankzugang` den Zugang jetzt ab, statt ihn ohne Gegenstelle zu speichern.
|
||
|
||
Dazu Migration `015_bankinstitute.sql` (Tabelle `bankinstitute`, reine Nachschlagedaten ohne
|
||
`benutzer_id`), `Models\Bankinstitut`, `Controllers\BankinstituteController`,
|
||
`tests/32_BankinstituteTest.php` und `tests/e2e/specs/bankinstitute.spec.js`.
|
||
|
||
## PayPal als zweite Art von Bankverbindung (18.08.2026)
|
||
|
||
*Bankverbindung anlegen* führt nicht mehr direkt in die Bankenmaske, sondern zur **Wahl
|
||
der Art**: HBCI/FinTS oder PayPal. Die erste Karte führt in die bisherige Maske und den
|
||
bekannten Dreischritt, die zweite in ein neues Formular mit allen Angaben für die
|
||
PayPal-Abfrage. Die Wahl steht danach fest – ein umgestellter Zugang hätte weder passende
|
||
Zugangsdaten noch passende Konten.
|
||
|
||
**Die Abfrage** läuft über die Classic-NVP-API (`Services\PaypalNvp`, `TransactionSearch`
|
||
und `GetBalance`), übernommen aus dem Parallelprojekt `Steuerkram` samt der Fallstricke,
|
||
die dort schon aufgelaufen sind:
|
||
|
||
- Live- und Sandbox-Zugangsdaten passen nur zu ihrem eigenen Endpunkt; mit dem falschen
|
||
antwortet PayPal mit *„Security header is not valid“* – **dieselbe** Antwort kommt aber
|
||
auch bei schlicht falschen Angaben. Die Fehlermeldung nennt deshalb beide Ursachen, und
|
||
weil PayPal nie sagt, welche der drei Angaben klemmt, prüft
|
||
`PaypalNvp::auffaelligkeiten()` ihre Form (Benutzername `name_api1.domain.de`, Kennwort
|
||
16 Zeichen, Signatur 56) und schreibt Abweichungen ins Protokoll und in die Meldung –
|
||
als Hinweis, nicht als Abweisung, und ohne die Werte mitzuliefern. Kennwort und Signatur
|
||
werden beim Speichern getrimmt: Eine eingefügte Signatur bringt gern ein Leerzeichen mit,
|
||
das dieselbe Meldung auslöst.
|
||
- `TransactionSearch` liefert je Aufruf höchstens 100 Buchungen und meldet das nur als
|
||
Warnung – abgefragt wird deshalb in Abschnitten von 30 Tagen, und eine Warnung gilt als
|
||
Erfolg statt als Abbruch.
|
||
- `GetBalance` antwortet je nach Konto mit `AMT` oder mit `L_AMT0`; gelesen werden beide,
|
||
sonst bliebe der Kontostand auf Live-Konten leer.
|
||
- Gerechnet wird mit `L_NETAMT` (nach Gebühr), Nullbuchungen aus Einwilligungen und
|
||
Abonnements fallen heraus.
|
||
|
||
**Die Kontenzuordnung** steht im selben Formular: PayPal führt je Konto genau ein
|
||
Guthabenkonto, eine eigene Seite wäre eine Zeile Tabelle. Das Dropdown listet die eigenen
|
||
Konten und bietet *neues Konto anlegen* mit Name, Kontoart und Anfangssaldo. Ohne
|
||
Zuordnung bleibt die Verbindung nutzbar – die Umsätze warten dann in der Abgleichliste.
|
||
|
||
Der Fingerabdruck der Umsätze hängt an der Transaktionsnummer von PayPal, nicht an Datum,
|
||
Betrag und Text: Eine nachträglich korrigierte Buchung käme sonst ein zweites Mal herein.
|
||
|
||
**Freigaben (MFA)** sind vorgesehen, ohne erfundenen Code: Die NVP-API authentifiziert
|
||
allein über die drei Zugangsdaten und stellt keine Rückfrage. `PaypalAbruf` liefert aber
|
||
dieselbe Ergebnisform wie `FinTsAbruf` – Status `tan` eingeschlossen –, der Controller
|
||
wertet beide gleich aus, und `bank_abrufe` kennt `tan_offen` bereits. Einzuhängen wäre nur
|
||
die Stelle, an der eine Antwort als Freigabeanforderung erkannt wird.
|
||
|
||
Neu: `migrations/014_paypal_zugang.sql` (Spalten `typ`, `api_benutzer`, `api_kennwort`,
|
||
`api_signatur`, `sandbox`), `Services\PaypalNvp`, `Services\PaypalAbruf`,
|
||
`Views/banking/typ.php`, `Views/banking/paypal.php`, `tests/31_PaypalZugangTest.php` und
|
||
`tests/e2e/specs/banking.spec.js`. Bestehende Zugänge bleiben FinTS-Zugänge.
|
||
|
||
## Spiegelung der Umbuchungen unter Test (17.08.2026)
|
||
|
||
Neuer Test `tests/30_UmbuchungSpiegelungTest.php`. Er nagelt die zwei Zusagen fest, die
|
||
sich aus dem Datenmodell ergeben – eine Umbuchung ist EIN Datensatz mit `gegenkonto_id`,
|
||
die Gegenseite leitet `Buchung::kontoblatt()` daraus ab:
|
||
|
||
1. **Was ein Gegenkonto trägt, steht im Kontoblatt dieses Gegenkontos** – auf allen vier
|
||
Wegen, auf denen ein Gegenkonto an eine Buchung kommt: beim Anlegen, über die
|
||
Schnellzuordnung im Kontoblatt, über die Buchungsmaske und als einzelne Splitzeile.
|
||
Ein gewechseltes Gegenkonto bleibt nicht beim alten hängen, ein entferntes taucht
|
||
nirgends mehr auf, und Such-, Zeitraum- und Kategoriefilter verstecken die Gegensicht
|
||
nicht.
|
||
2. **Wer die Buchung löscht, löscht beide Sichten** – Kontoblatt und Saldo des
|
||
Gegenkontos sind danach wieder wie vorher, Splitzeilen bleiben nicht als Waisen
|
||
zurück. Eine Gegenseite ohne Ursprungsbuchung kann nicht entstehen: Sie trägt die
|
||
Nummer des Originals, ein Löschen „auf dem Gegenkonto“ trifft deshalb dieses.
|
||
|
||
Dazu eine Invariantenprüfung über alle Daten des Mandanten, die an sieben Stellen des
|
||
Testablaufs läuft: Zu jedem gesetzten Gegenkonto gibt es eine Gegensicht, und keine
|
||
Buchung trägt ein Gegenkonto, das durch `gesplittet = 1` verdeckt wäre – das ist die
|
||
einzige Konstellation, in der eine Umbuchung unsichtbar würde, weil dann weder der Zweig
|
||
für Buchungen (`gesplittet = 0`) noch der für Splitzeilen greift. Gegenprobe: Vier
|
||
absichtliche Eingriffe ins Modell (beide Gegensicht-Zweige des Kontoblatts abgeklemmt,
|
||
Gegenkonto hinter einer Splitbuchung stehen gelassen, Löschen wirkungslos gemacht)
|
||
lassen den Test zuverlässig fehlschlagen.
|
||
|
||
## Buchungsmaske nennt das Konto und passt auf einen Bildschirm (17.08.2026)
|
||
|
||
*Neue Buchung* und *Buchung bearbeiten* zeigen oben, in welchem Konto die Buchung liegt
|
||
bzw. gespeichert wird; bisher stand das Konto nur klein in der Kopfzeile über dem
|
||
Formular. Die Maske steht zweispaltig, die Beschriftungen links neben ihren Eingaben und
|
||
die Erklärungen in deren Tooltip – damit endet sie samt Splitzeilen und Knopfleiste
|
||
oberhalb der Fensterhöhe, wo sie vorher über 1190 px lang war.
|
||
|
||
- Kontoangabe als erstes Feld über beide Spalten: Name des Kontos, dahinter die IBAN –
|
||
oder, wenn keine hinterlegt ist, Bank und Kontonummer.
|
||
- Feldreihenfolge zeilenweise: Art/Datum, Betrag/Empfänger, Kategorie-Gegenkonto/Klasse,
|
||
Buchungstext/Nummer, darunter *Abgeglichen*. Namen, Werte und Feld-IDs bleiben
|
||
unverändert; der Bereich *Splitbuchung* läuft weiter über die volle Breite.
|
||
- Beschriftungen stehen links neben ihrer Eingabe, nicht darüber (neue Variante
|
||
`.formular.beschriftung-links`). Das halbiert die Höhe je Feld; Hinweise laufen unter
|
||
Beschriftung und Eingabe hinweg, unter 560 px Fensterbreite rutscht die Beschriftung
|
||
wieder nach oben.
|
||
- Die erklärenden Texte stehen nicht mehr als eigene Zeile zwischen den Feldern, sondern
|
||
im `title` ihrer Beschriftung: Konto, Betrag, *Kategorie / Gegenkonto* und
|
||
*Splitbuchung*. Eine gepunktete Unterstreichung zeigt, wo ein Tooltip hängt.
|
||
- `.formular.zweispaltig` (`public/static/app.css`) stellt jetzt auch Tabellen und
|
||
`.feldwert` richtig: Tabellen laufen über beide Spalten, ohne eigenen Rand.
|
||
- E2E-Test in `tests/e2e/specs/buchungen.spec.js`: Die Maske nennt das Konto, Datum steht
|
||
rechts neben der Art, die Beschriftung links neben ihrer Eingabe, die Erklärungen hängen
|
||
in deren Tooltip, und das ganze Formular endet oberhalb der Fensterhöhe.
|
||
|
||
## Kontoformular zweispaltig (17.08.2026)
|
||
|
||
*Konto bearbeiten* und *Neues Konto* zeigen ihre Felder in zwei Spalten. Alle Eingaben
|
||
stehen damit auf einem Bildschirm, ohne zu scrollen; bisher lagen die vierzehn Felder
|
||
untereinander.
|
||
|
||
- Neue Formularvariante `.formular.zweispaltig` (`public/static/app.css`): ein Grid mit
|
||
zwei Spalten, je Feld ein `.feld` aus Beschriftung, Eingabe und Hinweis. Überschriften,
|
||
Knopfleiste und `.feld.ganz` laufen über beide Spalten. Unter 720 px Fensterbreite fällt
|
||
das Formular auf eine Spalte zurück.
|
||
- Feldreihenfolge zeilenweise: Name/Kontoart, Kontengruppe/Beschreibung, Bank/Kontonummer,
|
||
IBAN/BIC, Anfangssaldo/Kreditlimit, Sortierung/Kennzeichen. Namen, Werte und Feld-IDs
|
||
bleiben unverändert.
|
||
- Der Bereich *Konto löschen* darunter bekommt dieselbe Breite, bleibt aber einspaltig.
|
||
- E2E-Test in `tests/e2e/specs/stammdaten.spec.js`: Kontoart steht rechts neben dem Namen,
|
||
und das Formular endet oberhalb der Fensterhöhe.
|
||
|
||
## Import und Export in zwei Formaten (17.08.2026)
|
||
|
||
Aus der Kachel *QIF-Import* wird **Import/Export** mit je einem Knopf für beide Richtungen.
|
||
Wer darauf drückt, wählt zuerst das **Dateiformat** – QIF oder CSV –, und erst danach öffnet
|
||
sich der Dialog, der genau dieses Format annimmt bzw. schreibt. Bisher gab es nur den einen
|
||
Weg hinein: QIF, und nur herein.
|
||
|
||
**Formatwahl** (`Dateiformat`, `Views/verwaltung/formatwahl.php`)
|
||
|
||
- Die Formate stehen an einer Stelle (`Dateiformat::ALLE`); Dialoge, Auswahlkarten und die
|
||
Prüfung der Dateiendung hängen daran.
|
||
- Auf der Verwaltungsseite ist die Auswahl ein `<dialog>`; die Karten liefert die Ansicht
|
||
mit, das Skript setzt nur die Ziele. Ohne Skript – oder ohne `<dialog>` – führt derselbe
|
||
Knopf auf dieselbe Auswahl als eigene Seite (`/verwaltung/import` bzw. `/verwaltung/export`
|
||
ohne `format`).
|
||
|
||
**CSV-Import** (`CsvBuchungsImport`, `CsvTabelle`)
|
||
|
||
- Buchungen samt Splitzeilen. Die Kopfzeile bestimmt die Spalten, nicht ihre Reihenfolge –
|
||
verglichen wird kleingeschrieben und ohne Umlaute. Unverzichtbar sind `Datum`, `Konto` und
|
||
`Betrag`; fehlt eine, bricht der Lauf ab, statt eine halbe Datei einzulesen.
|
||
- Splitzeilen folgen unmittelbar auf ihre Buchung und tragen in `Art` das Wort *Splitzeile*.
|
||
Eine verwaiste Splitzeile ist ein Fehler und wird gemeldet, nicht stillschweigend fallen
|
||
gelassen.
|
||
- Kategorien und Klassen legt der Import bei Bedarf an, **Konten nicht**: Ein Konto trägt
|
||
Kontoart, Bankverbindung und Anfangssaldo, und nichts davon steht in der Zeile.
|
||
- Dieselbe Dublettenerkennung wie beim QIF (Fingerabdruck aus Konto, Datum, Betrag, Nummer
|
||
und Text, mit laufendem Index für echte Doppelbuchungen am selben Tag). Trockenlauf und
|
||
echter Lauf wie gehabt.
|
||
- `CsvTabelle` erkennt Semikolon oder Komma als Trenner und liest Beträge in deutscher wie
|
||
englischer Schreibweise; gelesen wird über `fgetcsv()`, damit ein maskiertes Feld einen
|
||
Zeilenumbruch enthalten darf.
|
||
|
||
**Export** (`BuchungsExport`, `ExportController`)
|
||
|
||
- Filter nach **Zeitraum, Konto, Kategorie und Klasse**; ohne Häkchen zählt jeweils alles.
|
||
Über dem Formular steht, wie viele Buchungen der Filter trifft.
|
||
- Der Kontofilter greift auf beide Seiten einer Umbuchung, und Kategorie wie Klasse zählen
|
||
auch in den Splitzeilen – sonst fielen genau die Buchungen heraus, die man sucht.
|
||
- Das QIF trägt den Dialekt des FinanzManagers und lässt sich vom eigenen Import wieder
|
||
einlesen; Klassen und Kategorien kommen vollständig mit, die Konten nur, soweit der Filter
|
||
sie zulässt. Das CSV ist dasselbe Format, das der CSV-Import liest.
|
||
- **Exportdateien werden nicht vorgehalten.** Sie entstehen im Speicher und gehen unmittelbar
|
||
an den Browser; ein Exportverzeichnis gibt es bewusst nicht.
|
||
- Das Filterformular sendet als POST: Bei vielen angehakten Kategorien wächst eine GET-Adresse
|
||
über das, was nginx annimmt – die Antwort wäre „414 Request-URI Too Large“ statt der Datei.
|
||
|
||
**Housekeeping** (`Housekeeping`, `scripts/housekeeping.php`, Migration `013`)
|
||
|
||
- Hochgeladene Importdateien bleiben absichtlich liegen (erst Trockenlauf, dann Übernahme)
|
||
und sammeln sich sonst unbegrenzt an. Was älter ist als die eingestellte Frist, wird
|
||
gelöscht – Vorgabe 14 Tage, `0` schaltet es ab (*Einstellungen → Import/Export*).
|
||
- Überwacht werden das Importverzeichnis und `data/tmp`; gelöscht werden nur gewöhnliche
|
||
Dateien unmittelbar darin, keine Symlinks und keine Unterverzeichnisse.
|
||
- Angestoßen beiläufig beim Öffnen von Import und Export, höchstens einmal am Tag. Ein Fehler
|
||
dabei reißt die Seite nicht mit. Für lange ungenutzte Installationen gibt es denselben Lauf
|
||
als Skript (`--probe` zählt nur auf, was wegfiele).
|
||
|
||
**Nebenbei**
|
||
|
||
- Der Upload nimmt jetzt beide Endungen an; welcher Importeur läuft, entscheidet die Endung
|
||
der Datei und nicht ein verstecktes Formularfeld.
|
||
- `BerichtExport` (Auswertungen) nutzt die Zeilen- und Zahlenregeln aus `CsvTabelle`, statt
|
||
sie ein zweites Mal zu führen.
|
||
- `docs/qif-import.md` heißt jetzt `docs/import-export.md` und beschreibt beide Richtungen.
|
||
|
||
## Umsätze im Banking sammelweise löschen (17.08.2026)
|
||
|
||
Die Umsatzlisten des Bankings haben jetzt dieselbe Löschfunktion wie ein Kontoblatt: ein
|
||
Ankreuzfeld vor jedem Umsatz, eines in der Titelleiste für alle angezeigten und darüber den
|
||
Knopf **Auswahl löschen**. Bisher ließ sich ein abgerufener Umsatz nur übernehmen oder
|
||
verwerfen – wegräumen ging gar nicht.
|
||
|
||
- Der Knopf steht in jedem Reiter – *offen*, *übernommen* und *verworfen* –, nennt die Anzahl
|
||
der Auswahl und bleibt ohne Auswahl gesperrt. Die Rückfrage vor dem Löschen nennt sie
|
||
ebenfalls.
|
||
- Gelöscht wird allein die abgerufene Zeile. Eine daraus entstandene Buchung bleibt im
|
||
Kontoblatt stehen; mit dem Umsatz geht aber sein Fingerabdruck, sodass er beim nächsten
|
||
Abruf wieder hereinkommen kann.
|
||
- Neue Route `POST /banking/umsaetze/loeschen` auf `Bankumsatz::mehrereLoeschen()`; sie kehrt
|
||
in den Reiter zurück, aus dem gelöscht wurde.
|
||
- Die Liste ist keine Tabelle, sondern eine Folge von Karten. Die Kopfzeile, die eine Tabelle
|
||
von selbst hätte, gibt es deshalb neu als `.listenkopf`; die Mechanik dahinter ist
|
||
dieselbe wie im Kontoblatt (`.alle-waehlen` mit `data-waehlt`).
|
||
|
||
## Ruhige Buchungszeilen im Kontoblatt (17.08.2026)
|
||
|
||
Die Zeilen des Kontoblatts sind jetzt alle gleich hoch – zwei Textzeilen – und ihr Inhalt
|
||
steht senkrecht in der Mitte. Bisher wuchs eine Zeile mit ihrem Text, sodass die Liste beim
|
||
Blättern sprang; der Buchungstext war zudem nach 90 Zeichen hart abgeschnitten und der Rest
|
||
nirgends zu sehen.
|
||
|
||
- Was auf zwei Zeilen nicht passt, schneidet das Stylesheet ab. Der vollständige Text steht
|
||
im Tooltip – und zwar nur dort, wo tatsächlich etwas fehlt: Ob ein Text abgeschnitten ist,
|
||
entscheidet sich erst nach dem Umbruch, `kontoblatt.js` setzt den Tooltip deshalb nach dem
|
||
Laden und nach jeder Größenänderung des Fensters.
|
||
- Der Buchungstext wird vollständig ausgegeben statt nach 90 Zeichen gekürzt; sichtbar bleibt
|
||
dank Abschnitt gleich viel, im Tooltip steht nun aber alles.
|
||
- Über dem Kategoriefeld einer **Splitbuchung** nennt der Tooltip die Zeilen der Aufteilung:
|
||
je Zeile Kategorie (oder Gegenkonto) und Betrag. In der Spalte selbst steht nur
|
||
*Splitbuchung*; wohin das Geld geht, war bisher erst im Dialog zu sehen.
|
||
|
||
## Klasse im Kontoblatt direkt auswählen (17.08.2026)
|
||
|
||
Die Klassenspalte des Kontoblatts ist jetzt anklickbar – nach derselben Bauart wie die
|
||
Zuordnung daneben: Ein Klick öffnet die Klassenliste in der Zelle, und die gewählte Klasse
|
||
wird sofort gespeichert. Der erste Eintrag *— keine —* löscht die vorhandene Klasse. Bisher
|
||
war dafür jedes Mal der Umweg über die Buchungsmaske nötig.
|
||
|
||
- Neue Route `POST /buchungen/{id}/klasse` auf `Buchung::klasseSetzen()`. Die Methode achtet
|
||
den Monatsabschluss und weist eine Klasse ab, die es nicht gibt.
|
||
- Die Klasse steht **neben** der Zuordnung, nicht in ihr: Kategorie, Gegenkonto und
|
||
Splitzeilen bleiben unberührt, eine gesplittete Buchung bleibt gesplittet.
|
||
- Die Liste gibt es – wie die der Zuordnung – genau einmal im HTML; `kontoblatt.js` hängt sie
|
||
in die angeklickte Zelle. Ohne JavaScript führt der Klick weiterhin in die Buchungsmaske.
|
||
- Zeilen der Gegensicht bleiben unverändert Text: Sie werden auf ihrem Ursprungskonto
|
||
geändert.
|
||
|
||
## Splitbuchungen in den Abgleichregeln (13.08.2026)
|
||
|
||
Eine Regel für den Abgleich kann jetzt eine **Splitbuchung** vorgeben: mehrere Kategorien mit
|
||
einem Anteil in Prozent. Greift die Regel, wird der Umsatz in diesem Verhältnis aufgeteilt –
|
||
bei jedem Betrag neu gerechnet, sodass eine Nebenkostenabrechnung über 300 € und eine über
|
||
450 € nach demselben Schlüssel gebucht werden.
|
||
|
||
Das Schema lag bereits im Modell und in der Datenbank, war aber über die Oberfläche nicht
|
||
erreichbar; die Dokumentation beschrieb ein Feld, das es nirgends zu sehen gab.
|
||
|
||
- Unter jeder Regel klappt *Splitbuchung* die Zeilen auf: Kategorie, Klasse, Notiz, Anteil.
|
||
Dasselbe Formular steht bei *Neue Regel* (`src/Views/banking/regelsplits.php`, in beiden
|
||
Ansichten eingebunden). Zugeklappt, solange eine Regel keine Splitzeilen hat.
|
||
- Die Anteile gelten **zueinander**, nicht absolut: 60 zu 30 sind zwei Drittel zu einem
|
||
Drittel. Bisher hätte die letzte Zeile als Rest 40 statt der eingetragenen 30 Prozent
|
||
bekommen – ein Fehler, der in der fertigen Buchung nicht mehr zu sehen gewesen wäre.
|
||
Deshalb ist der Anteil auch nicht länger auf 1 gedeckelt.
|
||
- Im Abgleichdialog zeigt der Vorschlag die errechneten Zeilen mit ihren Beträgen und schickt
|
||
sie mit. Zuvor gingen sie beim Knopf *Als neue Buchung* verloren: Das Formular kannte nur
|
||
Kategorie und Klasse, sodass von Hand übernommene Umsätze ungeteilt im Kontoblatt landeten.
|
||
Der automatische Lauf war davon nie betroffen.
|
||
|
||
## Kurse von Yahoo Finanzen (13.08.2026)
|
||
|
||
Dritter Anbieter im Kursabruf: **Yahoo Finanzen**, ohne Schlüssel und ohne Tageskontingent.
|
||
Eine offene Schnittstelle gibt es dort nicht mehr – `query1.finance.yahoo.com` antwortet
|
||
ohne Sitzung mit HTTP 429, auch mit Cookie und Crumb –, gelesen wird deshalb die Kursseite
|
||
selbst.
|
||
|
||
- Auswahl *Yahoo Finanzen* im Einstellungsdialog unter *Wertpapiere*; das Feld für den
|
||
API-Schlüssel bleibt dort leer.
|
||
- Das **Kürzel muss in den Stammdaten stehen** und den Handelsplatz nennen (`ALV.DE`,
|
||
`PTX.F`). Yahoo hat keine ISIN-Suche mehr, `/lookup` und `/search` antworten mit 404.
|
||
- `Kursabruf::yahooKurs()` liest beide Ausprägungen der Seite (`<fin-streamer>` und den
|
||
angezeigten Kurs in deutscher Schreibweise) und prüft am Seitentitel, dass die Seite zum
|
||
Kürzel gehört – ein Tippfehler bringt so keinen fremden Kurs in den Bestand.
|
||
- Eine Sekunde Abstand zwischen zwei Seiten; auf schnelle Serienabrufe antwortet Yahoo mit
|
||
HTTP 503. Weil dieser 503 vorübergehend ist und Kürzel trifft, die im Browser zur selben
|
||
Zeit funktionieren, wird bis zu dreimal angefragt. Der angezeigte Kursstand geht ins
|
||
Debug-Log, weil Yahoo gelegentlich eine ältere Fassung der Seite ausliefert.
|
||
|
||
Der Weg über fremdes Markup ist der empfindlichste der drei Anbieter: Baut Yahoo die Seite
|
||
um, meldet der Abruf einen Fehler, statt einen falschen Kurs zu speichern.
|
||
|
||
## Kursabruf: Financial Modeling Prep antwortete mit HTTP 403 (13.08.2026)
|
||
|
||
Financial Modeling Prep hat die alten `/api/v3/`-Adressen zum 31. August 2025 abgeschaltet;
|
||
sie beantworten seither jede Anfrage mit HTTP 403. Der Kursabruf sprach genau diese Adressen
|
||
an und meldete daraufhin *„Der API-Schlüssel wurde abgelehnt“* – eine falsche Fährte, denn
|
||
der Schlüssel war in Ordnung.
|
||
|
||
- `Kursabruf` nutzt für Financial Modeling Prep jetzt `/stable/quote` und `/stable/search-isin`.
|
||
- Fehlermeldungen nennen den Klartext des Dienstes. HTTP 403 heißt nicht mehr pauschal
|
||
„Schlüssel abgelehnt“, und HTTP 402 („im gebuchten Tarif nicht enthalten“) wird als
|
||
eigener Fall gemeldet statt als nacktes „antwortet mit HTTP 402“.
|
||
|
||
**Hinweis zum kostenlosen Tarif von Financial Modeling Prep:** Er umfasst weder die
|
||
ISIN-Suche noch deutsche Börsenplätze (`….DE`) – beides beantwortet der Dienst mit HTTP 402.
|
||
Für ein Depot mit deutschen ISINs ist dort auch nach dieser Korrektur nichts zu holen;
|
||
dafür ist Leeway.tech vorgesehen (Einstellungen → Wertpapiere).
|
||
|
||
## Detailseite eines Wertpapiers (13.08.2026)
|
||
|
||
Bisher endete der Weg zu einem Wertpapier in einer Tabellenzeile: Stückzahl, Kurs, Wert –
|
||
und wer mehr wissen wollte, musste sich die Angaben aus Stammdaten, Depotbuchungen und
|
||
Auswertungen zusammensuchen. Der Name in der Übersicht war kein Link.
|
||
|
||
**Die neue Seite** (`/wertpapiere/papier/{id}`, `Views/wertpapiere/papier.php`,
|
||
`WertpapiereController::papier()`)
|
||
- Erreichbar über den Wertpapiernamen in der Bestandsübersicht und in der Liste der
|
||
Depotbuchungen sowie über *Details* in den Stammdaten.
|
||
- Oben die Kennzahlen über die ganze Historie: Bestand, Wert, Einstand, nicht realisierter
|
||
und realisierter Gewinn, Erträge.
|
||
- Darunter zwei Kacheln: **Stammdaten** (ISIN, WKN, Art, Währung, Kürzel, Status, erste
|
||
und letzte Buchung) und **Kurs** (letzter Kurs mit Datum und Quelle, Hoch und Tief der
|
||
letzten zwölf Monate, Einstand je Stück).
|
||
- **Bestand je Depot** – ein Papier kann in mehreren Depots liegen – und die
|
||
**Wertentwicklung** als dasselbe Diagramm wie in der Depotübersicht, auf dieses Papier
|
||
gerechnet.
|
||
- Unten die **Buchungen** eines Zeitraums, voreingestellt das laufende Jahr, mit denselben
|
||
Vorgaben wie die Auswertungen. Dazu eine Zeile mit Zu- und Abgängen und den Erträgen des
|
||
Zeitraums. Die Kennzahlen oben bleiben davon unberührt.
|
||
|
||
**Unter der Haube**
|
||
- `Depot::bestand()` und `Depot::entwicklung()` nehmen eine Wertpapiernummer entgegen. Die
|
||
Detailseite rechnet damit nicht selbst, sondern bekommt dieselben Zahlen wie die
|
||
Übersicht – beide können nicht auseinanderlaufen.
|
||
- Neu: `Wertpapier::kurseckwerte()` (Hoch, Tief, Zahl der Kurse eines Zeitraums) und
|
||
`Depotbuchung::datumsgrenzen()` (erste und letzte Buchung, wahlweise je Papier).
|
||
- `View::stueck()` formatiert Stückzahlen an einer Stelle statt in vier Ansichten.
|
||
|
||
## Wertpapiergeschäft aus der Kontobuchung (13.08.2026)
|
||
|
||
Bisher konnte eine Depotbuchung ihre Geldbuchung nur selbst anlegen. Der Weg, den der
|
||
Alltag nimmt, fehlte: Der Bankabruf bringt die Wertpapierabrechnung als Umsatz auf dem
|
||
Girokonto, und erst dort entscheidet sich, dass es ein Kauf war. Wer die Buchung dann
|
||
einem Depot zuordnet, buchte eine Umbuchung – ohne Stückzahl, ohne Kurs, ohne Einstand.
|
||
|
||
**Das Depot in der Zuordnungsliste** (`Views/buchungen/zuordnungsliste.php`,
|
||
`kontoblatt.js`)
|
||
- Unter *Kategorie / Gegenkonto* steht eine dritte Gruppe: *Wertpapiergeschäft im Depot*.
|
||
Die Auswahl speichert nicht sofort, sondern öffnet die Wertpapiermaske.
|
||
- Die schlichte Umbuchung auf ein Depot bleibt daneben stehen – auf ein Depot lässt sich
|
||
auch Geld überweisen, ohne dass etwas gekauft wird.
|
||
- Ohne JavaScript führt derselbe Weg auf `/buchungen/{id}/wertpapier` mit denselben
|
||
Feldern; auch die Buchungsmaske leitet dorthin.
|
||
|
||
**Die beiden Dialoge** (`Views/buchungen/wertpapierfelder.php`, `wertpapierkosten.php`,
|
||
`wertpapierbuchung.js`) – zugeschnitten auf den *Wertpapier kaufen*-Dialog des
|
||
FinanzManagers:
|
||
- Vorne Vorgangsart, Datum, Wertpapier, Stück und Kurs; dahinter, hinter dem Knopf
|
||
*Kosten …*, die Nebenkosten nach Arten: sechs Gebühren (Bankprovision, Maklercourtage,
|
||
Börsenplatzentgelt, Spesen, Spesen Ausland, Sonstige Kosten) und sechs Steuern
|
||
(Zinsabschlagsteuer, Kapitalertragsteuer, Solidaritätszuschlag, Ausländische
|
||
Quellensteuer, Abgeltungsteuer, Kirchensteuer).
|
||
- ISIN und WKN werden aus dem Verwendungszweck gelesen (`Wertpapier::ausText()`); ein
|
||
bekanntes Papier ist damit vorausgewählt, ein unbekanntes vorausgefüllt.
|
||
- Ein Abfluss vom Konto kann nur ein Kauf sein, ein Zufluss ein Verkauf oder eine
|
||
Dividende – was nicht passt, steht gar nicht zur Wahl.
|
||
|
||
**Was gebucht wird** (`Services\Wertpapiergeschaeft`, `Services\Wertpapierkosten`)
|
||
- Der Buchungsbetrag ist die feste Größe. Die Nebenkosten werden aus ihm
|
||
herausgerechnet; was übrig bleibt, ist der Kurswert und damit der **Einstandspreis**.
|
||
Ein Kauf über 255,00 € mit 5,00 € Kosten ergibt 250,00 € Einstand, ein Verkauf mit
|
||
3.103,17 € Gutschrift und 84,83 € Kosten einen Bruttoerlös von 3.188,00 €.
|
||
- Im Kontoblatt entsteht eine **Splitbuchung**: je belegter Kostenart eine Zeile in ihrer
|
||
Kategorie, der Rest als Umbuchung ins Depot. Die Zeilen ergeben genau den
|
||
Buchungsbetrag. Ohne Nebenkosten bleibt es bei der schlichten Umbuchung.
|
||
- Die Depotbuchung hängt sich an die vorhandene Buchung, statt eine zweite anzulegen
|
||
(neue Spalte `depot_buchungen.buchung_erzeugt`, `migrations/012_wertpapierkosten.sql`).
|
||
Wird sie gelöscht, bleibt die Buchung der Bank stehen und ist nur wieder unzugeordnet.
|
||
|
||
**Kategorien der Kostenarten** (`Views/einstellungen/index.php`, Bereich *Wertpapiere*)
|
||
- Je Kostenart einmalig eine Kategorie hinterlegen; der Kostendialog fragt dann nur noch
|
||
Beträge ab. Für *Sonstige Kosten* ist die Einstellung nur die Vorgabe – sie lässt sich
|
||
im Dialog überschreiben, wie im FinanzManager.
|
||
- Arten ohne Kategorie werden zu Splitzeilen ohne Zuordnung, nicht zu einem Fehler.
|
||
|
||
## Stichtag für den Umsatzabruf (13.08.2026)
|
||
|
||
Der Abrufzeitraum kannte bisher nur Spannen: seit dem letzten Abruf, 7, 30 oder 365 Tage.
|
||
Wer einen Bestand ab einem festen Datum aufbauen wollte – etwa ab dem Wechsel vom Lexware
|
||
FinanzManager –, hatte dafür keine Angabe. Der Stichtag steht jetzt an zwei Stellen.
|
||
|
||
**Vorgabe der Bankverbindung** (`Models\Bankzugang`, `Views/banking/formular.php`,
|
||
`migrations/011_abruf_stichtag.sql`)
|
||
- Neuer Abrufzeitraum *Ab Stichtag* und das dazugehörige Datumsfeld im Zugangsformular
|
||
(neue Spalte `bank_zugaenge.abruf_stichtag`). Gewählt, wird jeder Abruf ab diesem Tag
|
||
geholt.
|
||
- *Ab Stichtag* ohne Datum wird abgewiesen – eine Vorgabe, die nichts vorgibt, führte beim
|
||
Abruf nur zu einem anderen Zeitraum als gedacht. Ein unlesbares Datum ebenso.
|
||
- Die Übersicht der Bankverbindungen zeigt den Stichtag neben dem Zeitraum.
|
||
|
||
**Stichtag für einen einzelnen Abruf** (`Services\FinTsAbruf`, `BankingController`,
|
||
`Views/banking/index.php`)
|
||
- Neben *Umsätze abrufen* steht ein Datumsfeld. Ausgefüllt geht es dem eingestellten
|
||
Abrufzeitraum vor und gilt **nur für diesen Lauf**: Wer einmalig weiter zurückgreifen
|
||
will, muss die Einstellung des Zugangs nicht ändern und hinterher zurückstellen.
|
||
- Der Stichtag liegt im Abrufzustand, nicht in der Datenbank – nach einer TAN-Runde läuft
|
||
der Abruf im nächsten Request weiter und holt dort denselben Zeitraum wie beim Start.
|
||
- Ein Stichtag in der Zukunft wird auf heute gesetzt, statt den Abruf leer ausgehen zu
|
||
lassen; das Protokoll vermerkt es.
|
||
|
||
Angenommen werden das Format des Datumsfelds (`2026-01-01`) und die deutsche Schreibweise
|
||
(`01.01.2026`); alles andere gilt als Fehleingabe. Wie weit die Bank zurückreicht,
|
||
entscheidet sie selbst – 90 Tage sind üblich, mehr liefert nicht jede.
|
||
|
||
## Termine und Verträge wandern unter die Verwaltung (13.08.2026)
|
||
|
||
Die Seitenleiste war lang geworden. *Termine* und *Verträge* sind kein täglicher Einstieg –
|
||
die Reihen laufen von selbst, Verträge sind Stammdaten – und stehen deshalb nicht mehr im
|
||
Hauptmenü, sondern als letzte Kacheln in der Verwaltung.
|
||
|
||
**Navigation** (`Views/layout/kopf.php`)
|
||
- Die Menüpunkte *Termine* und *Verträge* entfallen.
|
||
- Ein Menüeintrag kann jetzt weitere Pfade mitführen (`auch`), unter denen er als aktiv
|
||
gilt. *Verwaltung* leuchtet damit auch auf `/termine`, `/termine/kalender`,
|
||
`/termine/erinnerungen` und unterhalb von `/vertraege` – sonst stünde die Seitenleiste
|
||
dort ohne Markierung.
|
||
|
||
**Verwaltung** (`Views/verwaltung/index.php`, `VerwaltungController`)
|
||
- Neue Kachel *Termine* mit der Zahl der aktiven Reihen, den offenen Erinnerungen und
|
||
Wegen zu regelmäßigen Buchungen, Finanzkalender und Erinnerungen.
|
||
- Neue letzte Kachel *Verträge* mit der Zahl der laufenden Verträge, den in den nächsten
|
||
90 Tagen ablaufenden Kündigungsfristen und Wegen zu Verträgen, Adressen, Sparzielen und
|
||
Krediten.
|
||
- Beide Kacheln führen mehrere Wege und nutzen deshalb kleine Knöpfe und einen knappen
|
||
Beitext.
|
||
- Alle übrigen Kacheln ziehen nach: *Kategorien*, *Klassen*, *Konten*, *QIF-Import* und
|
||
*Online-Banking* tragen jetzt dieselben kleinen Knöpfe. Vorher standen zwei Größen
|
||
nebeneinander im selben Raster.
|
||
|
||
**Knopfleisten stehen am Kachelfuß** (`static/app.css`)
|
||
- Das Raster der Verwaltung heißt `kachelraster fussbuendig`: die Kacheln werden zu
|
||
Flexspalten, der freie Platz sammelt sich über der Knopfleiste. Die Knöpfe stehen damit
|
||
in jeder Kachel einer Zeile auf gleicher Höhe unten, statt der jeweiligen Textlänge zu
|
||
folgen. Der Abstand zum Text steckt im Padding – `margin-top: auto` schluckt den
|
||
Außenabstand und ließe die Knöpfe in der höchsten Kachel sonst am Text kleben.
|
||
|
||
**Knopfleisten brechen um** (`static/app.css`)
|
||
- `.knopfleiste` bekommt `flex-wrap: wrap`. Bisher ragten Leisten mit mehreren Knöpfen bei
|
||
schmalen Kacheln über den rechten Rand hinaus, statt in die nächste Zeile zu rutschen.
|
||
Das gilt für alle Knopfleisten, nicht nur die der Verwaltung.
|
||
|
||
Alle Adressen unter `/termine` und `/vertraege` bleiben unverändert erreichbar, ebenso die
|
||
Reiter innerhalb der Seiten und die Verlinkungen aus Cockpit und Buchungsvorlagen.
|
||
|
||
**Test:** `tests/24_NavigationTest.php`
|
||
|
||
## QIF-Import: nur ausgewählte Konten übernehmen (12.08.2026)
|
||
|
||
Der QIF-Import kannte bisher nur den Zeitraum als Filter – ein Export ließ sich nur ganz
|
||
einlesen. Jetzt lassen sich einzelne Konten wählen, etwa um ein Konto nachzuziehen, ohne
|
||
die übrigen anzufassen.
|
||
|
||
**Kontoauswahl im Importeur** (`QifBuchungsImport`)
|
||
- Neue Option `konten`. Maßgeblich ist nicht allein der `!Account`-Abschnitt, sondern jedes
|
||
Konto, das der Eintrag berührt: das eigene, die Umbuchungsziele aus `L` und den
|
||
Splitzeilen und beim Wertpapiergeschäft das Verrechnungskonto. Eine Umbuchung steht nur
|
||
einmal in der Datei – sonst fehlte dem gewählten Konto die Gegenseite.
|
||
- Die Anfangssalden werden weiter über die ganze Datei gerechnet, aber nur für die
|
||
gewählten Konten geschrieben. Ein Teilimport überschreibt so keine fremden Salden.
|
||
- Die Fingerabdrücke der Buchungen entstehen vorab in einem eigenen Durchgang. Der
|
||
laufende Zähler für gleiche Fingerabdrücke hing sonst an der Auswahl, und dieselbe
|
||
Buchung wäre bei einem zweiten Lauf mit anderer Auswahl doppelt angelegt worden.
|
||
- `kontenDerDatei()` nennt die Konten eines Exports samt der Zahl der Einträge, die sie
|
||
berühren – auch die, die nur Umbuchungsziel sind und keine eigene Zeile führen.
|
||
|
||
**Dialog** (`Views/verwaltung/import.php`, `ImportController`)
|
||
- Nach dem Trockenlauf steht unter *Konten* die Ankreuzliste der Konten dieser Datei; vor
|
||
dem ersten Lesen gibt es nichts zu wählen. Von dort geht es weiter zum erneuten
|
||
Trockenlauf mit der Auswahl oder zum Übernehmen.
|
||
- Die Vorschau zählt mit, wie viele Buchungen zu keinem der gewählten Konten gehören.
|
||
- Was im Formular ankommt, wird gegen die Konten der Datei geprüft.
|
||
|
||
**Kommandozeile** (`scripts/qif-import.php`)
|
||
- `--konto=NAME` (mehrfach angebbar) grenzt den Import ein, `--konten` nennt die Konten der
|
||
Datei und beendet sich. Ein Name, den die Datei nicht führt, bricht ab – sonst liefe der
|
||
Import durch und legte nichts an.
|
||
|
||
## Umbuchungen: Gegenkonto sichtbar, Zuordnung im Kontoblatt (12.08.2026)
|
||
|
||
Eine Umbuchung war auf dem Gegenkonto nicht mehr zuzuordnen: Die Zeile trug die Marke
|
||
*Umbuchung*, die Kategoriespalte daneben blieb leer. Zu sehen etwa bei den Buchungen
|
||
zwischen DKB Girokonto und Paypal – die Daten waren richtig, die Anzeige verschwieg sie.
|
||
|
||
**Beide Sichten nennen das Gegenüber** (`Views/buchungen/kontoblatt.php`)
|
||
- In der Kategoriespalte steht jetzt auch in der Gegensicht das Konto in eckigen Klammern,
|
||
von dem die Umbuchung kommt. Die Abfrage lieferte das Konto längst mit; die Ansicht hat
|
||
es nur nicht gezeigt.
|
||
|
||
**Kategorie und Gegenkonto in einem Feld** (`Views/buchungen/zuordnungsliste.php`,
|
||
`BuchungenController`)
|
||
- Unter den Kategorien stehen die eigenen Konten in eckigen Klammern – wie im
|
||
FinanzManager, dessen Kategoriefeld `[Konto]` annimmt. Eines davon macht die Buchung zur
|
||
Umbuchung. Die Auswahl *Ausgabe / Einnahme* bestimmt seither nur noch die Richtung; das
|
||
eigene Konto fehlt in der Liste.
|
||
- Dieselbe Liste steht in den Splitzeilen. Eine Splitzeile mit Gegenkonto ließ sich bisher
|
||
nur im Import anlegen und ging beim Bearbeiten der Buchung stillschweigend verloren.
|
||
|
||
**Schnellzuordnung im Kontoblatt** (`public/static/kontoblatt.js`, `Buchung::zuordnen()`)
|
||
- Ein Klick auf die Kategoriespalte öffnet die Liste in der Zelle: leerer Eintrag,
|
||
*Splitbuchung*, Kategorien, Konten. Die Auswahl speichert sofort
|
||
(`POST /buchungen/{id}/zuordnung`) und kommt in dieselbe Ansicht zurück – Filter und
|
||
Seite hängen am Ziel.
|
||
- Die Liste gibt es einmal und wandert in die angeklickte Zelle. Eine Liste je Zeile wären
|
||
bei hundert Zeilen ebenso viele vollständige Kategoriebäume im HTML.
|
||
- Feld und Liste sind gleich breit und gleich hoch, die Zelle zeigt nur, was auch in der
|
||
Liste steht – sonst rückte beim Anklicken die halbe Zeile.
|
||
- Ohne JavaScript führt derselbe Klick in die Buchungsmaske: Die Zelle ist ein Link.
|
||
- Auch die Gegenseite einer fremden Umbuchung ist anklickbar; sie führt zu ihrer Buchung
|
||
auf dem anderen Konto, wo sie geändert wird.
|
||
|
||
**Splitdialog** (`Views/buchungen/kontoblatt.php`, `Buchung::splitsErsetzen()`)
|
||
- Der Eintrag *Splitbuchung* öffnet ein modales Fenster mit Zeilen für Kategorie oder
|
||
Gegenkonto, Klasse, Notiz und Betrag; der Restbetrag wird mitgerechnet, weitere Zeilen
|
||
kommen auf Knopfdruck. Eine bestehende Aufteilung steht schon drin.
|
||
- Ein Klick auf eine bereits gesplittete Buchung öffnet direkt den Dialog statt der
|
||
Auswahlliste: Für sie hatte die Liste nichts anzubieten außer dem Weg dorthin.
|
||
- Rechts unten im Dialog steht *Splitbuchung löschen*. Der Knopf schickt dasselbe
|
||
Formular mit `aktion=loeschen`, verwirft alle Zeilen samt ihren Zuordnungen – auch die
|
||
Gegenseite einer Splitzeile mit Gegenkonto – und die Buchung steht danach unzugeordnet
|
||
im Kontoblatt. Er erscheint nur dort, wo es etwas aufzuheben gibt, und steht abseits
|
||
von *Speichern* und *Abbrechen*.
|
||
- Bleiben alle Zeilen leer, wird die Splitbuchung ebenso aufgehoben.
|
||
- Beide Wege achten den Monatsabschluss und lassen den Rest der Buchung unberührt.
|
||
|
||
`public/static/betrag.js` bündelt das Lesen deutscher Beträge und die Restbetragsrechnung
|
||
für Maske und Kontoblatt. `tests/23_ZuordnungTest.php` prüft Zuordnung, Splitzeilen und
|
||
Gegensicht; `tests/e2e/specs/buchungen.spec.js` die drei Wege im Browser.
|
||
|
||
## Wertpapiere: Depotbuchungen sammelweise löschen (12.08.2026)
|
||
|
||
Die Liste der Depotbuchungen räumt jetzt so auf wie das Kontoblatt – nach einem Import,
|
||
der schiefging, war das Löschen Zeile für Zeile mühsam.
|
||
|
||
**Auswahl löschen** (`Views/wertpapiere/buchungen.php`, `WertpapiereController`,
|
||
`public/index.php`)
|
||
- Ankreuzfeld je Zeile, das Feld in der Titelzeile wählt alle angezeigten Buchungen. Der
|
||
Knopf steht oben in der Aktionsleiste, nennt die Anzahl und bleibt ohne Auswahl
|
||
gesperrt; die Rückfrage nennt sie ebenfalls. Ohne JavaScript funktioniert alles weiter:
|
||
Dann ist der Knopf offen und `POST /wertpapiere/buchungen/loeschen` weist eine leere
|
||
Auswahl ab.
|
||
- Mit jeder Depotbuchung geht ihre Geldbuchung – wie beim Löschen einer einzelnen.
|
||
- **Am Bestand ist nichts nachzuziehen.** Er wird bei jedem Aufruf aus den Depotbuchungen
|
||
gerechnet und gibt die Löschung von selbst wieder her, ebenso realisierter Gewinn,
|
||
Veräußerungen und Erträge.
|
||
- Der eingestellte Filter (Depot, Wertpapier, Vorgang) hängt am Ziel des Formulars,
|
||
deshalb steht danach wieder dieselbe Ansicht da. Das gilt jetzt auch für das *Löschen*
|
||
je Zeile, das bisher zur ungefilterten Liste zurücksprang.
|
||
|
||
`tests/22_DepotbuchungenLoeschenTest.php` prüft Geldseite, Bestand und Auswertungen nach
|
||
dem Löschen; `tests/e2e/specs/module.spec.js` den Ablauf im Browser.
|
||
|
||
## Wertpapiere: Depotbestand aus einer CSV-Datei übernehmen (12.08.2026)
|
||
|
||
Ein Depot, das nie im FinanzManager geführt wurde, lässt sich jetzt in einem Zug
|
||
einlesen – aus der Bestandsliste, die die Bank zum Herunterladen anbietet.
|
||
|
||
**Neuer Knopf *Import*** (`Views/wertpapiere/index.php`, `Views/wertpapiere/import.php`)
|
||
- Steht auf der Bestandsseite neben *Depotbuchung erfassen*. Der Ablauf ist zweistufig:
|
||
hochladen, Vorschau prüfen, übernehmen. Die Vorschau zeigt je Position Stück, beide
|
||
Kurse, Einstand, Wert und ob sie neu ist oder schon einmal importiert wurde.
|
||
- Vor der Übernahme lassen sich Depot, Buchungsdatum und Verrechnungskonto noch ändern.
|
||
|
||
**Der Import** (`Services\DepotbestandImport`, `WertpapiereController`)
|
||
- Erwartet die Spalten Wertpapierbezeichnung, WKN, ISIN, Einstiegskurs, Bewertungskurs,
|
||
Stückzahl und Assetklasse; Trennzeichen und Zeichensatz erkennt er selbst. Beträge wie
|
||
`"1.471,88 €"` und Bruchstückzahlen wie `55,6863` werden gelesen, wie sie kommen.
|
||
- **Die Vergangenheit fehlt im Export.** Er beschreibt nur einen Stichtag. Je Position
|
||
entsteht deshalb *eine* Depotbuchung: ein Kauf zum Stichtag über die volle Stückzahl zum
|
||
Einstiegskurs. Der Einstand stimmt damit auf den Cent; frühere Käufe, Verkäufe und
|
||
Dividenden bleiben leer.
|
||
- **Der Bewertungskurs wird als Kurs des Stichtags hinterlegt.** Ohne ihn stünde der
|
||
frisch übernommene Bestand sofort wieder ohne Bewertung da. Am echten Export
|
||
nachgerechnet: 33.176,12 € Einstand, 47.120,25 € Wert – die Bank weist zehn Cent
|
||
weniger Gewinn aus, was an ihren auf zwei Stellen gerundeten Kursen liegt.
|
||
- **Kein Geldfluss.** Ein Verrechnungskonto ist wählbar, bleibt aber vorbelegt leer: Das
|
||
Geld ist vor Jahren geflossen, eine Belastung zum heutigen Tag würde das Girokonto
|
||
verfälschen.
|
||
- Ohne ausdrückliche Wahl findet der Import das Depot über die **Kontonummer**, die zur
|
||
Depotnummer der Datei passt. Ein `import_hash` je Position verhindert, dass dieselbe
|
||
Datei zweimal denselben Bestand anlegt.
|
||
- Assetklassen werden übersetzt: *Aktien* → Aktie, *ETFs* und *Fonds* → Fonds,
|
||
*Anleihen* → Anleihe, *Zertifikate* → Zertifikat.
|
||
|
||
**Neue Herkunft „CSV-Import“** (`Models\Buchung`)
|
||
- `Buchung::QUELLEN` kennt jetzt `csv`; Depot- und Geldbuchungen aus dem Bestandsimport
|
||
sind damit als solche erkennbar und fallen nicht mehr auf „manuell erfasst“ zurück.
|
||
|
||
`tests/21_DepotbestandImportTest.php` prüft das an einem Ausschnitt des echten Exports.
|
||
|
||
## Online-Banking: Umsätze, die niemand mehr zu sehen bekam (12.08.2026)
|
||
|
||
Drei Fehler, die zusammen dazu führten, dass abgerufene Umsätze spurlos verschwanden:
|
||
Der Abruf meldete sie als schon vorhanden, im Kontoblatt stand nichts, und in der
|
||
Vorschlagsliste tauchten sie ebenfalls nicht auf.
|
||
|
||
**Eine gelöschte Buchung gibt ihren Umsatz wieder frei** (`Models\Buchung`, `Models\Bankumsatz`)
|
||
- Der Fremdschlüssel setzte beim Löschen zwar `bank_umsaetze.buchung_id` auf NULL, ließ den
|
||
Status aber auf *übernommen* stehen. Der Umsatz war damit unsichtbar: keine Buchung, kein
|
||
Eintrag in der Vorschlagsliste, und beim nächsten Abruf über den Fingerabdruck als schon
|
||
vorhanden übersprungen. `Buchung::loeschen()` setzt ihn jetzt vorher auf *offen* zurück.
|
||
- `Bankumsatz::verwaisteOeffnen()` fängt die Wege ab, die daran vorbeiführen: Wird ein Konto
|
||
gelöscht, räumt der Fremdschlüssel seine Buchungen per CASCADE ab – und Kaskaden lösen in
|
||
MariaDB keine Trigger aus. Der Aufruf steht vor dem Abruf und vor der Vorschlagsliste,
|
||
damit sich dieser Zustand von selbst wieder auflöst.
|
||
|
||
**Ein Bankkonto entsteht nicht mehr doppelt** (`Models\Bankzugang`)
|
||
- Gesucht wird über die **Kontonummer**, nicht über IBAN und Kontonummer zusammen. Manche
|
||
Banken liefern die IBAN nicht bei jedem Dialog mit – die Comdirect nennt sie beim
|
||
Kontenabruf, beim Umsatzabruf aber nicht. So entstand beim zweiten Abruf ein zweiter
|
||
Eintrag fürs selbe Konto: einmal mit IBAN, einmal ohne, mit eigener Zuordnung und eigenem
|
||
Fingerabdruck. Jeder Umsatz stand daraufhin doppelt da.
|
||
- Liefert die Bank die IBAN erst später, wird sie am vorhandenen Eintrag nachgetragen.
|
||
|
||
**Der Fingerabdruck hängt am Bankkonto, nicht an der IBAN** (`Models\Bankumsatz`)
|
||
- `Bankumsatz::externId()` nimmt die `bank_konto_id` statt IBAN oder Kontonummer. Welche
|
||
Kennung eine Bank liefert, wechselt von Dialog zu Dialog; mit ihr kippte der Fingerabdruck,
|
||
und derselbe Umsatz kam ein zweites Mal herein. Die Kennung der Zeile in `bank_konten`
|
||
steht dagegen fest, solange es den Bankzugang gibt.
|
||
|
||
## Kontoblatt: Buchungen sammelweise löschen, Meldungen ohne Sprung (12.08.2026)
|
||
|
||
**Mehrfachauswahl im Kontoblatt** (`Views/buchungen/kontoblatt.php`, `BuchungenController`)
|
||
- Links vor dem Datum steht je Buchung ein Ankreuzfeld, in der Titelzeile eines für alle
|
||
gerade angezeigten Buchungen. Es zeigt auch den Zwischenstand an, wenn nur ein Teil
|
||
ausgewählt ist.
|
||
- Neben *Kontenübersicht* löscht **Auswahl löschen** die angekreuzten Buchungen nach einer
|
||
Rückfrage, die die Anzahl nennt. Ohne Auswahl bleibt der Knopf gesperrt; ohne JavaScript
|
||
weist der Controller eine leere Auswahl ab.
|
||
- Die Gegenseite einer fremden Umbuchung bekommt kein Ankreuzfeld – sie gehört dem
|
||
Ursprungskonto und wird auch weiterhin nur dort gelöscht.
|
||
- Nach dem Löschen erscheint wieder dieselbe Ansicht: Filter und Seite hängen am Ziel des
|
||
Formulars und werden aus bekannten Feldern neu zusammengesetzt, nicht übernommen.
|
||
|
||
**Statusmeldungen liegen über der Ansicht** (`Views/layout/meldungen.php`)
|
||
- Die Meldungen schieben sich von ganz oben ins Bild, statt im Inhalt Platz zu belegen. Der
|
||
Inhalt bleibt beim Ein- und Ausblenden an Ort und Stelle – vorher rückte die halbe Seite
|
||
auf und beim Verschwinden wieder zurück.
|
||
- Eine Meldung ist so breit wie ihr Text und bricht erst bei 80 % Fensterbreite um.
|
||
- Erfolgsmeldungen gehen nach fünf Sekunden von selbst, alle Meldungen lassen sich
|
||
anklicken – eine stehenbleibende Fehlermeldung verdeckte sonst die Werkzeugleiste.
|
||
- Beide Rahmen (`kopf.php`, `schlicht.php`) binden dieselbe Teilansicht ein.
|
||
|
||
## Online-Banking: Kontenzuordnung als dritter Schritt, Umsätze buchen sich selbst (12.08.2026)
|
||
|
||
**Einrichtung in drei Schritten** (`Views/banking/konten.php`, Migration `010_kontenzuordnung.sql`)
|
||
- Nach Zugangsdaten (1) und TAN-Verfahren (2) folgt die **Kontenzuordnung** (3): Welches
|
||
Bankkonto gehört zu welchem eigenen Konto? Die Liste kommt frisch von der Bank – das
|
||
Übernehmen des Verfahrens startet die Abfrage gleich mit, samt TAN-Maske, wenn die Bank
|
||
eine verlangt.
|
||
- Fehlt ein passendes eigenes Konto, legt *neues Konto anlegen* es an: IBAN, Kontonummer
|
||
und Bank stehen schon fest, einzutragen bleiben Name, Kontoart und Anfangssaldo. Letzterer
|
||
deckt die Zeit ab, die der Abruf nicht mehr hergibt.
|
||
- Die Zuordnung ist von der Zugangsmaske dorthin umgezogen – dort stand sie in einer
|
||
Tabelle, die nur zuordnen, aber nichts anlegen konnte. Geblieben ist eine Übersicht mit
|
||
Verweis.
|
||
- `bank_umsaetze.bank_konto_id` merkt sich, von welchem Bankkonto ein Umsatz stammt. Wird
|
||
die Zuordnung erst nach einem Abruf getroffen, ziehen die **offenen** Umsätze mit;
|
||
übernommene bleiben, wo sie sind – ihre Buchung steht bereits in einem Kontoblatt.
|
||
- Der Abruf kennt dafür einen kurzen Modus (`FinTsAbruf::starten($id, 'konten')`), der nach
|
||
der Kontenabfrage anhält. Dieselbe Zustandsmaschine, dieselbe TAN-Behandlung.
|
||
- Am Ende der Einrichtung geht es zurück in die **Übersicht der Bankverbindungen** – dort
|
||
steht die neue Verbindung fertig zwischen den anderen. Die Zugangsmaske böte nur noch an,
|
||
was gerade durchlaufen wurde. Dasselbe gilt beim vorzeitigen Verlassen über *Später
|
||
einstellen* und *Später zuordnen*; einzeln aufgerufen bleibt die Zuordnung auf ihrer Seite.
|
||
|
||
**Abgerufene Umsätze buchen sich selbst** (`Services\Bankabgleich`)
|
||
- Neue Einstellung *Abgerufene Umsätze selbst buchen* (`banking_automatisch_buchen`,
|
||
Vorgabe an). Nach jedem Abruf – und nach dem Speichern der Kontenzuordnung – landen die
|
||
Umsätze im zugeordneten Konto: verbunden mit einer schon erfassten Buchung, wenn eine
|
||
passt, sonst als neue Buchung mit der Kategorie aus der Regel, sonst ohne.
|
||
- Der Lauf läuft **einmal am Ende**, nicht je Konto: So sieht er alle Umsätze und kann jeden
|
||
mit der Buchung verbinden, die zu ihm passt.
|
||
- Ein einzelner Umsatz bricht den Lauf nicht ab. Ein abgeschlossener Monat oder ein
|
||
doppelter Fingerabdruck betrifft nur ihn; er bleibt offen und steht weiter in der
|
||
Vorschlagsliste. Die Meldung nennt die ersten Fälle.
|
||
- Umsätze ohne zugeordnetes Konto bleiben liegen – ohne Konto gibt es kein Kontoblatt, in
|
||
das sie gehörten.
|
||
- *Alles übernehmen* in der Vorschlagsliste bleibt zurückhaltender und lässt liegen, was
|
||
keine Regel trifft: Wer den Dialog öffnet, will die Zuordnung selbst treffen. Beide Wege
|
||
teilen sich jetzt denselben Dienst.
|
||
|
||
**Kontenübersicht: fett heißt „hier liegt Arbeit“**
|
||
- Konten mit Buchungen aus dem laufenden Jahr ohne Kategorie stehen **fett**
|
||
(`Buchung::unkategorisierteJeKonto()`), mit der Zahl im Titel. Dieselbe Zählweise wie im
|
||
Cockpit: keine Kategorie, kein Gegenkonto, nicht gesplittet – eine Umbuchung oder eine
|
||
Splitbuchung ist zugeordnet.
|
||
- Das ist die Gegenprobe zum automatischen Buchen: Die Umsätze stehen sofort im Kontoblatt,
|
||
und die Übersicht zeigt, wo die Kategorie noch fehlt.
|
||
|
||
## Phasen 4 bis 7 – Cockpit, Online-Banking, Wertpapiere, Nebenmodule (12.08.2026)
|
||
|
||
Damit ist der geplante Funktionsumfang beisammen: Von der Kontenübersicht bis zur
|
||
Überweisung läuft alles in der Anwendung.
|
||
|
||
### Phase 4 abgeschlossen – die übrigen Grafiken und das Cockpit
|
||
|
||
**Konfigurierbare Kacheln** (`src/Services/Cockpit.php`)
|
||
- Fünf Kacheln: Vermögensentwicklung, Einnahmen und Ausgaben, Meine Ausgaben,
|
||
Kontostände, Offene Vorgänge. Die Auswahl steht im Einstellungsdialog unter
|
||
*Darstellung*; abgewählte Kacheln werden nicht nur ausgeblendet, sondern gar nicht
|
||
erst berechnet.
|
||
- Die Reihenfolge im Raster kommt aus dem Katalog, nicht aus der Reihenfolge der
|
||
Ankreuzfelder – letztere wäre für den Nutzer keine sinnvolle Sortierung.
|
||
|
||
**Einnahmen und Ausgaben je Monat**
|
||
- Balkenpaare über zwölf Monate, darüber der Saldo als Linie.
|
||
- Die Zahlen kommen aus dem Summenbericht, nicht aus einer eigenen Abfrage: So zählt die
|
||
Kachel dieselben Bewegungen wie der Bericht und beide laufen nie auseinander.
|
||
|
||
**Meine Ausgaben als Ring**
|
||
- `Auswertung::verteilung()` fasst auf der **obersten Kategorie** zusammen – „300 - Wohnen“
|
||
statt fünfzehn Unterkategorien. Höchstens acht Segmente, der Rest wird gesammelt.
|
||
- Negative Kategoriesalden bekommen kein Segment, zählen aber in der Summe mit; sonst
|
||
ergänzten sich die Anteile nicht mehr zu hundert Prozent.
|
||
- Sonderfall Vollkreis: Bei einer einzigen Kategorie fielen Anfangs- und Endpunkt des
|
||
Bogens zusammen und es entstünde keine Fläche – deshalb zwei Halbringe.
|
||
|
||
**Offene Vorgänge**
|
||
- Nicht kategorisierte Buchungen, nicht abgeglichene Buchungen, fälliger Monatsabschluss –
|
||
später ergänzt um offene Bankumsätze, fällige Termine und Erinnerungen.
|
||
|
||
### Phase 5 – Online-Banking über FinTS
|
||
|
||
**Die Bibliothek als eigener Fork**
|
||
- `nemiah/php-fints` liegt als Fork auf Codeberg (`fds2610-bot/phpFinTS`), eingebunden über
|
||
ein VCS-Repository, Version fest auf `4.1.1` gepinnt.
|
||
- **psr/log ist dort direkt eingebaut** (acht Interface-Dateien unter `psr-log/`, eigener
|
||
Autoload-Eintrag, kein `require` mehr). Der Finanzmanager hängt damit an keinem fremden
|
||
Update-Zyklus; wie der Eingriff beim Nachziehen zu wiederholen ist, steht im Fork.
|
||
|
||
**Zugangsdaten**
|
||
- Migration `006_banking.sql`: `bank_zugaenge`, `bank_konten`, `bank_umsaetze`,
|
||
`abgleich_regeln`, `bank_abrufe`.
|
||
- PIN und TAN-Medium AES-256-GCM-verschlüsselt (`Services\Crypto`, übernommen aus
|
||
`Steuerkram`). Der Schlüssel kommt aus `APP_KEY` oder aus `data/schluessel/app_key`, den
|
||
der Entrypoint beim ersten Start erzeugt – eigenes Bind-Mount, damit er einen Neubau des
|
||
Images überlebt.
|
||
- Ein leeres PIN-Feld heißt „unverändert“, nicht „PIN löschen“.
|
||
|
||
**Abruf über mehrere Requests**
|
||
- Fast jede Bank verlangt schon beim Login eine TAN. `Services\FinTsAbruf` sichert den
|
||
Dialog (`FinTs::persist()`) und die laufende Aktion in der Sitzung und macht nach der
|
||
Eingabe an derselben Stelle weiter. Ein wartender Prozess wäre mit php-fpm der falsche
|
||
Weg.
|
||
- Klassische Verfahren zeigen die photoTAN-Grafik, entkoppelte (pushTAN) fragen auf
|
||
Knopfdruck nach, statt zu warten.
|
||
- Umsätze, die vor einer TAN-Unterbrechung geschrieben wurden, bleiben erhalten.
|
||
|
||
**Wahl des TAN-Verfahrens** (`Services\FinTsVerfahren`, Migration `009_tan_verfahren.sql`)
|
||
- **Einrichtung in zwei Schritten**: Zugangsdaten erfassen und mit *Weiter* speichern, dann
|
||
das TAN-Verfahren. Vorher lässt sich die Bank nicht fragen – ohne gespeicherte
|
||
Zugangsdaten gibt es keine Verbindung.
|
||
- Der zweite Schritt holt die Liste beim Aufruf selbst (`getTanModes()`, ohne Anmeldung und
|
||
ohne TAN); ein Knopf davor wäre ein Schritt, dessen Ergebnis feststeht. Ein erneuter
|
||
Anlauf steht daneben. TAN-Medien werden nur für die Verfahren geholt, die eines brauchen.
|
||
- Das Ergebnis liegt in der Sitzung, auch ein gescheitertes – sonst liefe jeder
|
||
Seitenaufruf wieder in dieselbe Zeitüberschreitung. Die sind für die Abfrage kürzer
|
||
gesetzt als in der Bibliothek (8/20 statt 15/30 Sekunden), weil sie an einem
|
||
Seitenaufruf hängt.
|
||
- Scheitert die Abfrage, erklärt die Seite, wo der Fehler meist steckt, und verlinkt zurück
|
||
auf die Zugangsdaten; überspringen lässt sich der Schritt ebenfalls.
|
||
- Abgefragt statt eingetippt: Die Kennungen sind bankspezifisch, 913 heißt nicht überall
|
||
chipTAN QR. Der Name wird mitgespeichert (`tan_verfahren_name`), damit das Formular ihn
|
||
ohne Bankverbindung zeigen kann.
|
||
- Beim Übernehmen wird gegen die abgefragte Liste geprüft; ein Verfahren ohne Medienzwang
|
||
bekommt kein Medium angehängt, weil manche Banken den Dialog sonst ablehnen.
|
||
- Ohne Wahl entscheidet weiterhin das erste gemeldete Verfahren – diese Notwahl wird
|
||
festgehalten, eine ausdrückliche Wahl dagegen **nie überschrieben**.
|
||
- Behoben: Das Speichern des Zugangsformulars setzte Verfahren und TAN-Medium zurück, weil
|
||
beide Felder darin nicht vorkommen. Sie werden jetzt wie die PIN unter Vorbehalt gestellt.
|
||
- Abruf und Überweisung benutzen dieselbe Wahl; die doppelte Auswahllogik in
|
||
`FinTsUeberweisung` ist entfallen.
|
||
|
||
**Abgleich statt Direktbuchung**
|
||
- Abgerufene Umsätze landen in `bank_umsaetze` und werden nicht sofort gebucht. Die
|
||
Vorschlagsliste kennt drei Wege: mit einer erfassten Buchung **verbinden**, als neue
|
||
Buchung **übernehmen** oder **verwerfen**.
|
||
- Gleiches Konto, gleicher Betrag, Datum innerhalb der Toleranz (Vorgabe fünf Tage,
|
||
einstellbar) – so findet der Abgleich die schon erfasste Buchung. Eine bereits
|
||
verbundene Buchung wird nie ein zweites Mal vorgeschlagen.
|
||
- `extern_id` verhindert, dass derselbe Umsatz bei überlappenden Abrufzeiträumen zweimal
|
||
hereinkommt.
|
||
|
||
**Regeln**
|
||
- Muster auf Empfänger und/oder Verwendungszweck, vier Vergleichsarten samt regulärem
|
||
Ausdruck, Betragsgrenzen mit Vorzeichen, Wirkung auf Kategorie, Klasse, Gegenkonto oder
|
||
ein festes Split-Schema in Prozent.
|
||
- Die erste passende Regel gewinnt (nach Priorität) – kein Sammeln über mehrere Regeln,
|
||
sonst wäre nicht mehr nachvollziehbar, warum am Ende die eine Kategorie dasteht.
|
||
- Reguläre Ausdrücke werden beim Speichern geprüft; ein kaputtes Muster brächte sonst
|
||
jeden Abruf zum Stehen.
|
||
|
||
### Phase 6 – Wertpapiere und Depots
|
||
|
||
- Migration `007_wertpapiere.sql`: `wertpapiere`, `depot_buchungen`, `kurse`.
|
||
- **Zwei Seiten je Geschäft**: die Depotbuchung mit Stück und Kurs, die Geldbuchung auf dem
|
||
Verrechnungskonto. `betrag` ist der Bruttowert, Gebühren und Steuern stehen daneben –
|
||
nur so enthält der Einstand die Nebenkosten genau einmal.
|
||
- **Einstand nach gleitendem Durchschnitt**, realisierte Gewinne beim Verkauf. (Für die
|
||
Steuer gilt FIFO; maßgeblich ist dort die Abrechnung der Bank.)
|
||
- Ohne Kurs wird nicht bewertet: Die Position sagt „kein Kurs“, statt den Einstand als
|
||
Ersatzwert auszugeben.
|
||
- **Kursabruf** über Leeway.tech oder Financial Modeling Prep, Kürzel wird nach dem ersten
|
||
Fund gespeichert. Abgerufen wird nur für Papiere mit Bestand und nur einmal am Tag;
|
||
täglicher Lauf über `scripts/kursabruf.php`.
|
||
- **QIF-Import für `!Type:Invst`**: Wertpapier, Depotbuchung und Geldbuchung in einem Zug.
|
||
- Depots erscheinen in der Kontenübersicht mit ihrem bewerteten Bestand.
|
||
|
||
**Dabei behoben:** Der Import buchte den Geldfluss *jedes* Wertpapiergeschäfts als Abgang –
|
||
auch bei Verkäufen und Dividenden. Das QIF nennt im Feld `$` immer einen positiven Betrag,
|
||
die Richtung steckt allein im Vorgang. Zudem wurde das Verrechnungskonto nicht erkannt,
|
||
wenn dem Konto im `L`-Feld eine Kategorie vorangeht
|
||
(`LKursgewinne:Realisierte Gewinne|[Konto]`) – bei Verkäufen also immer.
|
||
|
||
### Phase 7 – Nebenmodule und Zahlungsverkehr
|
||
|
||
- Migration `008_nebenmodule.sql`: `buchungsvorlagen`, `regelmaessige_buchungen`,
|
||
`erinnerungen`, `adressen`, `vertraege`, `sparziele`, `kredite`, `zahlungsauftraege`.
|
||
- **Buchungsvorlagen** inklusive QIF-Import (`!Type:Memorized`); Splitzeilen werden bei
|
||
abweichendem Betrag im Verhältnis mitskaliert.
|
||
- **Regelmäßige Buchungen** mit Fälligkeitslauf: automatisch oder auf Bestätigung, jeder
|
||
zurückliegende Termin wird einzeln gebucht, abgeschlossene Monate bleiben unangetastet.
|
||
- **Finanzkalender** aus vier Quellen – gerechnet, nicht gespeichert.
|
||
- **Adressen** mit IBAN-Prüfung nach ISO 13616 (Länge je Land, Prüfziffer modulo 97).
|
||
- **Verträge** mit Kündigungsfrist und Jahresbetrag; ablaufende Fristen stehen oben und im
|
||
Kalender.
|
||
- **Sparziele**, deren Stand aus dem Kontosaldo kommt statt aus einem zweiten Zähler.
|
||
- **Kredite** mit gerechnetem Tilgungsplan (Annuität, Sondertilgung, Restschuld zum
|
||
Zinsbindungsende). Deckt die Rate die Zinsen nicht, gibt es einen Hinweis statt eines
|
||
Plans.
|
||
- **Zahlungsverkehr**: Überweisungen und Terminüberweisungen über FinTS. Die Buchung
|
||
entsteht erst, wenn die Bank den Auftrag angenommen hat; gesendete Aufträge bleiben als
|
||
Beleg unveränderlich stehen.
|
||
- **SEPA-XML selbst erzeugt** (`Services\SepaXml`, pain.001.001.03) statt einer weiteren
|
||
Abhängigkeit – samt Umschrift der Umlaute in den SEPA-Zeichenvorrat.
|
||
|
||
### Nebenbei
|
||
|
||
- `.dockerignore` ergänzt: Ohne sie schickte der Build das ganze Projektverzeichnis an den
|
||
Daemon, einschließlich `data/mariadb` – der Build brach mit „permission denied“ ab.
|
||
- `PlatzhalterController` entfernt: Alle Navigationspunkte haben jetzt ihr Modul.
|
||
- Neue Reiter im Einstellungsdialog: *Online-Banking* und *Wertpapiere*.
|
||
|
||
**Doku:** `docs/cockpit.md`, `docs/banking.md`, `docs/wertpapiere.md`,
|
||
`docs/nebenmodule.md`.
|
||
|
||
**Tests:** 15 bis 18 (Cockpit, Banking, Wertpapiere, Nebenmodule) – zusammen 888 Prüfungen.
|
||
|
||
## Phase 4 – Auswertungen mit Berichtsmodul (11.08.2026)
|
||
|
||
**Datenmodell**
|
||
- Migration `005_berichte.sql`: `berichte`, `berichtsgruppen`
|
||
- Ein Bericht ist Name, Typ und Filter – die Zahlen entstehen bei jedem Aufruf neu aus den
|
||
Buchungen. Gespeichert wird ausschließlich die Frage.
|
||
- Der Filter liegt als JSON in einer Spalte und läuft beim Lesen immer durch
|
||
`Auswertung::normalisiere()`: Unbekannte Felder fallen weg, unbekannte Werte auf die
|
||
Vorgabe zurück. Keine eigene Spalte je Filterfeld – das sind Anzeigeparameter ohne
|
||
Fremdschlüssel, und jede neue Filtermöglichkeit bräuchte sonst eine Migration.
|
||
- Keine neuen Einstellungen: Zeitraum, Zeilen-Summe und Einrückung gehören zum einzelnen
|
||
Bericht, nicht in den Einstellungsdialog.
|
||
|
||
**Fünf Berichtstypen** (`src/Services/Auswertung.php`)
|
||
- **Summenbericht**: Summen je Zeilen-Summe und Spalte, getrennt nach Einnahmen und
|
||
Ausgaben, mit Schlusszeile *Gesamt Einnahmen − Ausgaben*
|
||
- **Buchungsbericht**: die Einzelbuchungen, gruppiert mit Zwischensummen
|
||
- **Vergleichsbericht**: Zeitraum gegen denselben Zeitraum des Vorjahres, mit Differenz
|
||
und Veränderung in Prozent
|
||
- **Kontostandsbericht**: Salden je Konto zu den Enden der Spaltenzeiträume
|
||
- **Budgetbericht**: Budget gegen Ist je Kategorie, mit Saldo
|
||
|
||
**Was gezählt wird**
|
||
- Bewegungen statt Buchungen: nicht gesplittete Buchungen mit ihrem Betrag, gesplittete
|
||
über ihre Splitzeilen – sonst zählte eine Splitbuchung doppelt oder gar nicht.
|
||
- **Umbuchungen bleiben außen vor** (weder Einnahme noch Ausgabe, wie im Budgetplaner).
|
||
Nur der Buchungsbericht führt sie auf Wunsch mit; sie erscheinen dort mit dem Gegenkonto
|
||
in eckigen Klammern.
|
||
- Unkategorisierte Buchungen stehen als eigene Zeile *Nicht kategorisiert* am Ende ihres
|
||
Abschnitts, statt lautlos aus der Summe zu fallen.
|
||
- Einnahme oder Ausgabe entscheidet die **Art der Kategorie**, nicht das Vorzeichen: Eine
|
||
Erstattung ist eine negative Ausgabe und gehört unter die Ausgaben.
|
||
- Aggregiert wird in PHP. Die Datenbank liefert die Bewegungen, das Einsortieren in Zeilen
|
||
und Spalten passiert im Service – Baumsummen, Vorzeichenregeln und Spaltenraster wären
|
||
in SQL erheblich schwerer nachzulesen.
|
||
|
||
**Vorzeichen**
|
||
- Intern durchgehend Buchhaltung (Ausgaben negativ), in der Anzeige dreht der
|
||
Ausgabenabschnitt sein Vorzeichen – wie im FinanzManager steht dort 636,39 statt
|
||
−636,39. Die Schlusszeile rechnet mit den echten Werten.
|
||
- Buchungs- und Budgetbericht sowie der Drilldown behalten das Vorzeichen: Sie haben keine
|
||
Abschnitte, die es erklären.
|
||
|
||
**Zeitraum und Spalten** (`src/Services/Zeitraum.php`, reine Kalenderrechnung ohne Datenbank)
|
||
- Neun Vorgaben von *Aktueller Monat* bis *Gesamter Zeitraum*; sie rechnen sich bei jedem
|
||
Aufruf neu aus dem Tagesdatum aus. Ein Bericht mit *Letzter Monat* zeigt im August den
|
||
Juli, nicht für immer den Monat seiner Entstehung.
|
||
- Spalten-Aufteilung nach Monat, Quartal oder Jahr; die Randspalten werden auf den
|
||
Zeitraum gestutzt.
|
||
- *Gesamter Zeitraum* nimmt die Datumsgrenzen der Buchungen (`Buchung::datumsgrenzen()`)
|
||
und endet spätestens heute – ein Ende in der Zukunft brächte nur leere Spalten.
|
||
- Beim Vorjahresvergleich bleibt ein Monatsende ein Monatsende, sonst träfe der 28.02. auf
|
||
einen 29.02.
|
||
|
||
**Summenbericht als Baum**
|
||
- Jede Kategorie zeigt ihre eigenen Werte, Kategorien mit Nachfahren bekommen dahinter
|
||
*Gesamt <Name>* mit der Astsumme. Eine Elternkategorie ohne eigene Buchungen steht als
|
||
bloße Kopfzeile da – genau wie im Original.
|
||
- *ohne Einrückung* schaltet auf eine flache Liste mit vollem Pfad und ohne Astsummen.
|
||
- Ein Kategoriefilter schließt die Unterkategorien ein: Wer „400 - Leben“ wählt, meint den
|
||
ganzen Ast.
|
||
|
||
**Oberfläche**
|
||
- Übersicht mit Kachel *Standardvorlagen* und je einer Kachel pro Berichtsgruppe,
|
||
Menü *Letzte Berichte*, Verwaltung der Gruppen unter `/auswertungen/gruppen`.
|
||
- Filterleiste sendet per GET: Jeder Zwischenstand ist eine Adresse, die man sich merken,
|
||
weitergeben und mit *Speichern unter* festhalten kann.
|
||
- *Speichern unter* kann die Berichtsgruppe gleich mit anlegen – wer sie erst anderswo
|
||
erzeugen müsste, verlöre den eingestellten Filter.
|
||
- Eine gelöschte Gruppe nimmt ihre Berichte nicht mit: Sie rücken unter *Nicht gruppiert*.
|
||
- Die Aufklappmenüs sind `<details>`-Elemente, kein JavaScript.
|
||
- Der Platzhalter für `/auswertungen` entfällt aus dem `PlatzhalterController`.
|
||
|
||
**Drilldown**
|
||
- Zeilenname und jede Zahl führen in die Buchungen dahinter, die Zahl zusätzlich
|
||
eingeschränkt auf ihre Spalte.
|
||
- Gefiltert wird durch Nachsieben derselben Bewegungen, aus denen die Zelle entstand –
|
||
nicht durch Übersetzen der Zeile in einen neuen Filter. Nur so trifft der Drilldown auch
|
||
*Nicht kategorisiert* oder eine Astsumme über mehrere Unterkategorien.
|
||
|
||
**CSV-Export** (`src/Services/BerichtExport.php`)
|
||
- Dieselben Zeilen und Vorzeichen wie am Bildschirm, je Berichtstyp eigene Spalten.
|
||
- Semikolon, Dezimalkomma, kein Tausenderpunkt, CRLF und BOM – sonst zeigt die
|
||
Tabellenkalkulation „Übrige“ statt „Übrige“ und liest Beträge als Text.
|
||
|
||
**Tests**
|
||
- `tests/14_AuswertungTest.php` (142 Prüfungen) und
|
||
`tests/e2e/specs/auswertungen.spec.js` (7 Abläufe)
|
||
- Einzelheiten in `docs/auswertungen.md`
|
||
|
||
## Cockpit: Vermögensentwicklung als gestapeltes Balkendiagramm (11.08.2026)
|
||
|
||
- Die Kachel *Vermögensentwicklung* war ein Platzhalter und zeigt jetzt die Kontostände
|
||
der letzten zwölf Monate je Monatsende – ein Balken pro Monat, ein Farbton pro Konto
|
||
(`Vermoegensentwicklung::reihe()`, `src/Views/cockpit/vermoegensdiagramm.php`).
|
||
- Negative Stände stapeln unter der Nulllinie.
|
||
- Schalter *nur aktive* oben rechts an der Kachel, im Standard eingeschaltet: Er blendet
|
||
stillgelegte Konten samt Legendeneintrag aus und merkt sich seinen Zustand über die
|
||
Einstellungen (`cockpit_vermoegen_nur_aktive`) – auch über das Abmelden hinaus. Er ist
|
||
das einzige Kriterium; das Σ-Kennzeichen filtert hier **nicht** mit, weil stillgelegte
|
||
Konten es meist nicht tragen und beim Ausschalten sonst genau das fehlte, was man sehen
|
||
will.
|
||
- `Buchung::bewegungenJeKontoUndMonat()` liefert die Monatsbewegungen in einer Abfrage je
|
||
Quelle – zwölf Stichtagsabfragen wären es sonst.
|
||
- Mehr Konten als Farbtöne: Die größten behalten ihre Farbe, der Rest wird zu *Übrige
|
||
Konten (n)* zusammengefasst. Zwei Konten in derselben Farbe wären in der Legende nicht
|
||
auseinanderzuhalten.
|
||
- Eigenes SVG, keine Diagrammbibliothek. Die Farben stehen als Klasse am Element und
|
||
folgen damit dem hellen wie dem dunklen Thema; jedes Balkenstück trägt ein `<title>`
|
||
als Tooltip, ohne eine Zeile JavaScript.
|
||
- **Übrige Konten (n)** ist ein Link: Er führt eine Ebene tiefer (`/?ebene=N`) und zeigt
|
||
die nächstkleineren Konten einzeln – beliebig tief, bis der Rest in die Palette passt.
|
||
Unter der Legende steht der Weg zurück. Jedes Konto erscheint auf genau einer Ebene, und
|
||
eine zu hohe Ebene fällt auf die letzte zurück (`Vermoegensentwicklung::ebenen()`).
|
||
- Die Balken nutzen die Kachelbreite aus, indem sie **auseinanderrücken**, nicht indem sie
|
||
breiter werden: Die Zeichenfläche rechnet waagerecht in Prozent, die Balkenbreite bleibt
|
||
bei 16 Punkten. Achsenbeschriftung und Zeichenfläche sind dafür zwei SVGs nebeneinander –
|
||
ein einzelnes könnte von seinen Prozentangaben keine feste Achsenbreite abziehen.
|
||
- Die Kachel ist so groß wie ihre Nachbarn: Der Ausgriff über zwei Rasterspalten
|
||
(`.kachel.breit`) und das engere Cockpit-Raster (`.kachelraster.dicht`) entfallen wieder.
|
||
- Die Legende steht unter der Zeichnung statt daneben und in kleinerer Schrift (.74rem);
|
||
sie füllt so viele Spalten, wie die Kachel hergibt. Der Schalter teilt sich die erste
|
||
Zeile mit der Überschrift, damit er in der schmaleren Kachel oben rechts bleibt.
|
||
|
||
## Budgetplaner: drei Ansichten und Verteilen über das Zahnrad (11.08.2026)
|
||
|
||
- Abgelaufene Monate werden **nicht mehr zusammengefasst**. Alle zwölf Monate stehen
|
||
einzeln da; die Sammelspalte „Januar–Vormonat“ entfällt samt
|
||
`Budgetplaner::abgelaufenBis()` und `Budgetplaner::summe()`.
|
||
- Statt *Verlauf* und *alle Monate* gibt es drei Ansichten: **Alles** (Budget, Ist und
|
||
Saldo je Monat), **Nur Budget** und **Nur IST** mit je einer Spalte. Alte URLs mit
|
||
`ansicht=verlauf|alle` fallen auf *Alles* zurück.
|
||
- Die Budgetspalte ist überall dort, wo sie steht, editierbar – auch in vergangenen
|
||
Monaten.
|
||
- Vor jedem Eingabefeld sitzt ein **⚙**: *Ganzes Jahr* oder *Restliches Jahr* schreibt den
|
||
Wert des Feldes in die übrigen Monate der Zeile und überschreibt, was dort stand.
|
||
Gearbeitet wird auf den Eingabefeldern, sichtbar vor dem Speichern; der Knopf **↦** und
|
||
`Budget::aufFolgemonateUebertragen()` entfallen dafür.
|
||
- `app.js` war doppelt eingebunden (Kopf **und** Fuß): Jeder Handler lief zweimal, jede
|
||
Rückfrage kam zweimal. Jetzt nur noch im Kopf.
|
||
|
||
## Phase 3 – Budgetplaner und Monatsabschluss (11.08.2026)
|
||
|
||
**Datenmodell**
|
||
- Migration `004_budget_monatsabschluss.sql`: `budgets`, `budget_positionen`,
|
||
`monatsabschluesse`
|
||
- Ein Budget gilt für ein Jahr, mehrere je Jahr sind möglich, genau eines ist aktiv
|
||
- Ein Monat ohne Zeile in `budget_positionen` ist *nicht geplant* – etwas anderes als
|
||
*mit 0,00 geplant*
|
||
|
||
**Budgetplaner**
|
||
- Zeilen sind der Kategoriebaum (auf- und zuklappbar), Spalten die Monate; die ganze
|
||
Tabelle wird in einem Formular gespeichert, damit mehrere Änderungen zusammen ankommen
|
||
- Zwei Ansichten: *Verlauf* (Jahressumme, abgelaufene Monate zusammengefasst, laufender
|
||
Monat mit Budget/Ist/Saldo, danach Folgemonate) und *alle Monate* zum Planen
|
||
- Ausgaben werden ohne Vorzeichen eingetragen; das Vorzeichen kommt aus der Kategorieart.
|
||
Saldo ist Ist minus Budget – positiv heißt besser als geplant, negativ ist die
|
||
Überschreitung und wird rot ausgewiesen
|
||
- Übergeordnete Kategorien summieren ihre Nachfahren und sind selbst nicht beplanbar
|
||
- Wert auf alle Folgemonate übertragen, Budget als Kopie ins neue Jahr
|
||
|
||
**Monatsabschluss**
|
||
- Prüflauf über nicht kategorisierte Buchungen, nicht abgeglichene Umsätze und
|
||
Splitsummen-Differenzen; er hält niemanden auf, sondern zeigt den Stand
|
||
- Snapshot der Kontosalden und Kategoriesummen als JSON, auch nach Wiedereröffnung
|
||
- Abgeschlossene Monate sperren Buchungen (`Buchung::anlegen/speichern/loeschen`), in
|
||
beide Richtungen: weder hinein- noch herausschieben. Abschaltbar über die Einstellung
|
||
`monatsabschluss_sperrt_buchungen`
|
||
- Wiedereröffnen bleibt am Datensatz vermerkt
|
||
|
||
**Vorarbeiten in der Oberfläche**
|
||
- `.knopf.primaer` trug einen eigenen `margin-top` und stand dadurch in jeder Knopfleiste
|
||
tiefer als seine Nachbarn – sichtbar etwa an „Abbrechen“ neben „Konto anlegen“. Der
|
||
Abstand liegt jetzt allein bei der Knopfleiste.
|
||
- Kontengruppen werden wie Konten und Kategorien auf einer eigenen Formularseite gepflegt
|
||
statt inline in der Tabelle. Dort hängt auch die Zuordnung der Konten: ankreuzen statt
|
||
jedes Konto einzeln zu öffnen (`Kontengruppe::kontenZuordnen()`).
|
||
- Der QIF-Export enthält **keine** Kontengruppen – kein Feld im `!Account`-Eintrag, nichts
|
||
in der `QSD`-Datei. Import-Oberfläche und Dokumentation sagen das jetzt und verweisen
|
||
auf die Zuordnung.
|
||
- Die Einrückung des Kategoriebaums lief über ein `style`-Attribut und wurde von der
|
||
Content-Security-Policy (`style-src 'self'`) stillschweigend verworfen; sie steht jetzt
|
||
als `data-ebene` im Markup. Aus demselben Grund blendete `app.js` Erfolgsmeldungen nie
|
||
aus – das läuft jetzt über eine Klasse statt über `element.style`.
|
||
|
||
**Tests**
|
||
- `11_BudgetTest`, `12_MonatsabschlussTest` (76 Prüfungen), E2E `planung.spec.js`
|
||
- Einzelheiten in `docs/planung.md`
|
||
|
||
## Korrektur – Σ-Kennzeichen ließ sich nicht abschalten (10.08.2026)
|
||
|
||
Das Kennzeichen *Zählt zur Summe der Kontostände* blieb beim Speichern eines Kontos
|
||
unverändert stehen, obwohl die Anwendung „Konto gespeichert“ meldete.
|
||
|
||
- Ursache: `KontenController::formulardaten()` reichte `in_summe` gar nicht durch. Ein
|
||
nicht angehaktes Ankreuzfeld steht nicht im POST, also griff im Model der Standardwert
|
||
„zählt mit“ – gespeichert wurde ein Ja, das niemand angegeben hatte.
|
||
- `formulardaten()` nimmt das POST-Array jetzt als Parameter und ist damit ohne HTTP
|
||
prüfbar; die Ankreuzfelder werden ausdrücklich auf `true`/`false` abgebildet.
|
||
- Der Standard „zählt zur Summe“ bleibt für Aufrufe ohne den Schlüssel bestehen – der
|
||
QIF-Import gibt das Feld nicht an und soll es auch nicht.
|
||
- `07_KontenTest` prüft die Formularabbildung und den Weg durch das Model; E2E
|
||
`stammdaten.spec.js` hakt das Kennzeichen in der Oberfläche ab, setzt es wieder und
|
||
sieht in beiden Fällen nach, statt der Erfolgsmeldung zu glauben.
|
||
|
||
## Phase 2 – Buchungen, Kontoblatt und Buchungsimport (10.08.2026)
|
||
|
||
**Erkenntnis aus dem Vollexport**
|
||
|
||
Der Plan ging davon aus, Umbuchungen stünden zweimal in der Datei und müssten
|
||
zusammengeführt werden. Das Gegenteil ist der Fall: Eine Umbuchung steht **einmal**, in
|
||
dem Konto, in dem sie erfasst wurde; die Gegenseite ist nur durch `L[Zielkonto]` benannt
|
||
und muss beim Import erzeugt werden. Von 965 Umbuchungen im Export hat keine ein zweites
|
||
Vorkommen. Mit erzeugter Gegenseite stimmen acht gegen den FinanzManager geprüfte
|
||
Kontosalden auf den Cent.
|
||
|
||
**Datenmodell**
|
||
- Migration `003_buchungen.sql`: `buchungen`, `buchung_splits`, `buchungsadressen`
|
||
- Eine Umbuchung ist ein Datensatz mit `gegenkonto_id`, kein gespiegeltes Paar – das
|
||
Kontoblatt des Gegenkontos zeigt sie mit umgekehrtem Vorzeichen
|
||
- Auch einzelne Splitzeilen dürfen ein Gegenkonto tragen
|
||
- Konten führen `in_summe` (die Σ-Spalte des FinanzManagers) sowie `saldo_online`
|
||
|
||
**Kontoblatt und Buchungsmaske**
|
||
- Kontoblatt mit laufendem Saldo, Filter auf Zeitraum, Kategorie, Klasse und Abgleich,
|
||
Volltextsuche und Seitenblättern
|
||
- Buchungsmaske für Einnahme, Ausgabe und Umbuchung in beide Richtungen; Splitzeilen mit
|
||
Restbetragsanzeige, geprüft auch serverseitig
|
||
- Aktueller und erwarteter Saldo; in die Summen zählen nur Konten mit Σ-Kennzeichen
|
||
|
||
**QIF-Buchungsimport**
|
||
- `QifBuchungsImport` mit Stichtag: alles davor wird je Konto zum Anfangssaldo
|
||
aufsummiert – einschließlich der erzeugten Gegenbuchungen –, alles danach importiert
|
||
- Buchungen mit Datum nach dem Importtag gelten als Vorausberechnung und bleiben draußen
|
||
- Eröffnungsbuchungen (`L[eigenes Konto]`) werden nicht zu Umbuchungen
|
||
- Wertpapiergeschäfte schlagen vorerst nur mit ihrem Geldfluss auf dem Verrechnungskonto
|
||
zu Buche; Bestände folgen in Phase 6
|
||
- Idempotent über einen Fingerabdruck je Buchung, der auch echte Doppelbuchungen am
|
||
selben Tag auseinanderhält
|
||
- Am Vollexport geprüft: 833 Buchungen ab 01.01.2026, 7946 in die Anfangssalden von 35
|
||
Konten verrechnet, eine vorausberechnete verworfen
|
||
|
||
**Behoben**
|
||
- Die `confirm()`-Rückfragen vor dem Löschen waren wirkungslos. `public/static/app.js`
|
||
brachte den Mechanismus (`data-bestaetigen`) von Anfang an mit, war aber nie im Layout
|
||
eingebunden; ersatzweise standen Inline-Handler (`onsubmit`) im HTML, und die verstoßen
|
||
gegen die eigene Content-Security-Policy (`script-src 'self'`) – der Browser verwarf
|
||
sie stillschweigend, gelöscht wurde ohne Rückfrage. Jetzt ist das Skript eingebunden,
|
||
die Inline-Handler sind raus, und ein E2E-Test besteht auf dem Dialog.
|
||
|
||
**Tests**
|
||
- `09_BuchungenTest` – Umbuchungen mit zwei Sichten, Splitzeilen samt Summenprüfung,
|
||
Splitzeile mit Gegenkonto, aktueller und erwarteter Saldo, Σ-Kennzeichen, Filter
|
||
- `10_QifBuchungsImportTest` – Stichtag, Anfangssalden aus der Vorgeschichte, erzeugte
|
||
Gegenbuchung, Eröffnungsbuchung, Zukunftsfilter, zweiter Lauf ohne Wirkung
|
||
- E2E `buchungen.spec.js` – Ausgabe, Umbuchung auf beiden Kontoblättern, Splitbuchung,
|
||
abgewiesene Splitsumme, Suche, Löschbestätigung
|
||
|
||
## Phase 1 – Stammdaten und QIF-Stammdatenimport (09.08.2026)
|
||
|
||
**Datenmodell**
|
||
- Migration `002_stammdaten.sql`: `kontengruppen`, `konten`, `kategorien`, `klassen`
|
||
- Alle Tabellen führen `benutzer_id`; `Mandant::id()` liefert die Kennung aus der Session
|
||
oder – im CLI – vom ersten aktiven Benutzer
|
||
|
||
**Konten**
|
||
- Kontenübersicht nach Kontengruppen gebündelt, mit Gruppensummen und Gesamtsumme
|
||
- Die drei Saldospalten des FinanzManagers (Online-Saldo, aktueller und erwarteter Saldo)
|
||
sind angelegt und werden bis Phase 2 aus dem Anfangssaldo gespeist
|
||
- Kontoarten des FinanzManagers samt Zuordnung zu den QIF-Typen
|
||
- Kontenpflege mit Anfangssaldo, Kreditlimit, IBAN/BIC, Kontengruppe, aktiv/inaktiv
|
||
- Kontengruppen anlegen, umbenennen, sortieren, löschen – die Konten bleiben erhalten
|
||
|
||
**Kategorien und Klassen**
|
||
- Hierarchische Kategorien mit `:` als Trenner; der vollständige Pfad wird mitgeführt und
|
||
beim Umbenennen oder Verschieben für den ganzen Teilbaum neu geschrieben
|
||
- Art (Einnahme/Ausgabe) gilt für den ganzen Ast und vererbt sich an Unterkategorien
|
||
- Verschieben, Zusammenführen und Löschen; Verschieben in den eigenen Teilbaum wird
|
||
abgewiesen
|
||
- Register *Alle / Einnahmen / Ausgaben / Umbuchungskonten* wie im Original
|
||
- Klassen als flache zweite Auswertungsdimension, in der Liste direkt bearbeitbar
|
||
|
||
**QIF-Import**
|
||
- `QifParser`: zustandsbehaftetes, sequenzielles Lesen; ISO-8859-1 nach UTF-8, US-Beträge,
|
||
Datumsformat aus der Kopfzeile, Mehrfachfelder für spätere Splitzeilen
|
||
- `QifStammdatenImport` für Klassen, Kategorien und Konten – idempotent, mit Trockenlauf
|
||
und Protokoll; fehlende Zwischenstufen im Kategoriepfad entstehen selbsttätig
|
||
- Dialog unter *Verwaltung → QIF-Import* mit Upload, Trockenlauf und Übernahme
|
||
- `scripts/qif-import.php` für dieselbe Aufgabe auf der Kommandozeile
|
||
- Geprüft am echten Export: 17 Klassen, 105 Kategorien, 37 Konten
|
||
|
||
**Tests**
|
||
- `06_KategorienTest` – Hierarchie, Pfadpflege, Artvererbung, Verschieben, Zusammenführen
|
||
- `07_KontenTest` – Kontoarten, Gruppierung und Summen, Betragseingabe
|
||
- `08_QifImportTest` – Parser, Zeichensatz, Beträge und Datum, Import, Trockenlauf,
|
||
Prüfung der Dateinamen
|
||
- E2E `stammdaten.spec.js` – Konto, Kategorie mit Unterkategorie, Register, Klasse,
|
||
QIF-Upload mit Trockenlauf
|
||
|
||
## Korrektur – Monatsnamen auf Deutsch (09.08.2026)
|
||
|
||
- Der Einstellungsdialog zeigte unter *Buchhaltung* englische Monatsnamen, weil
|
||
`date('F')` sprachlich nicht umschaltbar ist.
|
||
- Monatsnamen liegen jetzt zentral als `View::MONATE` und `View::MONATE_KURZ` vor,
|
||
abrufbar über `View::monat()`. Budgetplaner und Auswertungen greifen später darauf zu.
|
||
- `05_DarstellungTest` prüft Monatsnamen, Betrags- und Datumsformat gegen eine feste
|
||
Sollliste – der Vergleich gegen `date('F')` allein würde April, August, September und
|
||
November nicht auffallen lassen.
|
||
|
||
## Phase 0 – Projektgerüst (09.08.2026)
|
||
|
||
Grundgerüst der Anwendung. Fachliche Module folgen ab Phase 1.
|
||
|
||
**Betrieb**
|
||
- Docker Compose mit drei Services: `nginx` (Port 8000), `php` (php-fpm 8.3), `db`
|
||
(MariaDB 11)
|
||
- `docker-entrypoint.sh` legt Datenverzeichnisse an und spielt Migrationen ein
|
||
- `scripts/migrate.php` – nummerierte SQL-Migrationen, idempotent, mit Journal-Tabelle
|
||
- `scripts/update.sh` – Update für den Produktionsserver: Backup, `git pull`,
|
||
Image-Neubau bei Bedarf, Migrationen, Rechte, Tests
|
||
- `scripts/backup.sh` – Datenbank- und Dateisicherung mit Rotation (7 Tage / 4 Wochen /
|
||
12 Monate), gegen Parallelläufe per `flock` abgesichert
|
||
|
||
**Anwendung**
|
||
- `Config`, `Database` (PDO-Wrapper, verschachtelbare Transaktionen), `Router`
|
||
(Platzhalter-Routen, CSRF-Prüfung für alle verändernden Requests), `View`, `Auth`
|
||
- Anmeldung mit Session-Härtung, Ersteinrichtung des ersten Benutzers
|
||
- Zentraler Einstellungsdialog mit Reitern (Allgemein, Darstellung, Buchhaltung,
|
||
Online-Banking, Import/Export, Wertpapiere, Diagnose); Werte in Tabelle
|
||
`einstellungen`
|
||
- Debug-Log, ein- und ausschaltbar, mit Maskierung von PINs, Passwörtern, TANs und
|
||
API-Keys; Ausnahmen werden immer protokolliert
|
||
- Layout nach Vorbild des FinanzManagers: feste Navigation links, Werkzeugleiste oben,
|
||
Tab-Leiste; Farbschema hell/dunkel/System
|
||
|
||
**Tests**
|
||
- Test-Harness mit Transaktions-Rollback und Aufräumen temporärer Pfade
|
||
- `01_AuthTest` – Anmeldung, Passwort-Hashing, CSRF, Cookie-Attribute
|
||
- `02_EinstellungenTest` – Lesen/Schreiben, Cache-Invalidierung, Bereichsabdeckung
|
||
- `03_RouterTest` – Pfad-Muster, Platzhalter, keine Treffer über Pfadgrenzen
|
||
- `04_DebugLogTest` – Schalter, Maskierung, Fehlerprotokollierung
|