Files
rechnungsgenerator/UMSETZUNGSPLAN.md
Matthias Mintert 7b60be2933 Add architecture spec and implementation plan
Transcribe the original architecture PDF into SPEZIFIKATION.md and add
UMSETZUNGSPLAN.md with the refined implementation plan (ZUGFeRD on-the-fly
invoicing, uv + Docker Compose, grouped Leistungserfassung).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-11 00:22:16 +02:00

140 lines
8.5 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# Umsetzungsplan: Django ZUGFeRD Abrechnungssystem (Therapie-/Sozialdienste)
> Ursprüngliche Spezifikation: [`SPEZIFIKATION.md`](./SPEZIFIKATION.md).
> Dieser Plan verfeinert die Spezifikation um die im Interview getroffenen
> Entscheidungen und ist die Grundlage für die Implementierung.
## Context
Greenfield-Projekt. Ziel laut [`SPEZIFIKATION.md`](./SPEZIFIKATION.md): eine Django-Web-App,
die Auftraggeber, Therapeuten, Teilnehmer und erbrachte Leistungen verwaltet und daraus
**monatliche deutsche E-Rechnungen on-the-fly** als **ZUGFeRD**-Dokumente erzeugt (PDF/A-3
mit eingebettetem EN-16931-CII-XML). Zentraler rechtlicher Treiber: keine fertigen
Rechnungsdokumente werden auf dem Server gespeichert (Minimierung der Entwicklerhaftung);
nur ein Metadaten-Journal (`InvoiceLog`) hält einen lückenlosen Nummernkreis und die
Stornohistorie. Dokumente werden dem Nutzer als Download gestreamt.
**Bestätigte Entscheidungen (aus dem Interview):**
- USt: **USt-frei nach §4 UStG** → CII-Steuerkategorie `E`, 0%, mit Befreiungsgrund-Text.
- Rechnungslayout: **sauberes, an DIN 5008 orientiertes Template** mit Platzhalter-Logo/
Firmendaten.
- Umfang: **vollständiges End-to-End-MVP**.
- Auth: **einfacher Django-Login**, wenige interne Nutzer.
## Tech stack
- Django 5.2 LTS, PostgreSQL, Settings über `django-environ`.
- **Packaging & Runtime: `uv`** (`pyproject.toml` + `uv.lock`, `uv sync` / `uv run`).
- **Containerisiert: Docker Compose** (`web` + `db` Postgres).
- **WeasyPrint** (HTML/CSS → PDF, `pdf_variant='pdf/a-3b'`).
- **drafthorse** (EN-16931-CII-XML bauen) + **factur-x** (XML einbetten → ZUGFeRD PDF/A-3).
- django-crispy-forms + crispy-bootstrap5, django-tables2 (lt. Spezifikation).
- pytest-django für Tests.
- ⚠️ Python-Version in `pyproject.toml` auf **3.13** pinnen (Django 5.2/WeasyPrint auf
3.12/3.13 validiert; Host hat 3.14). `uv` stellt 3.13 unabhängig vom Host bereit und
umgeht so das Kompatibilitätsrisiko.
## Docker & uv
- **`pyproject.toml`** deklariert alle Deps; `uv.lock` pinnt sie; `.python-version` → 3.13.
- **`Dockerfile`** (multi-stage): Builder installiert Deps via `uv sync --frozen`; Runtime
ist ein schlankes Python-3.13-Image plus WeasyPrint-Systembibliotheken (`libpango-1.0-0`,
`libpangocairo-1.0-0`, `libcairo2`, `libgdk-pixbuf-2.0-0`, Fonts). Start via
`uv run gunicorn`. `.dockerignore` schließt venv/pyc/git aus.
- **`docker-compose.yml`**: `db` (postgres:16, named volume) + `web` (build ., depends_on
db, `.env` für Secrets/DB-URL, Port 8000). `entrypoint.sh` wartet auf DB → `migrate`
`collectstatic` → startet gunicorn (bzw. `runserver` im Dev-Override).
- **`docker-compose.override.yml`** (Dev): Source bind-mount, `runserver`, DEBUG=1.
## Project structure (single app for simplicity)
```
manage.py, pyproject.toml, uv.lock, .python-version, .env(.example)
Dockerfile, .dockerignore, docker-compose.yml, docker-compose.override.yml, entrypoint.sh
config/ settings.py, urls.py, wsgi.py
abrechnung/
models.py admin.py forms.py
views.py # entry, invoicing, storno, dashboard views
services/numbering.py # gapless invoice-number allocation
services/invoicing.py # on-the-fly generation + storno orchestration
services/zugferd.py # HTML->PDF/A-3 + CII XML build + embed
templates/abrechnung/ # invoice.html (WeasyPrint) + UI pages
tests/
templates/base.html static/
```
## Data model (`abrechnung/models.py`)
- **Firma** (seller singleton): name, Anschrift, USt-IdNr/Steuernr, IBAN/BIC, Kontakt,
Logo, `ust_befreiungsgrund` (default §4-text). Used for ZUGFeRD Seller + header.
- **Auftraggeber** (buyer/payer): name, Anschrift, Typ (Amt/Firma/Privat), optional
USt-IdNr, E-Mail, optional Leitweg-ID. → ZUGFeRD Buyer.
- **Teilnehmer**: Name, optional Aktenzeichen/Geburtsdatum, FK → Auftraggeber,
**FK → Therapeut (zugeordneter Therapeut)**. Each Teilnehmer has one assigned
therapist; drives the grouped capture flow and prefills `Leistung.therapeut`.
- **Therapeut**: Name, Kürzel, `aktiv`.
- **Leistungsart** (admin-configurable): `bezeichnung` (z.B. Sitzung, Vorbereitung,
Elterngespräch, Beratung), optional `standard_stundensatz`, `aktiv`. Used to prefill
rates and to group statistics.
- **Leistung** (renamed from `TherapySession` — covers sessions *and* preparation,
Elterngespräche, Beratung etc.): FK Therapeut, FK Teilnehmer, FK Leistungsart,
`datum`, `stundenanzahl` (Decimal), `stundensatz` (Decimal, prefilled from
Leistungsart), optional Beschreibung, `abgerechnet_mit` (FK InvoiceLog, null) =
billing link. Auftraggeber derived via Teilnehmer.
- **InvoiceLog** (journal, no documents): `rechnungsnummer` (unique), `datum`,
FK Auftraggeber, `gesamtbetrag`, `status` (GÜLTIG/STORNIERT), `typ`
(RECHNUNG/STORNO), `storno_von` (self-FK), `created_at`, `created_by`.
## Core processes
1. **Leistungserfassung — grouped by Therapeut → Monatsblatt pro Teilnehmer (Spez. 4.1)**
— the primary Excel-like capture view. **Higher level:** select **Therapeut + Monat**.
The page then renders one **collapsible Monatsblatt section per Teilnehmer** assigned
to that Therapeut. Each section is a spreadsheet-style, row-per-entry editable table
(`modelformset_factory(Leistung)`) with columns **Datum · Leistungsart · Stunden ·
Satz · Beschreibung** (Therapeut is the group context, so no per-row Therapeut column;
`Leistung.therapeut` is set from the section). Stundensatz prefilled from the chosen
Leistungsart; add/remove rows, keyboard-friendly cell nav, live per-Teilnehmer and
per-Therapeut monthly totals. Existing entries for that Therapeut+Monat pre-load for
editing. Lightweight formset + minimal JS (row cloning, tab/enter nav, total calc);
no heavy grid dependency.
2. **Rechnungslegung (on-the-fly, Spez. 4.2)**`services/invoicing.py` in a single
`transaction.atomic`: filter unbilled Leistungen of an Auftraggeber → allocate next
number (`numbering.py`, `select_for_update` on a counter row → guaranteed gapless)
→ create `InvoiceLog` → set `abgerechnet_mit` on Leistungen → `zugferd.py` renders
`invoice.html` via WeasyPrint to PDF/A-3b, drafthorse builds CII XML (category `E`,
0%, exemption reason), factur-x embeds it → `FileResponse` download, **nothing
persisted to disk**.
3. **Storno/Korrektur (Spez. 4.3)** — set original `status=STORNIERT`; create STORNO
`InvoiceLog` (new number, references original, negative total) + generate its
ZUGFeRD doc; release Leistungen (`abgerechnet_mit=NULL`) so a corrected regular
invoice can be re-billed.
4. **Dashboard (Spez. 5)** — ORM aggregation over `Leistung`
(`annotate`/`Sum` of hours & hours×rate) grouped **by Auftraggeber / Teilnehmer /
Therapeut** (+ optional by Leistungsart), with month filter; rendered via
django-tables2.
## Auth
Django built-in auth, `LoginRequiredMixin` on all views, a login template; users
created via admin/superuser. No custom roles for MVP.
## Files to create (primary)
`config/settings.py`, `config/urls.py`; `abrechnung/models.py`, `admin.py`, `forms.py`,
`views.py`, `services/{numbering,invoicing,zugferd}.py`,
`templates/abrechnung/invoice.html` + UI templates (incl. `monatsblatt.html` capture
grid), `templates/base.html`, `static/abrechnung/monatsblatt.js` (row cloning / cell
nav / live totals), `abrechnung/tests/*`, `pyproject.toml` + `uv.lock`, `.env.example`,
`Dockerfile`, `docker-compose.yml`(+override), `entrypoint.sh`, `.dockerignore`.
## Verification
- `pytest` covering: gapless numbering under concurrency (`select_for_update`),
invoice generation returns a PDF whose embedded XML parses as valid EN 16931 CII
(category E, exemption reason present, totals match `InvoiceLog`), storno releases
Leistungen & writes correct journal entries, dashboard aggregation numbers.
- `docker compose up` builds and runs cleanly (web + db); entrypoint migrates.
- Manual E2E: create superuser + Firma/Auftraggeber/Therapeut/Teilnehmer (assigned to a
Therapeut)/Leistungsart → open Leistungserfassung for that **Therapeut + Monat**
capture Leistungen across their Teilnehmer sections (grid) → generate invoice →
download PDF → validate ZUGFeRD (`facturx-check` / mustangproject) → run storno →
confirm Leistungen freed → open dashboard.
## Assumptions (flag if wrong)
- Single-tenant (one billing Firma). Invoice currency EUR. All MVP services USt-frei
(no mixed-rate invoices). ZUGFeRD profile: EN 16931 (COMFORT).
- Each Teilnehmer has exactly one assigned Therapeut; a Leistung's therapist is that
assigned Therapeut (no per-row substitution in the MVP grid).