Files
dashboard/.claude/plans/2026-03-28-buchhaltung-design.md

377 lines
13 KiB
Markdown

# Buchhaltung System — Design
**Date:** 2026-03-28
**Status:** Approved
---
## Overview
A budget and transaction tracking system for the Feuerwehr. Tracks money flows against budget pots and real bank accounts, scoped by fiscal year. Integrates with the existing Bestellungen system and permission matrix.
---
## Full Cycle
```
Budget Plan → Fiscal Year → Freigaben → Bestellungen → Pending Transactions → Booked Transactions
```
---
## 1. Data Model
### `buchhaltung_konto_typen`
Dynamic list of bank account types (Girokonto, Tagesgeldkonto, Handkasse, PayPal, …). Managed in settings by users with `manage_settings`.
| Column | Type | Notes |
|--------|------|-------|
| id | serial PK | |
| name | varchar | |
| beschreibung | text | nullable |
| created_at | timestamptz | |
---
### `buchhaltung_bankkonten`
Real bank accounts (where money physically is).
| Column | Type | Notes |
|--------|------|-------|
| id | serial PK | |
| name | varchar | |
| typ_id | FK konto_typen | |
| iban | varchar | nullable |
| opening_balance | decimal(12,2) | starting balance when first added |
| aktiv | bool | default true |
| created_at | timestamptz | |
Balance = `opening_balance + SUM(booked transactions)`.
---
### `buchhaltung_haushaltsjahre`
Fiscal years. Bank accounts are not year-scoped; budget accounts are.
| Column | Type | Notes |
|--------|------|-------|
| id | serial PK | |
| jahr | int | unique (e.g. 2026) |
| start_datum | date | |
| end_datum | date | |
| abgeschlossen | bool | default false; manually closed by manage_settings user |
Closing a year locks all its transactions (no edits, no new bookings).
---
### `buchhaltung_konten`
Budget accounts / pots. Scoped per fiscal year.
| Column | Type | Notes |
|--------|------|-------|
| id | serial PK | |
| haushaltsjahr_id | FK haushaltsjahre | |
| name | varchar | |
| beschreibung | text | nullable |
| budget | decimal(12,2) | |
| alert_threshold | int | % (0-100); falls back to global setting if null |
| aktiv | bool | default true |
| created_at | timestamptz | |
---
### `buchhaltung_transaktionen`
Core transaction table.
| Column | Type | Notes |
|--------|------|-------|
| id | serial PK | internal DB key |
| haushaltsjahr_id | FK haushaltsjahre | |
| laufende_nummer | int | sequential per fiscal year; display as `2026/0001` |
| datum | date | |
| beschreibung | text | |
| betrag | decimal(12,2) | nullable for variable recurring transactions |
| typ | enum | `einnahme`, `ausgabe`, `transfer` |
| konto_id | FK konten | nullable (null for transfers) |
| bankkonto_id | FK bankkonten | source bank account |
| transfer_ziel_bankkonto_id | FK bankkonten | nullable; only for typ=transfer |
| bestellung_id | FK bestellungen | nullable; set when auto-created from order |
| split_gruppe_id | uuid | nullable; groups split transactions from same Bestellung |
| gebucht | bool | false = pending, true = booked |
| erstellt_von | FK users | |
| gebucht_von | FK users | nullable |
| gebucht_am | timestamptz | nullable |
| created_at | timestamptz | |
Display ID: `${jahr}/${laufende_nummer.toString().padStart(4, '0')}` e.g. `2026/0001`.
`laufende_nummer` is unique per `haushaltsjahr_id`, incremented per new transaction.
---
### `buchhaltung_belege`
Receipt/invoice attachments per transaction.
| Column | Type | Notes |
|--------|------|-------|
| id | serial PK | |
| transaktion_id | FK transaktionen | |
| dateiname | varchar | |
| pfad | varchar | disk path |
| mime_type | varchar | |
| erstellt_von | FK users | |
| created_at | timestamptz | |
Uses same multer disk storage pattern as Bestellungen (`uploads/buchhaltung/`).
---
### `buchhaltung_wiederkehrend`
Recurring transaction templates.
| Column | Type | Notes |
|--------|------|-------|
| id | serial PK | |
| beschreibung | varchar | |
| betrag | decimal(12,2) | nullable (e.g. variable energy bills) |
| typ | enum | `einnahme`, `ausgabe` |
| konto_id | FK konten | nullable |
| bankkonto_id | FK bankkonten | nullable |
| frequenz | enum | `monatlich`, `vierteljaehrlich`, `jaehrlich` |
| naechste_faelligkeit | date | advanced by job after each creation |
| aktiv | bool | |
| erstellt_von | FK users | |
Job runs daily → creates pending transaction → advances `naechste_faelligkeit`.
Betrag null → pending transaction has null betrag → Kassenwart fills it in before booking.
---
### `buchhaltung_freigaben`
Budget delegation — authorizes a user to spend against a budget account.
| Column | Type | Notes |
|--------|------|-------|
| id | serial PK | |
| konto_id | FK konten | budget account |
| user_id | FK users | authorized user |
| beschreibung | text | what this covers |
| betrag_limit | decimal(12,2) | nullable; max spending allowed |
| planposition_id | FK planpositionen | nullable; source plan item |
| aktiv | bool | |
| erstellt_von | FK users | |
| created_at | timestamptz | |
Amount is editable after creation (plan may change mid-year).
Users with a Freigabe can create Bestellungen against that account even without `buchhaltung:create`.
---
### `buchhaltung_audit`
Append-only audit log.
| Column | Type | Notes |
|--------|------|-------|
| id | serial PK | |
| transaktion_id | FK transaktionen | |
| aktion | enum | `erstellt`, `gebucht`, `bearbeitet`, `geloescht` |
| benutzer_id | FK users | |
| details | jsonb | before/after snapshot |
| created_at | timestamptz | |
---
### `buchhaltung_einstellungen`
Key/value store for global settings.
| Key | Default | Notes |
|-----|---------|-------|
| `default_alert_threshold` | `80` | % budget used before notification |
| `pdf_footer` | `''` | reuses PDF settings pattern |
---
### Budget Planning Tables
#### `buchhaltung_planung`
A plan for a future fiscal year.
| Column | Type | Notes |
|--------|------|-------|
| id | serial PK | |
| ziel_jahr | int | the year being planned |
| name | varchar | e.g. "Haushaltsplan 2027" |
| status | enum | `entwurf`, `genehmigt` |
| erstellt_von | FK users | |
| created_at | timestamptz | |
Starts pre-populated from current open budget accounts. New accounts can be added.
Approving locks the plan. Only approved plans can be used for fiscal year rollover.
#### `buchhaltung_plankonten`
Planned budget pots within a plan.
| Column | Type | Notes |
|--------|------|-------|
| id | serial PK | |
| planung_id | FK planung | |
| konto_referenz_id | FK konten | nullable; links to existing account if carried over |
| name | varchar | |
| beschreibung | text | nullable |
| sort_order | int | |
#### `buchhaltung_planpositionen`
Line items within a planned pot. Sum = pot total.
| Column | Type | Notes |
|--------|------|-------|
| id | serial PK | |
| plankonto_id | FK plankonten | |
| beschreibung | text | what it's for |
| betrag | decimal(12,2) | |
| sort_order | int | |
From approved plan items → manual Freigaben creation flow (UI, not automatic). Amount editable on Freigabe creation and after.
---
## 2. Permissions
New `buchhaltung` feature group in the existing permission matrix:
| Permission | Description |
|------------|-------------|
| `buchhaltung:view` | See transactions, accounts, bank accounts |
| `buchhaltung:create` | Add transactions manually |
| `buchhaltung:edit` | Edit unbooked transactions |
| `buchhaltung:delete` | Delete unbooked transactions |
| `buchhaltung:manage_accounts` | Create/edit/archive budget accounts, bank accounts, Freigaben |
| `buchhaltung:manage_settings` | Global settings, account types, fiscal year management, recurring templates |
| `buchhaltung:export` | PDF and CSV export |
| `buchhaltung:widget` | See the dashboard widget |
`manage_settings``dashboard_admin` — a dedicated Kassenwart can have this without being a global admin.
---
## 3. Backend Architecture
Follows the existing project pattern (service → controller → routes → app.ts).
**Routes** mounted at `/api/buchhaltung/`:
| Route | Description |
|-------|-------------|
| `bankkonten` | CRUD bank accounts |
| `konten` | CRUD budget accounts (scoped by fiscal year) |
| `transaktionen` | list, create, book, edit, delete |
| `transfers` | create bank-to-bank transfers |
| `freigaben` | CRUD budget delegations |
| `wiederkehrend` | CRUD recurring templates |
| `haushaltsjahre` | create new year, close year, list |
| `planung` | CRUD budget plans and plan items |
| `einstellungen` | key/value settings |
| `export/pdf` | annual report PDF |
| `export/csv` | transaction CSV export |
| `audit/:transaktionId` | audit trail for one transaction |
**Jobs:**
- `buchhaltung-recurring.job.ts` — runs daily; creates pending transactions from due recurring templates; advances `naechste_faelligkeit`
- Budget alert fires **inline** after each booking: recalculates account total → if crosses threshold → `notificationService.createNotification()` for `manage_accounts` users (deduped by DB constraint)
---
## 4. Bestellungen Integration
### Costs on positions
- `bestellpositionen` gets an `einzelpreis` / `gesamtpreis` field (if not already present)
- Status change to `vollstaendig`/`abgeschlossen` is **blocked** until all positions have costs entered
- Attempting the status change without costs → `showWarning` snackbar: *"Bestellung kann erst abgeschlossen werden, wenn Kosten für alle Positionen eingetragen wurden."*
- Backend enforces the same rule (400 response)
- The **order creator/manager** enters costs — not the Kassenwart
### Pending transaction creation
When a Bestellung reaches `abgeschlossen`:
1. `buchhaltungService.createPendingFromBestellung(bestellung)` is called
2. Creates a pending `ausgabe` transaction: betrag = sum of position costs, beschreibung = order title, bestellung_id = FK
3. Notifies users with `buchhaltung:create` or `buchhaltung:manage_accounts`
### Split booking
When booking a pending transaction from a Bestellung, the Kassenwart can split it into N parts:
- Each part: betrag, budget konto, bankkonto, Beleg (receipt)
- Parts sum to ≤ total (partial booking allowed; remainder stays pending)
- Each part becomes its own booked transaction with its own `<year>/<id>`
- All parts share the same `bestellung_id` and `split_gruppe_id` (visually grouped in audit trail)
---
## 5. Frontend Pages
### Navigation
New "Buchhaltung" sidebar group:
- Übersicht
- Transaktionen
- Bankkonten
- Konten & Haushaltsjahre
- Freigaben *(only with `manage_accounts`)*
- Haushaltsplan *(only with `manage_settings`)*
### Pages
**`Buchhaltung.tsx`** — overview. Year selector at top. Budget account cards: name, budget, spent, remaining, % bar, alert indicator. Pending transactions count with link.
**`Transaktionen.tsx`** — full list. Filter by year / account / bank account / typ. Columns: ID (`2026/0001`), Datum, Beschreibung, Konto, Bankkonto, Betrag, Status. Click → booking dialog.
**`BankkontoDetail.tsx`** — statement view. Running balance column per row. Date range filter. CSV/PDF export.
**`BuchhaltungKonten.tsx`** — tabs: Konten (current year) | Bankkonten | Haushaltsjahre. Year-end actions: "Jahr abschließen" (manual, locks year) + "Neues Haushaltsjahr" (from approved plan or copy previous).
**`BuchhaltungEinstellungen.tsx`** (or AdminDashboard tab) — Kontentypen list, default alert threshold, recurring templates.
**`Haushaltsplan.tsx`** — plan management. Pre-populated from current accounts. Add/edit/remove plankonten and planpositionen. Approve plan. From approved items → create Freigaben (manual flow, amounts editable).
### Booking Dialog
Used from pending transaction list:
- Pre-filled from Bestellung (amount, description, reference)
- "Split" button adds a row
- Each row: betrag, budget konto, bankkonto, Beleg upload
- Sum validation
- Confirm → books all rows at once
### Widget
`BuchhaltungWidget.tsx` — compact card in Status group. Shows pending transaction count. Tap → filtered pending list. Only shown with `buchhaltung:widget`.
---
## 6. PDF Export
Uses existing `addPdfHeader` / `addPdfFooter` from `frontend/src/utils/pdfExport` (same red header, logo, org name as Bestellungen and Kalender).
Content of annual report TBD (separate discussion).
---
## 7. Fiscal Year Rollover
1. **Close current year** — manual action by `manage_settings` user → sets `abgeschlossen = true` → all transactions locked
2. **Create new year** — choose source:
- **From approved plan** — plankonten become new konten, plankonto totals (sum of planpositionen) become budgets
- **Copy previous year** — all active konten copied with same budget amounts (editable before confirming)
3. Bank accounts carry over automatically (not year-scoped)
4. Old year remains readable/exportable forever
---
## 8. Future: Sub-Budget Flow from Plan Items
*(Planned feature — not in initial implementation)*
From an approved plan's items, a manual UI flow to create Freigaben:
- User selects plan items
- Sets/adjusts amount per Freigabe (editable at creation and later — plan may change mid-year)
- Assigns a user
- Creates the Freigabe linked to `planposition_id`
This closes the full loop: Plan item → Freigabe → Bestellung → Pending transaction → Booked transaction.