"""Datenmodell des Abrechnungssystems (siehe UMSETZUNGSPLAN.md §"Data model").""" from __future__ import annotations from decimal import Decimal from django.conf import settings from django.core.validators import MinValueValidator from django.db import models class Firma(models.Model): """Verkäufer / Rechnungssteller (Singleton). Basis für ZUGFeRD-Seller & Kopf.""" name = models.CharField("Firmenname", max_length=200) strasse = models.CharField("Straße & Hausnummer", max_length=200) plz = models.CharField("PLZ", max_length=10) ort = models.CharField("Ort", max_length=100) land = models.CharField("Land (ISO)", max_length=2, default="DE") ust_idnr = models.CharField("USt-IdNr.", max_length=20, blank=True) steuernummer = models.CharField("Steuernummer", max_length=30, blank=True) iban = models.CharField("IBAN", max_length=34, blank=True) bic = models.CharField("BIC", max_length=11, blank=True) bank = models.CharField("Bank", max_length=100, blank=True) email = models.EmailField("E-Mail", blank=True) telefon = models.CharField("Telefon", max_length=40, blank=True) logo = models.ImageField("Logo", upload_to="firma/", blank=True, null=True) ust_befreiungsgrund = models.CharField( "USt-Befreiungsgrund", max_length=255, default="Steuerfrei gemäß § 4 UStG", help_text="Erscheint als Befreiungsgrund auf Rechnung und im ZUGFeRD-XML.", ) class Meta: verbose_name = "Firma" verbose_name_plural = "Firma" def __str__(self) -> str: return self.name class Therapeut(models.Model): """Leistungserbringer. Basis für Leistungsstatistiken.""" name = models.CharField("Name", max_length=150) kuerzel = models.CharField("Kürzel", max_length=10, unique=True) aktiv = models.BooleanField("aktiv", default=True) class Meta: verbose_name = "Therapeut" verbose_name_plural = "Therapeuten" ordering = ["name"] def __str__(self) -> str: return f"{self.name} ({self.kuerzel})" class Auftraggeber(models.Model): """Rechnungsempfänger / Bezahler. → ZUGFeRD-Buyer.""" class Typ(models.TextChoices): AMT = "AMT", "Amt / Behörde" FIRMA = "FIRMA", "Firma" PRIVAT = "PRIVAT", "Privatzahler" name = models.CharField("Name", max_length=200) typ = models.CharField("Typ", max_length=10, choices=Typ.choices, default=Typ.AMT) strasse = models.CharField("Straße & Hausnummer", max_length=200) plz = models.CharField("PLZ", max_length=10) ort = models.CharField("Ort", max_length=100) land = models.CharField("Land (ISO)", max_length=2, default="DE") ust_idnr = models.CharField("USt-IdNr.", max_length=20, blank=True) email = models.EmailField("E-Mail", blank=True) leitweg_id = models.CharField( "Leitweg-ID", max_length=50, blank=True, help_text="Nur für Behörden mit XRechnung-Pflicht.", ) class Meta: verbose_name = "Auftraggeber" verbose_name_plural = "Auftraggeber" ordering = ["name"] def __str__(self) -> str: return self.name class Teilnehmer(models.Model): """Leistungsempfänger (Patient/Klient). Fest einem Auftraggeber zugeordnet.""" name = models.CharField("Name", max_length=200) aktenzeichen = models.CharField("Aktenzeichen", max_length=50, blank=True) geburtsdatum = models.DateField("Geburtsdatum", null=True, blank=True) auftraggeber = models.ForeignKey( Auftraggeber, on_delete=models.PROTECT, related_name="teilnehmer", verbose_name="Auftraggeber", ) therapeut = models.ForeignKey( Therapeut, on_delete=models.PROTECT, related_name="teilnehmer", verbose_name="Zugeordneter Therapeut", ) aktiv = models.BooleanField("aktiv", default=True) class Meta: verbose_name = "Teilnehmer" verbose_name_plural = "Teilnehmer" ordering = ["name"] def __str__(self) -> str: return self.name class Leistungsart(models.Model): """Admin-konfigurierbare Leistungsart (Sitzung, Vorbereitung, Elterngespräch, …).""" bezeichnung = models.CharField("Bezeichnung", max_length=100, unique=True) standard_stundensatz = models.DecimalField( "Standard-Stundensatz (€)", max_digits=8, decimal_places=2, null=True, blank=True, validators=[MinValueValidator(Decimal("0"))], ) aktiv = models.BooleanField("aktiv", default=True) class Meta: verbose_name = "Leistungsart" verbose_name_plural = "Leistungsarten" ordering = ["bezeichnung"] def __str__(self) -> str: return self.bezeichnung class InvoiceLog(models.Model): """Rechnungsjournal: nur Metadaten, keine Dokumente. Lückenloser Nummernkreis.""" class Status(models.TextChoices): GUELTIG = "GUELTIG", "Gültig" STORNIERT = "STORNIERT", "Storniert" class Typ(models.TextChoices): RECHNUNG = "RECHNUNG", "Rechnung" STORNO = "STORNO", "Storno" rechnungsnummer = models.CharField("Rechnungsnummer", max_length=30, unique=True) datum = models.DateField("Rechnungsdatum") auftraggeber = models.ForeignKey( Auftraggeber, on_delete=models.PROTECT, related_name="rechnungen", verbose_name="Auftraggeber", ) gesamtbetrag = models.DecimalField("Gesamtbetrag (€)", max_digits=12, decimal_places=2) status = models.CharField( "Status", max_length=10, choices=Status.choices, default=Status.GUELTIG, ) typ = models.CharField("Typ", max_length=10, choices=Typ.choices, default=Typ.RECHNUNG) storno_von = models.ForeignKey( "self", on_delete=models.PROTECT, null=True, blank=True, related_name="storno_belege", verbose_name="Storno von", ) created_at = models.DateTimeField("erstellt am", auto_now_add=True) created_by = models.ForeignKey( settings.AUTH_USER_MODEL, on_delete=models.PROTECT, null=True, blank=True, related_name="erstellte_rechnungen", verbose_name="erstellt von", ) class Meta: verbose_name = "Rechnungsjournal-Eintrag" verbose_name_plural = "Rechnungsjournal" ordering = ["-datum", "-id"] def __str__(self) -> str: return f"{self.rechnungsnummer} ({self.get_status_display()})" class InvoiceCounter(models.Model): """Zähler-Zeile für die lückenlose Nummernvergabe (siehe services/numbering.py). Genau eine Zeile pro Präfix; wird per select_for_update gesperrt, damit parallele Abrechnungen keine Nummern doppelt oder mit Lücken vergeben. """ praefix = models.CharField("Präfix", max_length=20, unique=True) letzter_wert = models.PositiveIntegerField("letzter vergebener Wert", default=0) class Meta: verbose_name = "Nummernkreis-Zähler" verbose_name_plural = "Nummernkreis-Zähler" def __str__(self) -> str: return f"{self.praefix}: {self.letzter_wert}" class Leistung(models.Model): """Erbrachte Leistung (Sitzung, Vorbereitung, Elterngespräch, Beratung …). Umbenannt von TherapySession. Auftraggeber wird über den Teilnehmer abgeleitet. """ therapeut = models.ForeignKey( Therapeut, on_delete=models.PROTECT, related_name="leistungen", verbose_name="Therapeut", ) teilnehmer = models.ForeignKey( Teilnehmer, on_delete=models.PROTECT, related_name="leistungen", verbose_name="Teilnehmer", ) leistungsart = models.ForeignKey( Leistungsart, on_delete=models.PROTECT, related_name="leistungen", verbose_name="Leistungsart", ) datum = models.DateField("Datum") stundenanzahl = models.DecimalField( "Stunden", max_digits=6, decimal_places=2, validators=[MinValueValidator(Decimal("0"))], ) stundensatz = models.DecimalField( "Stundensatz (€)", max_digits=8, decimal_places=2, validators=[MinValueValidator(Decimal("0"))], ) beschreibung = models.CharField("Beschreibung", max_length=255, blank=True) abgerechnet_mit = models.ForeignKey( InvoiceLog, on_delete=models.PROTECT, null=True, blank=True, related_name="leistungen", verbose_name="abgerechnet mit", ) class Meta: verbose_name = "Leistung" verbose_name_plural = "Leistungen" ordering = ["datum", "id"] indexes = [ models.Index(fields=["therapeut", "datum"]), models.Index(fields=["abgerechnet_mit"]), ] def __str__(self) -> str: return f"{self.datum} · {self.teilnehmer} · {self.leistungsart}" @property def betrag(self) -> Decimal: """Zeilensumme = Stunden × Stundensatz.""" return (self.stundenanzahl or Decimal("0")) * (self.stundensatz or Decimal("0")) @property def auftraggeber(self) -> Auftraggeber: """Abgeleitet über den Teilnehmer (kein direkter FK).""" return self.teilnehmer.auftraggeber