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>
8.5 KiB
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+dbPostgres). - 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.tomlauf 3.13 pinnen (Django 5.2/WeasyPrint auf 3.12/3.13 validiert; Host hat 3.14).uvstellt 3.13 unabhängig vom Host bereit und umgeht so das Kompatibilitätsrisiko.
Docker & uv
pyproject.tomldeklariert alle Deps;uv.lockpinnt sie;.python-version→ 3.13.Dockerfile(multi-stage): Builder installiert Deps viauv 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 viauv run gunicorn..dockerignoreschließt venv/pyc/git aus.docker-compose.yml:db(postgres:16, named volume) +web(build ., depends_on db,.envfür Secrets/DB-URL, Port 8000).entrypoint.shwartet auf DB →migrate→collectstatic→ startet gunicorn (bzw.runserverim 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), optionalstandard_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
- 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.therapeutis 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. - Rechnungslegung (on-the-fly, Spez. 4.2) —
services/invoicing.pyin a singletransaction.atomic: filter unbilled Leistungen of an Auftraggeber → allocate next number (numbering.py,select_for_updateon a counter row → guaranteed gapless) → createInvoiceLog→ setabgerechnet_miton Leistungen →zugferd.pyrendersinvoice.htmlvia WeasyPrint to PDF/A-3b, drafthorse builds CII XML (categoryE, 0%, exemption reason), factur-x embeds it →FileResponsedownload, nothing persisted to disk. - Storno/Korrektur (Spez. 4.3) — set original
status=STORNIERT; create STORNOInvoiceLog(new number, references original, negative total) + generate its ZUGFeRD doc; release Leistungen (abgerechnet_mit=NULL) so a corrected regular invoice can be re-billed. - Dashboard (Spez. 5) — ORM aggregation over
Leistung(annotate/Sumof 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
pytestcovering: 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 matchInvoiceLog), storno releases Leistungen & writes correct journal entries, dashboard aggregation numbers.docker compose upbuilds 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).