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

8.5 KiB
Raw Blame History

Umsetzungsplan: Django ZUGFeRD Abrechnungssystem (Therapie-/Sozialdienste)

Ursprüngliche Spezifikation: 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: 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 → migratecollectstatic → 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).