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:
co-authored by
Claude Opus 5
parent
d70acad4cd
commit
4165334c5a
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user