Kontenzuordnung als dritter Einrichtungsschritt: Welches Bankkonto gehört zu welchem eigenen Konto? Umsätze merken sich über `bank_konto_id`, woher sie stammen, und ziehen mit, wenn die Zuordnung erst nach einem Abruf fällt. Abgerufene Umsätze buchen sich auf Wunsch selbst (`Services\Bankabgleich`). Im Kontoblatt lassen sich Buchungen sammelweise löschen; Statusmeldungen liegen jetzt über der Ansicht, statt den Inhalt springen zu lassen. Dazu drei Fehler, die zusammen dafür sorgten, dass abgerufene Umsätze spurlos verschwanden – der Abruf meldete sie als vorhanden, das Kontoblatt zeigte nichts, und in der Vorschlagsliste standen sie ebenfalls nicht: - Eine gelöschte Buchung ließ ihren Umsatz auf „uebernommen“ stehen. Der Fremdschlüssel setzte nur `buchung_id` auf NULL. `Buchung::loeschen()` gibt den Umsatz jetzt vorher frei, `Bankumsatz::verwaisteOeffnen()` fängt die Wege ab, die daran vorbeiführen – Kaskaden lösen in MariaDB keine Trigger aus. - Ein Bankkonto entstand doppelt, wenn die Bank die IBAN nicht bei jedem Dialog mitliefert. Gesucht wird jetzt über die Kontonummer; eine nachgelieferte IBAN wird am vorhandenen Eintrag nachgetragen. - Der Fingerabdruck hing an der IBAN und kippte mit ihr. Er nimmt jetzt die `bank_konto_id`, die fest steht, solange es den Bankzugang gibt. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
88 lines
4 KiB
Markdown
88 lines
4 KiB
Markdown
# Buchungen
|
||
|
||
Kontoblatt, Buchungsmaske und die Saldenrechnung.
|
||
|
||
## Umbuchungen: ein Datensatz, zwei Sichten
|
||
|
||
Eine Umbuchung wird **einmal** gespeichert – als Buchung auf dem Ausgangskonto mit
|
||
`gegenkonto_id`. Das Kontoblatt des Gegenkontos zeigt sie mit umgekehrtem Vorzeichen.
|
||
|
||
Der Grund ist nicht nur Sparsamkeit: Zwei gespiegelte Datensätze laufen früher oder später
|
||
auseinander (einer wird geändert, der andere nicht). Und der QIF-Export macht es genauso –
|
||
dort steht jede Umbuchung genau einmal, die Gegenseite nur als `L[Zielkonto]`.
|
||
|
||
`Buchung::kontoblatt()` vereinigt deshalb drei Mengen:
|
||
|
||
1. Buchungen mit `konto_id = X` (eigene Sicht)
|
||
2. Buchungen mit `gegenkonto_id = X` ohne Splitzeilen, Betrag negiert
|
||
3. Splitzeilen mit `gegenkonto_id = X`, Betrag negiert
|
||
|
||
Zeilen aus 2 und 3 tragen `gegensicht = true`. Sie sind nicht bearbeitbar – das geht auf
|
||
dem Ursprungskonto –, damit eine Umbuchung nicht aus zwei Richtungen widersprüchlich
|
||
geändert wird.
|
||
|
||
Dieselbe Dreiteilung gilt für die Salden (`Buchung::saldenJeKonto()`), die alle Konten mit
|
||
sechs Abfragen auf einmal berechnet statt je Konto einzeln.
|
||
|
||
## Mehrfachauswahl im Kontoblatt
|
||
|
||
Vor dem Datum steht je Zeile ein Ankreuzfeld, in der Titelzeile eines für alle gerade
|
||
angezeigten Buchungen. *Auswahl löschen* in der Aktionsleiste schickt die Auswahl an
|
||
`POST /konten/{id}/buchungen/loeschen`.
|
||
|
||
Zwei Punkte, die sich nicht ändern dürfen:
|
||
|
||
* Der Knopf steht **über** der Tabelle, die Ankreuzfelder stehen **in** ihr. Verbunden sind
|
||
sie über `form="buchungsauswahl"`, nicht über Verschachtelung – ein Formular um die ganze
|
||
Tabelle würde das Filterformular umschließen.
|
||
* Zeilen mit `gegensicht = true` bekommen kein Ankreuzfeld, und der Controller prüft
|
||
zusätzlich `konto_id`. Die Gegenseite einer fremden Umbuchung wird nur auf ihrem
|
||
Ursprungskonto gelöscht – dieselbe Regel wie beim Bearbeiten.
|
||
|
||
Filter und Seite hängen an der Abfragezeichenfolge des Formularziels. Der Controller baut
|
||
den Weg zurück aus den bekannten Feldern neu zusammen, statt ein mitgeschicktes Ziel zu
|
||
übernehmen; so lässt sich kein fremder Weiterleitungsort einschleusen.
|
||
|
||
## Splitbuchungen
|
||
|
||
Eine Buchung ist entweder einfach zugeordnet **oder** gesplittet. Sobald Splitzeilen
|
||
vorliegen, räumt das Model `kategorie_id` und `gegenkonto_id` der Buchung; die Zuordnung
|
||
steckt dann ausschließlich in den Zeilen.
|
||
|
||
Die Summe der Zeilen muss dem Buchungsbetrag entsprechen – geprüft im Model (Toleranz ein
|
||
halber Cent, für Rundungen aus der Betragsumrechnung) und in der Maske als
|
||
Restbetragsanzeige. Eine Splitzeile darf ein Gegenkonto tragen; dann erscheint auf jenem
|
||
Konto nur der Betrag dieser Zeile.
|
||
|
||
## Salden
|
||
|
||
| Spalte | Bedeutung |
|
||
|---|---|
|
||
| Online-Saldo | Stand des letzten Bankabrufs (`konten.saldo_online`, ab Phase 5) |
|
||
| Aktueller Saldo | Anfangssaldo plus alle Buchungen bis heute |
|
||
| Erwarteter Saldo | zusätzlich die künftig datierten Buchungen |
|
||
|
||
In die **Summe der Kontostände** zählen nur Konten mit `in_summe = 1` – das entspricht der
|
||
Σ-Spalte im FinanzManager. Durchlaufende Auslagen und Rentenversicherungen bleiben so
|
||
außen vor, erscheinen aber weiter in der Liste. Der QIF-Export kennt das Kennzeichen
|
||
nicht; es wird im Kontoformular gepflegt.
|
||
|
||
## Beträge in der Oberfläche
|
||
|
||
Eingegeben wird immer ein **positiver** Betrag; die Richtung bestimmt die Auswahl
|
||
*Ausgabe / Einnahme / Umbuchung*. `KontenController::zahl()` liest das deutsche Format und
|
||
entscheidet bei einem Punkt ohne Komma nach der Stellenzahl: drei Ziffern dahinter heißt
|
||
Tausendertrenner (`1.500`), sonst Dezimaltrenner (`1234.56`).
|
||
|
||
## Bestätigungen und die Content-Security-Policy
|
||
|
||
Die CSP erlaubt keine Inline-Skripte (`script-src 'self'`). Rückfragen vor dem Löschen
|
||
dürfen deshalb **nicht** als `onsubmit="return confirm(…)"` im HTML stehen – der Browser
|
||
verwirft sie stillschweigend, und gelöscht wird ohne Nachfrage. Stattdessen:
|
||
|
||
```html
|
||
<form method="post" action="…" data-bestaetigen="Buchung wirklich löschen?">
|
||
```
|
||
|
||
`public/static/app.js` hängt den Dialog daran. Ein E2E-Test besteht auf dem Dialog, damit
|
||
das nicht unbemerkt zurückfällt.
|