Phase 9: Kassenverwaltung

Kassenbuch, Mitgliedsbeitraege, Rechnungen an Dritte, Kassenpruefung und
die Auswertungen fuer die Jahreshauptversammlung.

Die drei Festlegungen aus Phase-9.md sitzen in der Datenbank, nicht im
Router: eine festgeschriebene Buchung ist unveraenderbar, ein geprueftes
Jahr nimmt kein Datum mehr an, und gerechnet wird mit Prisma.Decimal.
Korrigiert wird ueber eine Gegenbuchung mit Verweis - beide Buchungen
bleiben stehen, damit im Kassenbuch zu sehen ist, dass etwas berichtigt
wurde.

Neun Modelle, drei Trigger, zwoelf CHECKs, sechs Router, fuenf Ansichten.
Dazu der Bankimport fuer CAMT.053 und MT940 ohne Fremdbibliothek, der
Kategorievorschlag aus dem eigenen Buchungsbestand und die Bruecke von
Phase 8: aus einem gegengezeichneten Beleg wird eine Buchung, das Foto
haengt danach an ihr.

Nebenbei behoben: die Rechnungsnummer war ein Wettlauf, und eine
Organisation liess sich nicht mehr loeschen, sobald sie eine
festgeschriebene Buchung hatte - der Trigger kannte den
Aufbewahrungsschalter nicht.

