Die Klassenspalte ist anklickbar wie die Zuordnung daneben: Ein Klick
öffnet die Klassenliste in der Zelle, die Auswahl wird sofort über
POST /buchungen/{id}/klasse gespeichert. Buchung::klasseSetzen() lässt
Kategorie, Gegenkonto und Splitzeilen unberührt.
Die Buchungszeilen sind jetzt alle zwei Textzeilen hoch, ihr Inhalt steht
senkrecht in der Mitte. Was nicht auf zwei Zeilen passt, wird
abgeschnitten; den vollständigen Text zeigt der Tooltip – bei Empfänger
und Buchungstext nur dort, wo wirklich etwas fehlt, über dem
Kategoriefeld einer Splitbuchung deren Zeilen mit Kategorie und Betrag.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
203 lines
11 KiB
Markdown
203 lines
11 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.
|
||
|
||
**Beide Sichten nennen das Gegenüber.** In der Kategoriespalte steht das jeweils andere
|
||
Konto in eckigen Klammern: auf dem Ausgangskonto das Ziel, auf dem Gegenkonto das
|
||
Ursprungskonto. In der Gegensicht liefert die Abfrage dafür `b.konto_id` als
|
||
`gegenkonto_id` – dieselbe Spalte, aus der anderen Richtung gelesen. Ohne diese Angabe
|
||
stünde auf dem Gegenkonto eine Zeile, die sich „Umbuchung“ nennt und verschweigt, woher
|
||
sie kommt.
|
||
|
||
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.
|
||
|
||
## Kategorie und Gegenkonto in einem Feld
|
||
|
||
Eine Buchung ist entweder kategorisiert **oder** eine Umbuchung – beides zugleich gibt es
|
||
nicht. Deshalb stehen Kategorien und Konten in derselben Auswahlliste; die Konten unten,
|
||
in eckigen Klammern, wie das Kategoriefeld im FinanzManager sie schreibt. Das eigene Konto
|
||
fehlt in der Liste.
|
||
|
||
Die Liste steht in `Views/buchungen/zuordnungsliste.php` und wird an vier Stellen
|
||
eingebunden: Buchungsmaske, deren Splitzeilen, Schnellzuordnung im Kontoblatt und
|
||
Splitdialog. Die Werte tragen ein Präfix, das `BuchungenController::zuordnungAus()` wieder
|
||
auseinandernimmt:
|
||
|
||
| Wert | Bedeutung |
|
||
|---|---|
|
||
| `` (leer) | keine Zuordnung |
|
||
| `kategorie:12` | Kategorie 12 |
|
||
| `konto:7` | Umbuchung mit Gegenkonto 7 |
|
||
| `split` | Splitdialog öffnen (nur im Kontoblatt) |
|
||
| `depot:7` | Wertpapiergeschäft im Depot 7 (Kontoblatt und Buchungsmaske) |
|
||
|
||
Die Auswahl *Ausgabe / Einnahme* bestimmt seither nur noch die Richtung des Betrags.
|
||
|
||
Ein Depot steht **zweimal** in der Liste: als gewöhnliche Umbuchung (`konto:7`) und in der
|
||
Gruppe *Wertpapiergeschäft im Depot* (`depot:7`). Beides gibt es wirklich – auf ein Depot
|
||
lässt sich Geld überweisen, ohne dass etwas gekauft wird. `depot:7` speichert keine
|
||
Zuordnung, sondern öffnet die Wertpapiermaske; daraus wird am Ende meist eine Splitbuchung
|
||
(siehe `docs/wertpapiere.md`, *Geschäft aus einer Kontobuchung*). In den **Splitzeilen**
|
||
fehlt die Gruppe: Dort steht der Betrag der Zeile schon fest, und die Umbuchung ins Depot
|
||
ist genau das, was gebraucht wird.
|
||
|
||
## Schnellzuordnung im Kontoblatt
|
||
|
||
Ein Klick auf die Kategoriespalte öffnet die Liste in der Zelle; die Auswahl geht an
|
||
`POST /buchungen/{id}/zuordnung`, der Eintrag *Splitbuchung* öffnet stattdessen den
|
||
Splitdialog (`POST /buchungen/{id}/splitbuchung`). Beide Wege gehen über
|
||
`Buchung::zuordnen()` und `Buchung::splitsErsetzen()`, die den Rest der Buchung unberührt
|
||
lassen und den Monatsabschluss achten.
|
||
|
||
Drei Punkte, die sich nicht ändern dürfen:
|
||
|
||
* Die Auswahlliste gibt es **einmal**; `kontoblatt.js` hängt sie in die angeklickte Zelle
|
||
und danach zurück. Eine Liste je Zeile wären bei hundert Zeilen ebenso viele
|
||
vollständige Kategoriebäume im HTML.
|
||
* Ohne JavaScript führt derselbe Klick in die Buchungsmaske – die Zelle ist ein Link, kein
|
||
Knopf.
|
||
* Zeilen mit `gegensicht = true` führen zu ihrer Buchung, statt die Auswahl zu öffnen: Sie
|
||
werden auf ihrem Ursprungskonto geändert, wie beim Bearbeiten und Löschen.
|
||
`kontoblatt.js` erkennt sie an der Klasse `gegenseite`.
|
||
* Feld und Auswahlliste sind **gleich breit** (`13rem`), und die Zelle zeigt nichts an, was
|
||
nicht auch in der Liste stünde – kein „zuordnen …“. Sonst rückt beim Anklicken die halbe
|
||
Zeile: Die Spalte wüchse auf die Breite der längsten Kategorie, und der Empfänger daneben
|
||
bräche um. Senkrecht rückt nichts, weil die Zeilenhöhe ohnehin feststeht (siehe unten).
|
||
|
||
Die Auswahl eines Depots (`depot:7`) öffnet statt beider den **Wertpapierdialog**; ohne
|
||
JavaScript führt sie auf `/buchungen/{id}/wertpapier?depot=7`, wo dieselben Felder als
|
||
gewöhnliches Formular stehen. Auch die Buchungsmaske leitet dorthin, nachdem sie die
|
||
Buchung gespeichert hat.
|
||
|
||
Eine schon gesplittete Buchung öffnet auf Klick **direkt den Splitdialog**, nicht die
|
||
Auswahlliste: Deren einziger sinnvoller Eintrag wäre für sie ohnehin *Splitbuchung*
|
||
gewesen. Wer eine Aufteilung wieder in eine einzelne Zuordnung wandeln will, löscht sie
|
||
im Dialog und ordnet die dann unzugeordnete Buchung anschließend zu – oder geht über die
|
||
Buchungsmaske.
|
||
|
||
## Klasse im Kontoblatt
|
||
|
||
Die Klassenspalte ist nach derselben Bauart anklickbar: eine zweite Liste
|
||
(`#klassenauswahl`), die `kontoblatt.js` in die angeklickte Zelle hängt. Ihr erster Eintrag
|
||
*— keine —* löscht die vorhandene Klasse, jeder andere setzt sie; gespeichert wird sofort
|
||
beim Anklicken, über `POST /buchungen/{id}/klasse` und `Buchung::klasseSetzen()`.
|
||
|
||
Die Klasse steht **neben** der Zuordnung, nicht in ihr: Sie ist die zweite
|
||
Auswertungsdimension. Anders als `Buchung::zuordnen()` rührt `klasseSetzen()` deshalb weder
|
||
Kategorie noch Gegenkonto noch Splitzeilen an – auch eine gesplittete Buchung bleibt beim
|
||
Wechsel der Klasse gesplittet. Den Monatsabschluss achtet die Methode wie alle anderen.
|
||
|
||
## Zeilenhöhe und Tooltips im Kontoblatt
|
||
|
||
Jede Buchungszeile ist **zwei Textzeilen hoch**, gleich wie viel in ihr steht, und ihr
|
||
Inhalt steht senkrecht in der Mitte. Eine Zeile, die mit ihrem Text wächst, ließe die Liste
|
||
beim Blättern hüpfen; eine, die auf eine Zeile schrumpft, machte lange Empfänger unlesbar.
|
||
Die Höhe hängt an `table.kontoblatt td`, das Abschneiden an `.zellentext` (zwei Zeilen,
|
||
danach Auslassungspunkte). Empfänger und Buchungstext stehen deshalb in einem `<div>` und
|
||
nicht unmittelbar in der Zelle: Abschneiden kann nur ein Element, das kein Tabellenfeld ist.
|
||
|
||
Die Marke der Umbuchungs-Gegenseite steht **neben** dem abgeschnittenen Kasten
|
||
(`.zellenreihe`), nicht darin – sonst zählte sie als eine der beiden Zeilen und die Zeile
|
||
würde höher als die übrigen.
|
||
|
||
Den vollständigen Text zeigt der Tooltip:
|
||
|
||
* **Empfänger und Buchungstext**: `kontoblatt.js` setzt den Tooltip nach dem Laden und nach
|
||
jeder Größenänderung des Fensters – aber nur dort, wo wirklich etwas abgeschnitten ist.
|
||
Ob das der Fall ist, weiß erst der Browser nach dem Umbruch, und das hängt an der
|
||
Fensterbreite. Ein Tooltip, der bloß wiederholt, was ohnehin dasteht, wäre lästig.
|
||
* **Kategorie und Klasse** tragen ihren Tooltip vom Server. Bei einer **Splitbuchung** nennt
|
||
er die Zeilen der Aufteilung: je Zeile Kategorie (oder Gegenkonto) und Betrag, sonst
|
||
nichts. Notiz und Klasse stünden nur im Weg; wer sie ändern will, öffnet ohnehin den
|
||
Dialog.
|
||
|
||
## 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 beiden Masken als
|
||
Restbetragsanzeige. Beide rechnen mit `public/static/betrag.js`; die Regel für das
|
||
deutsche Zahlenformat steht damit einmal im Browser und einmal in PHP, nicht dreimal. Eine
|
||
Splitzeile darf ein Gegenkonto tragen; dann erscheint auf jenem Konto nur der Betrag dieser
|
||
Zeile. Auf das eigene Konto darf sie nicht zeigen – die Buchung zählte dort doppelt.
|
||
|
||
Der Knopf *Splitbuchung löschen* im Dialog schickt `POST /buchungen/{id}/splitbuchung` mit
|
||
`aktion=loeschen`; der Controller ruft dann `Buchung::splitsErsetzen($id, [])`, ganz gleich
|
||
was in den Feldern steht. Gelöscht werden die Zeilen samt Zuordnung, nicht die Buchung –
|
||
sie steht danach mit ihrem Betrag und ohne Kategorie im Kontoblatt, und die Gegenseite
|
||
einer Zeile mit Gegenkonto verschwindet von jenem Konto.
|
||
|
||
## 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*. `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.
|