562 Unit-Tests und 239 E2E-Tests gruen.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Florian Wilde
2026-09-04 19:23:34 +02:00
co-authored by Claude Opus 5
parent d70acad4cd
commit 4165334c5a
78 changed files with 12314 additions and 42 deletions
+151 -1
View File
@@ -32,7 +32,7 @@ Anmelden mit `SEED_ADMIN_EMAIL` und `SEED_ADMIN_PASSWORT` aus der `.env`
| Phase 6 — Kartenmodule | fertig |
| Phase 7 — Benachrichtigungen und Termine | fertig |
| Phase 8 — Lokale KI | fertig |
| Phase 9 — Kassenverwaltung | offen |
| Phase 9 — Kassenverwaltung | fertig |
Vollständige Planung mit Aufwänden: [docs/ROADMAP.md](docs/ROADMAP.md) und
[docs/AUFWANDSSCHAETZUNG.md](docs/AUFWANDSSCHAETZUNG.md).
@@ -1155,6 +1155,156 @@ Fehlermeldung der Datenbank durchzureichen, die niemand versteht.
---
## Phase 9 — Kassenverwaltung
Kassenbuch, Mitgliedsbeiträge, Rechnungen an Dritte, Kassenprüfung und die
Zahlen für die Jahreshauptversammlung.
Eine Feuerwehrkasse wird von einem Ehrenamtlichen geführt und von zwei anderen
geprüft. Was die Prüfung wert ist, hängt einzig daran, dass sich das Geprüfte
hinterher nicht mehr ändern lässt. Deshalb ist der zentrale Punkt dieser Phase
kein Formular, sondern eine Sperre.
### Drei Festlegungen
**Eine festgeschriebene Buchung ist unveränderbar.** Nicht, weil ein Zweig im
Router es so vorsieht, sondern weil die Datenbank es ablehnt — ein vergessener
Zustandscheck, ein Skript, ein Import würden sonst reichen. Korrigiert wird
über eine **Gegenbuchung** mit Verweis: beide Buchungen bleiben stehen, und im
Kassenbuch ist zu sehen, dass etwas berichtigt wurde. Genau das will ein
Kassenprüfer sehen; eine verschwundene Buchung kann er nicht prüfen.
**Der Kassenabschluss schließt seinen Zeitraum.** Danach wird keine Buchung
mit einem Datum darin mehr angenommen — auch keine nachträglich
hineindatierte. Ohne diese zweite Hälfte stimmte der geprüfte Bestand nach
einem Datumswechsel nicht mehr, und niemand hätte es bemerkt. Wer nach der
Prüfung noch eine Quittung findet, bucht sie mit dem heutigen Datum.
**Beträge sind `DECIMAL(12,2)`, gerechnet wird mit `Prisma.Decimal`.**
`0.1 + 0.2` ergibt in JavaScript 0.30000000000000004. In einer Kasse ist das
kein Rundungsfehler, sondern eine Differenz in der Prüfung, die niemand
erklären kann.
### Das Kassenbuch
Drei Kontenarten: Barkasse, Bankkonto, Rücklage. Der **Bestand wird gerechnet,
nie gespeichert** — Anfangsbestand plus die Wirkung aller Buchungen. Ein
gespeicherter Bestand neben einer nachgetragenen Buchung wäre falsch, ohne
falsch auszusehen, und genau danach sucht ein Kassenprüfer.
Der Betrag einer Buchung ist immer positiv; die Richtung steht daneben. Eine
negative Ausgabe wäre eine Einnahme in Verkleidung und in keiner Auswertung
als solche zu erkennen — die Datenbank nimmt sie deshalb nicht an.
Die Buchungsmaske schlägt eine Kategorie vor: zuerst aus den **eigenen
früheren Buchungen** der Wehr, erst danach aus einer kurzen Stichwortliste.
Der Nutzen liegt nicht in den gesparten Sekunden, sondern darin, dass dieselbe
Sache immer unter derselben Kategorie landet — erst das macht die
Jahresauswertung brauchbar. Unter der Hälfte gemeinsamer Wörter schweigt der
Vorschlag: ein falscher wird übernommen, ohne gelesen zu werden.
Ein **gegengezeichneter Beleg** aus Phase 8 wird mit einem Klick zur Buchung:
Konto und Kategorie wählen, fertig. Betrag, Datum und Händler stehen schon
fest, und das Foto hängt danach an der Buchung — zu jeder Ausgabe ein Beleg,
an der Buchung und nicht in einem Ordner daneben. Ein Entwurf lässt sich
nicht buchen: er ist ein von einem Modell ausgefülltes Formular.
### Kontoauszug einlesen
Das Online-Banking bietet beides zum Herunterladen an, meist unter
„Umsatzexport“:
| Format | Datei | Was gelesen wird |
|---|---|---|
| CAMT.053 | `.xml`, `.camt` | Datum, Richtung, Betrag, Verwendungszweck, Gegenseite, Bankkennung |
| MT940 | `.txt`, `.sta` | dasselbe, aus den Feldern `:61:` und `:86:` |
Erkannt wird am Inhalt, nicht an der Dateiendung. Es entstehen **Entwürfe,
keine fertigen Buchungen** — durchsehen muss sie der Kassier.
Jeder Umsatz trägt eine Kennung: die der Bank, sonst eine aus Datum, Betrag
und Zweck gebildete. Derselbe Auszug zweimal eingelesen wird dadurch als
solcher erkannt und in der Liste als „steht schon in der Kasse“ ausgewiesen.
Ohne das wäre es ein doppelt gebuchter Monat, und der fiele erst beim
Kassenabschluss auf.
Was nicht gelesen werden konnte, steht mit Grund daneben. Ein Parser, der
stillschweigend drei Umsätze verschluckt, ist schlimmer als einer, der gar
nicht erst liest.
### Mitgliedsbeiträge
Sätze gelten ab einem Jahr und je Abteilung; ein Satz ohne Abteilung gilt für
alle übrigen. Rückwirkend geändert wird nichts — sonst stimmten die Beiträge
vergangener Jahre nicht mehr, und die sind bereits geprüft.
Der **Beitragslauf** zeigt vorher, was entstehen wird: wie viele Beiträge,
welche Summe, und wer keinen passenden Satz hat. Der letzte Teil ist der
interessante — das sind die Leute, für die niemand entschieden hat, was sie
zahlen. Der Lauf ist wiederholbar; im Februar noch einmal gestartet legt er
nur die Neuen an.
Ein Zahlungseingang erzeugt zugleich die Buchung. Ein Häkchen ohne Geld in der
Kasse wäre eine Behauptung. Ein Erlass braucht eine Begründung — die Datenbank
lehnt einen ohne ab, weil er in der Kassenprüfung sonst eine offene Frage ist.
Die Rückstandsliste geht über alle Jahre. Sie ist die einzige Ansicht, in der
auffällt, wer seit drei Jahren offen steht.
### Rechnungen an Dritte
Kostenersatz nach Art. 28 BayFwG, Vermietungen, Veranstaltungen. Die Nummer
vergibt FLORIAN fortlaufend je Jahr und **lückenlos**: eine fehlende Nummer
muss in der Kassenprüfung erklärt werden.
Der Betrag ist die Summe der Positionen, gerechnet und nicht eingegeben. Ein
eingetippter Gesamtbetrag neben Positionen, die etwas anderes ergeben, ist der
Fehler, den niemand bemerkt, bis der Empfänger anruft. Die Menge hat drei
Nachkommastellen — Einsatzstunden werden viertelstündlich abgerechnet, und
0,25 × 47,50 muss auf den Cent stimmen.
Bis zum Versenden ist eine Rechnung ein Entwurf, danach unveränderbar: was
beim Empfänger liegt, muss dem entsprechen, was hier steht. Korrigiert wird
über Storno und eine neue Rechnung; die stornierte bleibt mit Grund stehen.
### Kassenprüfung
In der Prüfmaske steht links der Bestand laut Buch und rechts der gezählte.
In dieser Reihenfolge, und die gezählten Felder fangen leer an — wer erst das
Buch sieht und dann zählt, zählt, bis es passt.
Ohne Prüfer und Prüfdatum lässt sich nichts festschreiben: ohne beides ist es
kein Abschluss, sondern eine Notiz. Eine Differenz braucht eine Erläuterung,
sonst steht in fünf Jahren „stimmt nicht“ im Bericht und niemand weiß mehr,
warum.
Der Kassenprüfbericht entsteht als PDF mit vier Unterschriftsfeldern und wird
im Hintergrund erzeugt.
### Auswertungen
Einnahmen und Ausgaben nach Kategorie und Monat, mit dem Vorjahr daneben —
eine Zahl ohne Vergleich sagt nichts. Der Export ist eine CSV-Datei mit
Semikolon und BOM: das ist, was Excel in Deutschland ohne Rückfrage richtig
öffnet. Eine Datei, die beim Doppelklick als eine Spalte voller Semikolons
erscheint, wird zurückgeschickt.
### Wer was sieht
| Recht | Wer | Was |
|---|---|---|
| `kasse.lesen` | Kassier, Kommandant, Vorstand | Buchungen und Auswertungen ansehen |
| `kasse.bearbeiten` | Kassier | Buchungen, Beiträge, Rechnungen erfassen |
| `kasse.abschluss` | Kassier, Kassenprüfer | Kassenabschluss anlegen und festschreiben |
Der Gerätewart sieht keine Buchungen, der Kassier keine Atemschutzdaten. Die
Trennung liegt in `lib/authz.ts`, nicht in den Komponenten.
**Keine zusätzlichen Umgebungsvariablen.** Die Kasse braucht nur, was ohnehin
läuft: Postgres, MinIO für die Belege und Gotenberg für die PDFs.
---
## Befehle
```bash