import uuid import csv from io import StringIO from django.db import models from django.core.validators import MinValueValidator, MaxValueValidator from django.utils import timezone from dateutil.relativedelta import relativedelta from stiftung.utils.date_utils import ensure_date, get_year_from_date class CSVImport(models.Model): """Track CSV import operations for audit purposes""" IMPORT_TYPE_CHOICES = [ ('destinataere', 'Destinatäre'), ('paechter', 'Pächter'), ('laendereien', 'Ländereien'), ('verpachtungen', 'Verpachtungen'), ('personen', 'Personen (Legacy)'), ] STATUS_CHOICES = [ ('pending', 'Ausstehend'), ('processing', 'Wird verarbeitet'), ('completed', 'Abgeschlossen'), ('failed', 'Fehlgeschlagen'), ('partial', 'Teilweise erfolgreich'), ] id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False) import_type = models.CharField(max_length=20, choices=IMPORT_TYPE_CHOICES, verbose_name="Import-Typ") filename = models.CharField(max_length=255, verbose_name="Dateiname") file_size = models.IntegerField(verbose_name="Dateigröße (Bytes)") status = models.CharField(max_length=20, choices=STATUS_CHOICES, default='pending') # Results total_rows = models.IntegerField(default=0, verbose_name="Gesamtzeilen") imported_rows = models.IntegerField(default=0, verbose_name="Importierte Zeilen") failed_rows = models.IntegerField(default=0, verbose_name="Fehlgeschlagene Zeilen") error_log = models.TextField(null=True, blank=True, verbose_name="Fehlerprotokoll") # Metadata created_by = models.CharField(max_length=100, null=True, blank=True, verbose_name="Erstellt von") started_at = models.DateTimeField(auto_now_add=True, verbose_name="Gestartet um") completed_at = models.DateTimeField(null=True, blank=True, verbose_name="Abgeschlossen um") class Meta: verbose_name = "CSV Import" verbose_name_plural = "CSV Imports" ordering = ['-started_at'] def __str__(self): return f"{self.get_import_type_display()} - {self.filename} ({self.status})" def get_duration(self): """Calculate import duration""" if self.completed_at and self.started_at: return self.completed_at - self.started_at return None def get_success_rate(self): """Calculate success rate percentage""" if self.total_rows > 0: return (self.imported_rows / self.total_rows) * 100 return 0 class Paechter(models.Model): """Pächter (Tenants) für Ländereien und Verpachtungen""" id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False) vorname = models.CharField(max_length=100, verbose_name="Vorname") nachname = models.CharField(max_length=100, verbose_name="Nachname") geburtsdatum = models.DateField(null=True, blank=True, verbose_name="Geburtsdatum") email = models.EmailField(null=True, blank=True, verbose_name="E-Mail") telefon = models.CharField(max_length=20, null=True, blank=True, verbose_name="Telefon") iban = models.CharField(max_length=34, null=True, blank=True, verbose_name="IBAN") # Adressfelder strasse = models.CharField(max_length=200, verbose_name="Straße", blank=True, null=True) plz = models.CharField(max_length=10, verbose_name="PLZ", blank=True, null=True) ort = models.CharField(max_length=100, verbose_name="Ort", blank=True, null=True) # Typ des Pächters PERSONENTYP_CHOICES = [ ('natuerlich', 'Natürliche Person'), ('gesellschaft', 'Gesellschaft (GmbH, KG, etc.)'), ] personentyp = models.CharField( max_length=20, choices=PERSONENTYP_CHOICES, default='natuerlich', verbose_name="Typ des Pächters" ) # Pacht-spezifische Felder pachtnummer = models.CharField(max_length=50, null=True, blank=True, verbose_name="Pachtnummer") pachtbeginn_erste = models.DateField(null=True, blank=True, verbose_name="Erster Pachtbeginn") pachtende_letzte = models.DateField(null=True, blank=True, verbose_name="Letztes Pachtende") pachtzins_aktuell = models.DecimalField( max_digits=12, decimal_places=2, null=True, blank=True, verbose_name="Aktueller Pachtzins (€/Jahr)" ) # Landwirtschaftliche Informationen landwirtschaftliche_ausbildung = models.BooleanField(default=False, verbose_name="Landwirtschaftliche Ausbildung") berufserfahrung_jahre = models.IntegerField(null=True, blank=True, verbose_name="Berufserfahrung (Jahre)") spezialisierung = models.CharField(max_length=100, null=True, blank=True, verbose_name="Spezialisierung") # Kontakt und Notizen notizen = models.TextField(null=True, blank=True, verbose_name="Notizen") aktiv = models.BooleanField(default=True, verbose_name="Aktiv") class Meta: verbose_name = "Pächter" verbose_name_plural = "Pächter" ordering = ['nachname', 'vorname'] def __str__(self): if self.vorname: return f"{self.nachname}, {self.vorname}" else: return self.nachname def get_full_name(self): if self.vorname: return f"{self.vorname} {self.nachname}" else: return self.nachname def get_aktive_verpachtungen(self): """Get all active leases for this tenant""" return self.neue_verpachtungen.filter(status='aktiv') def get_gesamt_pachtflaeche(self): """Calculate total leased area""" return self.neue_verpachtungen.filter(status='aktiv').aggregate( total=models.Sum('verpachtete_flaeche') )['total'] or 0 def get_gesamt_pachtzins(self): """Calculate total annual rent""" return self.neue_verpachtungen.filter(status='aktiv').aggregate( total=models.Sum('pachtzins_pauschal') )['total'] or 0 class Destinataer(models.Model): """Destinatäre (Beneficiaries) für Förderungen""" FAMILIENZWIG_CHOICES = [ ('hauptzweig', 'Hauptzweig'), ('nebenzweig', 'Nebenzweig'), ('verwandt', 'Verwandt'), ('anderer', 'Anderer'), ] BERUFSGRUPPE_CHOICES = [ ('student', 'Student/Studentin'), ('wissenschaftler', 'Wissenschaftler/in'), ('künstler', 'Künstler/in'), ('sozialarbeiter', 'Sozialarbeiter/in'), ('umweltschützer', 'Umweltschützer/in'), ('andere', 'Andere'), ] id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False) familienzweig = models.CharField(max_length=100, choices=FAMILIENZWIG_CHOICES, default='hauptzweig') vorname = models.CharField(max_length=100, verbose_name="Vorname") nachname = models.CharField(max_length=100, verbose_name="Nachname") geburtsdatum = models.DateField(null=True, blank=True, verbose_name="Geburtsdatum") email = models.EmailField(null=True, blank=True, verbose_name="E-Mail") telefon = models.CharField(max_length=20, null=True, blank=True, verbose_name="Telefon") iban = models.CharField(max_length=34, null=True, blank=True, verbose_name="IBAN") # Adressfelder strasse = models.CharField(max_length=200, verbose_name="Straße", blank=True, null=True) plz = models.CharField(max_length=10, verbose_name="PLZ", blank=True, null=True) ort = models.CharField(max_length=100, verbose_name="Ort", blank=True, null=True) # Förderungs-spezifische Felder berufsgruppe = models.CharField(max_length=20, choices=BERUFSGRUPPE_CHOICES, default='andere', verbose_name="Berufsgruppe") ausbildungsstand = models.CharField(max_length=100, null=True, blank=True, verbose_name="Ausbildungsstand") institution = models.CharField(max_length=200, null=True, blank=True, verbose_name="Institution/Organisation") projekt_beschreibung = models.TextField(null=True, blank=True, verbose_name="Projektbeschreibung") # Finanzielle Informationen jaehrliches_einkommen = models.DecimalField( max_digits=12, decimal_places=2, null=True, blank=True, verbose_name="Jährliches Einkommen (€)" ) finanzielle_notlage = models.BooleanField(default=False, verbose_name="Finanzielle Notlage") # Kontakt und Notizen notizen = models.TextField(null=True, blank=True, verbose_name="Notizen") aktiv = models.BooleanField(default=True, verbose_name="Aktiv") # Unterstützung – Prüf- und Verwaltungsfelder ist_abkoemmling = models.BooleanField(default=False, verbose_name="Abkömmling gem. Satzung") haushaltsgroesse = models.PositiveIntegerField(default=1, verbose_name="Haushaltsgröße") monatliche_bezuege = models.DecimalField(max_digits=10, decimal_places=2, null=True, blank=True, verbose_name="Monatliche Bezüge (€)") vermoegen = models.DecimalField(max_digits=12, decimal_places=2, null=True, blank=True, verbose_name="Vermögen (€)") unterstuetzung_bestaetigt = models.BooleanField(default=False, verbose_name="Unterstützung bestätigt") standard_konto = models.ForeignKey('StiftungsKonto', null=True, blank=True, on_delete=models.SET_NULL, verbose_name="Standard Auszahlungskonto") vierteljaehrlicher_betrag = models.DecimalField(max_digits=12, decimal_places=2, null=True, blank=True, verbose_name="Vierteljährlicher Betrag (€)") # Studiennachweise studiennachweis_erforderlich = models.BooleanField(default=False, verbose_name="Studiennachweis erforderlich") letzter_studiennachweis = models.DateField(null=True, blank=True, verbose_name="Letzter Studiennachweis") class Meta: verbose_name = "Destinatär" verbose_name_plural = "Destinatäre" ordering = ['nachname', 'vorname'] def __str__(self): if self.vorname: return f"{self.nachname}, {self.vorname}" else: return self.nachname def get_full_name(self): if self.vorname: return f"{self.vorname} {self.nachname}" else: return self.nachname def get_total_foerderungen(self): """Calculate total funding received""" return self.foerderung_set.aggregate(total=models.Sum('betrag'))['total'] or 0 def get_foerderungen_count(self): """Count total funding grants""" return self.foerderung_set.count() def get_letzte_foerderung(self): """Get the most recent funding grant""" return self.foerderung_set.order_by('-jahr', '-betrag').first() def erfuellt_voraussetzungen(self): """Prüft die Unterstützungsvoraussetzungen gemäß Angaben. - Abkömmling muss True sein - Monatliche Bezüge ≤ zulässige Grenze - Vermögen ≤ 15.500 € Die zulässige Grenze wird aus dem Regelsatz (standard 563 €) * 5 für die erste Person und + 0.8 * Regelsatz je weiterer Person approximiert. """ from decimal import Decimal regelsatz = Decimal('563.00') basis = regelsatz * 5 zuschlag = max(0, (self.haushaltsgroesse or 1) - 1) * (regelsatz * Decimal('0.80')) grenze = basis + zuschlag einkommen_ok = (self.monatliche_bezuege or Decimal('0')) <= grenze vermoegen_ok = (self.vermoegen or Decimal('0')) <= Decimal('15500') return bool(self.ist_abkoemmling and einkommen_ok and vermoegen_ok) def naechste_studiennachweis_termine(self): """Gibt die nächsten beiden Stichtage (15.03, 15.09) zurück.""" import datetime as _dt today = _dt.date.today() jahr = today.year maerz = _dt.date(jahr, 3, 15) sep = _dt.date(jahr, 9, 15) termine = [] for d in (maerz, sep): if d >= today: termine.append(d) if len(termine) < 2: # Ergänzen aus folgendem Jahr termine.append(_dt.date(jahr + 1, 3, 15)) if len(termine) < 2: termine.append(_dt.date(jahr + 1, 9, 15)) return termine[:2] # Keep the old Person model for backward compatibility (will be removed in future) class Person(models.Model): FAMILIENZWIG_CHOICES = [ ('hauptzweig', 'Hauptzweig'), ('nebenzweig', 'Nebenzweig'), ('verwandt', 'Verwandt'), ('anderer', 'Anderer'), ] id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False) familienzweig = models.CharField(max_length=100, choices=FAMILIENZWIG_CHOICES, default='hauptzweig') vorname = models.CharField(max_length=100) nachname = models.CharField(max_length=100) geburtsdatum = models.DateField(null=True, blank=True) email = models.EmailField(null=True, blank=True) telefon = models.CharField(max_length=20, null=True, blank=True) iban = models.CharField(max_length=34, null=True, blank=True) adresse = models.TextField(null=True, blank=True) notizen = models.TextField(null=True, blank=True) aktiv = models.BooleanField(default=True) class Meta: verbose_name = "Person (Legacy)" verbose_name_plural = "Personen (Legacy)" ordering = ['nachname', 'vorname'] def __str__(self): return f"{self.nachname}, {self.vorname} (Legacy)" def get_full_name(self): return f"{self.vorname} {self.nachname}" def get_total_foerderungen(self): return self.foerderung_set.aggregate(total=models.Sum('betrag'))['total'] or 0 class Land(models.Model): """Landverwaltung für verpachtete Ländereien""" id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False) # Grundlegende Identifikation lfd_nr = models.CharField(max_length=20, unique=True, verbose_name="Lfd. Nr.") ew_nummer = models.CharField(max_length=50, null=True, blank=True, verbose_name="EW-Nummer") grundbuchblatt = models.CharField(max_length=50, null=True, blank=True, verbose_name="Grundbuchblatt") # Gerichtliche Zuständigkeit amtsgericht = models.CharField(max_length=100, verbose_name="Amtsgericht") # Verwaltungsstruktur gemeinde = models.CharField(max_length=100, verbose_name="Gemeinde") gemarkung = models.CharField(max_length=100, verbose_name="Gemarkung") flur = models.CharField(max_length=50, verbose_name="Flur") flurstueck = models.CharField(max_length=50, verbose_name="Flurstück") adresse = models.CharField(max_length=200, null=True, blank=True, verbose_name="Adresse/Ortsangabe") # Flächenangaben groesse_qm = models.DecimalField( max_digits=12, decimal_places=2, verbose_name="Größe in qm", validators=[MinValueValidator(0.01)] ) # Landnutzung gruenland_qm = models.DecimalField( max_digits=12, decimal_places=2, default=0, verbose_name="Grünland (qm)", validators=[MinValueValidator(0)] ) acker_qm = models.DecimalField( max_digits=12, decimal_places=2, default=0, verbose_name="Acker (qm)", validators=[MinValueValidator(0)] ) wald_qm = models.DecimalField( max_digits=12, decimal_places=2, default=0, verbose_name="Wald (qm)", validators=[MinValueValidator(0)] ) sonstiges_qm = models.DecimalField( max_digits=12, decimal_places=2, default=0, verbose_name="Sonstiges (qm)", validators=[MinValueValidator(0)] ) # Verpachtung (Legacy-Felder für Kompatibilität) verpachtete_gesamtflaeche = models.DecimalField( max_digits=12, decimal_places=2, verbose_name="Verpachtete Gesamtfläche (qm)", validators=[MinValueValidator(0)] ) flaeche_alte_liste = models.DecimalField( max_digits=12, decimal_places=2, null=True, blank=True, verbose_name="Fläche alte Liste (qm)" ) verp_flaeche_aktuell = models.DecimalField( max_digits=12, decimal_places=2, verbose_name="Verp. Fläche aktuell (qm)", validators=[MinValueValidator(0)] ) # Aktuelle Verpachtung (Neue Struktur) aktueller_paechter = models.ForeignKey( 'Paechter', on_delete=models.SET_NULL, null=True, blank=True, verbose_name="Aktueller Pächter", related_name="gepachtete_laendereien" ) paechter_name = models.CharField(max_length=150, null=True, blank=True, verbose_name="Pächter Name") paechter_anschrift = models.TextField(null=True, blank=True, verbose_name="Pächter Anschrift") pachtbeginn = models.DateField(null=True, blank=True, verbose_name="Pachtbeginn") pachtende = models.DateField(null=True, blank=True, verbose_name="Pachtende") verlaengerung_klausel = models.BooleanField(default=False, verbose_name="Automatische Verlängerung") # Pachtzins und Zahlungsweise ZAHLUNGSWEISE_CHOICES = [ ('jaehrlich', 'Jährlich'), ('halbjaehrlich', 'Halbjährlich'), ('vierteljaehrlich', 'Vierteljährlich'), ('monatlich', 'Monatlich'), ] zahlungsweise = models.CharField( max_length=20, choices=ZAHLUNGSWEISE_CHOICES, default='jaehrlich', verbose_name="Zahlungsweise" ) pachtzins_pro_ha = models.DecimalField( max_digits=12, decimal_places=2, null=True, blank=True, verbose_name="Pachtzins pro ha (€)", validators=[MinValueValidator(0)] ) pachtzins_pauschal = models.DecimalField( max_digits=12, decimal_places=2, null=True, blank=True, verbose_name="Pachtzins pauschal/Jahr (€)", validators=[MinValueValidator(0)] ) # Umsatzsteuer ust_option = models.BooleanField(default=False, verbose_name="USt-Option") ust_satz = models.DecimalField( max_digits=4, decimal_places=2, default=19.00, verbose_name="USt-Satz (%)" ) # Umlagen (Durchreichungen) grundsteuer_umlage = models.BooleanField(default=True, verbose_name="Grundsteuer umlagefähig") versicherungen_umlage = models.BooleanField(default=True, verbose_name="Versicherungen umlagefähig") verbandsbeitraege_umlage = models.BooleanField(default=True, verbose_name="Verbandsbeiträge umlagefähig") jagdpacht_anteil_umlage = models.BooleanField(default=False, verbose_name="Jagdpachtanteile umlagefähig") # Steuern und Abgaben anteil_grundsteuer = models.DecimalField( max_digits=8, decimal_places=2, null=True, blank=True, verbose_name="Anteil Grundsteuer (%)" ) anteil_lwk = models.DecimalField( max_digits=8, decimal_places=2, null=True, blank=True, verbose_name="Anteil LWK (%)" ) # Status aktiv = models.BooleanField(default=True, verbose_name="Aktiv") notizen = models.TextField(null=True, blank=True, verbose_name="Ergänzende Kommentare") # Zeitstempel erstellt_am = models.DateTimeField(auto_now_add=True) aktualisiert_am = models.DateTimeField(auto_now=True) class Meta: verbose_name = "Land" verbose_name_plural = "Ländereien" ordering = ['gemeinde', 'gemarkung', 'flur', 'flurstueck'] def __str__(self): return f"{self.gemeinde} - {self.gemarkung} Flur {self.flur} Flurstück {self.flurstueck}" def get_gesamtflaeche(self): """Berechnet die Gesamtfläche aus allen Nutzungsarten""" return (self.gruenland_qm + self.acker_qm + self.wald_qm + self.sonstiges_qm) def get_verpachtungsgrad(self): """Berechnet den Verpachtungsgrad in Prozent""" if self.get_gesamtflaeche() > 0: return (self.get_verpachtete_flaeche_aktuell() / self.get_gesamtflaeche()) * 100 return 0 def get_verpachtete_flaeche_aktuell(self): """Gibt die aktuell verpachtete Fläche zurück (aus neuen Verpachtungen oder Legacy)""" from django.db.models import Sum # Priorität 1: Neue Verpachtungen (LandVerpachtung) neue_total = self.neue_verpachtungen.filter(status='aktiv').aggregate( total=Sum('verpachtete_flaeche') )['total'] or 0 if neue_total > 0: return neue_total # Priorität 2: Einzelverpachtung im Land-Model (verp_flaeche_aktuell) if self.verp_flaeche_aktuell and self.verp_flaeche_aktuell > 0: return self.verp_flaeche_aktuell # No legacy system - return neue_total (could be 0) return neue_total def get_verfuegbare_flaeche(self): """Berechnet die noch verfügbare Fläche für neue Verpachtungen""" return self.groesse_qm - self.get_verpachtete_flaeche_aktuell() def get_verpachtungsgrad_neu(self): """Berechnet den Verpachtungsgrad basierend auf neuen Verpachtungen""" if self.groesse_qm > 0: return (self.get_verpachtete_flaeche_aktuell() / self.groesse_qm) * 100 return 0 def get_steuer_gesamt(self): """Berechnet den Gesamtsteueranteil""" grundsteuer = self.anteil_grundsteuer or 0 lwk = self.anteil_lwk or 0 return grundsteuer + lwk def _qm_to_hektar(self, qm_value): """Hilfsmethode zur Umrechnung von qm in Hektar""" from decimal import Decimal, ROUND_HALF_UP if qm_value and qm_value > 0: # Umrechnung: 1 Hektar = 10.000 qm hektar = Decimal(str(qm_value)) / Decimal('10000') # Runden auf 2 Nachkommastellen return hektar.quantize(Decimal('0.01'), rounding=ROUND_HALF_UP) return Decimal('0.00') @property def groesse_hektar(self): """Berechnet die Gesamtgröße in Hektar""" return self._qm_to_hektar(self.groesse_qm) @property def gruenland_hektar(self): """Berechnet die Grünlandfläche in Hektar""" return self._qm_to_hektar(self.gruenland_qm) @property def acker_hektar(self): """Berechnet die Ackerfläche in Hektar""" return self._qm_to_hektar(self.acker_qm) @property def wald_hektar(self): """Berechnet die Waldfläche in Hektar""" return self._qm_to_hektar(self.wald_qm) @property def sonstiges_hektar(self): """Berechnet die sonstige Fläche in Hektar""" return self._qm_to_hektar(self.sonstiges_qm) @property def verpachtete_gesamtflaeche_hektar(self): """Berechnet die verpachtete Gesamtfläche in Hektar""" return self._qm_to_hektar(self.verpachtete_gesamtflaeche) @property def flaeche_alte_liste_hektar(self): """Berechnet die Fläche aus alter Liste in Hektar""" return self._qm_to_hektar(self.flaeche_alte_liste) @property def verp_flaeche_aktuell_hektar(self): """Berechnet die aktuell verpachtete Fläche in Hektar""" return self._qm_to_hektar(self.verp_flaeche_aktuell) def get_gesamtflaeche_hektar(self): """Berechnet die Gesamtfläche aus allen Nutzungsarten in Hektar""" return self._qm_to_hektar(self.get_gesamtflaeche()) def get_verpachtete_flaeche_aktuell_hektar(self): """Berechnet die aktuell verpachtete Fläche basierend auf aktiven Verpachtungen in Hektar""" return self._qm_to_hektar(self.get_verpachtete_flaeche_aktuell()) class LandVerpachtung(models.Model): """Neue Verpachtungsverträge - mehrere pro Land möglich""" STATUS_CHOICES = [ ('aktiv', 'Aktiv'), ('beendet', 'Beendet'), ('gekuendigt', 'Gekündigt'), ('verlängert', 'Verlängert'), ] id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False) # Grundlegende Verknüpfungen land = models.ForeignKey(Land, on_delete=models.CASCADE, related_name='neue_verpachtungen', verbose_name="Länderei") paechter = models.ForeignKey(Paechter, on_delete=models.CASCADE, related_name='neue_verpachtungen', verbose_name="Pächter") # Vertragsdaten vertragsnummer = models.CharField(max_length=50, unique=True, verbose_name="Vertragsnummer") pachtbeginn = models.DateField(verbose_name="Pachtbeginn") pachtende = models.DateField(null=True, blank=True, verbose_name="Pachtende") verlaengerung_klausel = models.BooleanField(default=False, verbose_name="Automatische Verlängerung") # Flächenangaben verpachtete_flaeche = models.DecimalField( max_digits=12, decimal_places=2, verbose_name="Verpachtete Fläche (qm)", validators=[MinValueValidator(0.01)] ) # Pachtzins pachtzins_pauschal = models.DecimalField( max_digits=12, decimal_places=2, verbose_name="Pachtzins pauschal/Jahr (€)", validators=[MinValueValidator(0)] ) pachtzins_pro_ha = models.DecimalField( max_digits=12, decimal_places=2, null=True, blank=True, verbose_name="Pachtzins pro ha (€)", validators=[MinValueValidator(0)] ) # Zahlungsweise ZAHLUNGSWEISE_CHOICES = [ ('jaehrlich', 'Jährlich'), ('halbjaehrlich', 'Halbjährlich'), ('vierteljaehrlich', 'Vierteljährlich'), ('monatlich', 'Monatlich'), ] zahlungsweise = models.CharField( max_length=20, choices=ZAHLUNGSWEISE_CHOICES, default='jaehrlich', verbose_name="Zahlungsweise" ) # Umsatzsteuer ust_option = models.BooleanField(default=False, verbose_name="USt-Option") ust_satz = models.DecimalField( max_digits=4, decimal_places=2, default=19.00, verbose_name="USt-Satz (%)" ) # Umlagen (Durchreichungen) grundsteuer_umlage = models.BooleanField(default=True, verbose_name="Grundsteuer umlagefähig") versicherungen_umlage = models.BooleanField(default=True, verbose_name="Versicherungen umlagefähig") verbandsbeitraege_umlage = models.BooleanField(default=True, verbose_name="Verbandsbeiträge umlagefähig") jagdpacht_anteil_umlage = models.BooleanField(default=False, verbose_name="Jagdpachtanteile umlagefähig") # Status und Notizen status = models.CharField(max_length=20, choices=STATUS_CHOICES, default='aktiv', verbose_name="Status") bemerkungen = models.TextField(null=True, blank=True, verbose_name="Bemerkungen") # Zeitstempel erstellt_am = models.DateTimeField(auto_now_add=True) aktualisiert_am = models.DateTimeField(auto_now=True) class Meta: verbose_name = "Landverpachtung" verbose_name_plural = "Landverpachtungen" ordering = ['-pachtbeginn', 'land'] def __str__(self): return f"{self.land} - {self.paechter} ({self.vertragsnummer})" @property def verpachtete_flaeche_hektar(self): """Berechnet die verpachtete Fläche in Hektar""" from decimal import Decimal, ROUND_HALF_UP if self.verpachtete_flaeche and self.verpachtete_flaeche > 0: hektar = Decimal(str(self.verpachtete_flaeche)) / Decimal('10000') return hektar.quantize(Decimal('0.01'), rounding=ROUND_HALF_UP) return Decimal('0.00') def is_aktiv(self): """Prüft ob der Vertrag noch aktiv ist""" from datetime import date heute = date.today() pachtbeginn_date = ensure_date(self.pachtbeginn) pachtende_date = ensure_date(self.pachtende) if not pachtbeginn_date: return False if pachtende_date: return pachtbeginn_date <= heute <= pachtende_date return pachtbeginn_date <= heute # Unbefristet def get_restlaufzeit_tage(self): """Berechnet die Restlaufzeit in Tagen""" from datetime import date heute = date.today() pachtende_date = ensure_date(self.pachtende) if pachtende_date and pachtende_date > heute: return (pachtende_date - heute).days return None # Unbefristet @property def ust_pacht_betrag(self): """Berechnet die USt auf Pacht (falls optiert)""" from decimal import Decimal, ROUND_HALF_UP if self.ust_option and self.pachtzins_pauschal: ust_betrag = Decimal(str(self.pachtzins_pauschal)) * Decimal(str(self.ust_satz)) / Decimal('100') return ust_betrag.quantize(Decimal('0.01'), rounding=ROUND_HALF_UP) return Decimal('0.00') def save(self, *args, **kwargs): """Override save to trigger Abrechnung updates""" is_new = self.pk is None old_instance = None if not is_new: try: old_instance = LandVerpachtung.objects.get(pk=self.pk) except LandVerpachtung.DoesNotExist: old_instance = None super().save(*args, **kwargs) # Update Abrechnungen after save self._update_abrechnungen(old_instance, is_new) def _update_abrechnungen(self, old_instance, is_new): """Update LandAbrechnung records when Verpachtung changes""" from datetime import date # Determine affected years years_to_update = set() pachtbeginn_year = get_year_from_date(self.pachtbeginn) if pachtbeginn_year: years_to_update.add(pachtbeginn_year) pachtende_year = get_year_from_date(self.pachtende) if pachtende_year: years_to_update.add(pachtende_year) # If updated, check old dates too if old_instance: old_pachtbeginn_year = get_year_from_date(old_instance.pachtbeginn) if old_pachtbeginn_year: years_to_update.add(old_pachtbeginn_year) old_pachtende_year = get_year_from_date(old_instance.pachtende) if old_pachtende_year: years_to_update.add(old_pachtende_year) # Add current year if contract is active if self.is_aktiv(): years_to_update.add(date.today().year) # Update each affected year for year in years_to_update: self._update_abrechnung_for_year(year, old_instance, is_new) def _update_abrechnung_for_year(self, year, old_instance, is_new): """Update or create LandAbrechnung for specific year""" from decimal import Decimal from datetime import date # Get or create Abrechnung for this year abrechnung, created = LandAbrechnung.objects.get_or_create( land=self.land, abrechnungsjahr=year, defaults={ 'pacht_vereinnahmt': Decimal('0.00'), 'umlagen_vereinnahmt': Decimal('0.00'), 'bemerkungen': f'Automatisch erstellt für {self.vertragsnummer}' } ) # Calculate rent for this year rent_for_year = self._calculate_rent_for_year(year) umlage_for_year = self._calculate_umlage_for_year(year) # Update or add to existing amounts if created or is_new: # New Abrechnung or new Verpachtung abrechnung.pacht_vereinnahmt += rent_for_year abrechnung.umlagen_vereinnahmt += umlage_for_year change_note = f"Neue Verpachtung {self.vertragsnummer} hinzugefügt" else: # Update existing - calculate difference old_rent = old_instance._calculate_rent_for_year(year) if old_instance else Decimal('0.00') old_umlage = old_instance._calculate_umlage_for_year(year) if old_instance else Decimal('0.00') rent_diff = rent_for_year - old_rent umlage_diff = umlage_for_year - old_umlage abrechnung.pacht_vereinnahmt += rent_diff abrechnung.umlagen_vereinnahmt += umlage_diff if rent_diff != 0 or umlage_diff != 0: change_note = f"Verpachtung {self.vertragsnummer} geändert: Pacht {rent_diff:+.2f}€, Umlagen {umlage_diff:+.2f}€" else: change_note = f"Verpachtung {self.vertragsnummer} aktualisiert (keine Betragsänderung)" # Add change tracking to bemerkungen (if significant change) if change_note and ('hinzugefügt' in change_note or 'geändert' in change_note): if abrechnung.bemerkungen: abrechnung.bemerkungen += f"\n[{date.today().strftime('%d.%m.%Y')}] {change_note}" else: abrechnung.bemerkungen = f"[{date.today().strftime('%d.%m.%Y')}] {change_note}" abrechnung.save() def _calculate_rent_for_year(self, year): """Calculate rent amount for specific year""" from decimal import Decimal from datetime import date from django.utils.dateparse import parse_date # Helper function to convert date strings to date objects def ensure_date(date_value): if not date_value: return None if isinstance(date_value, str): return parse_date(date_value) return date_value if not self.pachtzins_pauschal or not self.pachtbeginn: return Decimal('0.00') # Check if contract is active in this year year_start = date(year, 1, 1) year_end = date(year, 12, 31) # Convert dates to ensure they are date objects pachtbeginn_date = ensure_date(self.pachtbeginn) pachtende_date = ensure_date(self.pachtende) if not pachtbeginn_date: return Decimal('0.00') contract_start = max(pachtbeginn_date, year_start) contract_end = min(pachtende_date or year_end, year_end) if contract_start > contract_end: return Decimal('0.00') # No overlap # Calculate proportion of year days_in_year = (year_end - year_start).days + 1 days_active = (contract_end - contract_start).days + 1 proportion = Decimal(str(days_active)) / Decimal(str(days_in_year)) return Decimal(str(self.pachtzins_pauschal)) * proportion def _calculate_umlage_for_year(self, year): """Calculate Umlage amount for specific year based on what can be passed through""" from decimal import Decimal # This would need to be calculated based on actual costs and what's umlagefähig # For now, return 0 - this can be enhanced later with actual cost calculation return Decimal('0.00') def delete(self, *args, **kwargs): """Override delete to update Abrechnungen when Verpachtung is removed""" # Calculate what needs to be removed from Abrechnungen years_to_update = set() pachtbeginn_year = get_year_from_date(self.pachtbeginn) if pachtbeginn_year: years_to_update.add(pachtbeginn_year) pachtende_year = get_year_from_date(self.pachtende) if pachtende_year: years_to_update.add(pachtende_year) # Remove from Abrechnungen before deleting for year in years_to_update: try: abrechnung = LandAbrechnung.objects.get(land=self.land, abrechnungsjahr=year) rent_to_remove = self._calculate_rent_for_year(year) umlage_to_remove = self._calculate_umlage_for_year(year) abrechnung.pacht_vereinnahmt -= rent_to_remove abrechnung.umlagen_vereinnahmt -= umlage_to_remove # Add deletion note from datetime import date change_note = f"Verpachtung {self.vertragsnummer} gelöscht: Pacht -{rent_to_remove:.2f}€, Umlagen -{umlage_to_remove:.2f}€" if abrechnung.bemerkungen: abrechnung.bemerkungen += f"\n[{date.today().strftime('%d.%m.%Y')}] {change_note}" else: abrechnung.bemerkungen = f"[{date.today().strftime('%d.%m.%Y')}] {change_note}" abrechnung.save() except LandAbrechnung.DoesNotExist: pass # No Abrechnung to update super().delete(*args, **kwargs) class LandAbrechnung(models.Model): """Jahresabrechnung für Ländereien""" id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False) land = models.ForeignKey(Land, on_delete=models.CASCADE, related_name='abrechnungen', verbose_name="Länderei") abrechnungsjahr = models.IntegerField(verbose_name="Abrechnungsjahr", validators=[MinValueValidator(2000)]) # Einnahmen pacht_vereinnahmt = models.DecimalField( max_digits=12, decimal_places=2, default=0, verbose_name="Pacht vereinnahmt (€)", validators=[MinValueValidator(0)] ) umlagen_vereinnahmt = models.DecimalField( max_digits=12, decimal_places=2, default=0, verbose_name="Umlagen vereinnahmt (€)", validators=[MinValueValidator(0)] ) sonstige_einnahmen = models.DecimalField( max_digits=12, decimal_places=2, default=0, verbose_name="Sonstige Einnahmen (€)", validators=[MinValueValidator(0)] ) # Zahlungstermine (optional) zahlungen = models.JSONField( null=True, blank=True, verbose_name="Zahlungstermine", help_text="Liste von Objekten {datum, betrag, art}" ) # Ausgaben grundsteuer_bescheid_nr = models.CharField(max_length=80, null=True, blank=True, verbose_name="Grundsteuer-Bescheid Nr.") grundsteuer_betrag = models.DecimalField( max_digits=12, decimal_places=2, default=0, verbose_name="Grundsteuer Betrag (€)", validators=[MinValueValidator(0)] ) versicherungen_betrag = models.DecimalField( max_digits=12, decimal_places=2, default=0, verbose_name="Versicherungen Betrag (€)", validators=[MinValueValidator(0)] ) verbandsbeitraege_betrag = models.DecimalField( max_digits=12, decimal_places=2, default=0, verbose_name="Verbandsbeiträge Betrag (€)", validators=[MinValueValidator(0)] ) sonstige_abgaben_betrag = models.DecimalField( max_digits=12, decimal_places=2, default=0, verbose_name="Sonstige öffentliche Abgaben (€)", validators=[MinValueValidator(0)] ) instandhaltung_betrag = models.DecimalField( max_digits=12, decimal_places=2, default=0, verbose_name="Instandhaltung/Reparaturen (€)", validators=[MinValueValidator(0)] ) verwaltung_recht_betrag = models.DecimalField( max_digits=12, decimal_places=2, default=0, verbose_name="Verwaltung/Recht (€)", validators=[MinValueValidator(0)] ) # Umsatzsteuer/Vorsteuer vorsteuer_aus_umlagen = models.DecimalField( max_digits=12, decimal_places=2, default=0, verbose_name="Vorsteuer aus umgelegten Kosten (€)", validators=[MinValueValidator(0)] ) # Sonstiges offene_posten = models.DecimalField( max_digits=12, decimal_places=2, default=0, verbose_name="Offene Posten (€)" ) bemerkungen = models.TextField(null=True, blank=True, verbose_name="Bemerkungen Abrechnung") # Dokumente pachtvertrag_datei = models.FileField( upload_to='land_abrechnungen/vertraege/', null=True, blank=True, verbose_name="Pachtvertrag (Datei)" ) grundsteuer_bescheid_datei = models.FileField( upload_to='land_abrechnungen/bescheide/', null=True, blank=True, verbose_name="Grundsteuerbescheid (Datei)" ) versicherungsnachweis_datei = models.FileField( upload_to='land_abrechnungen/versicherungen/', null=True, blank=True, verbose_name="Versicherungsnachweis (Datei)" ) # Zeitstempel erstellt_am = models.DateTimeField(auto_now_add=True) aktualisiert_am = models.DateTimeField(auto_now=True) class Meta: verbose_name = "Landabrechnung" verbose_name_plural = "Landabrechnungen" ordering = ['-abrechnungsjahr', 'land__gemeinde', 'land__gemarkung'] unique_together = ['land', 'abrechnungsjahr'] # Ein Jahr pro Land def __str__(self): return f"{self.land} - Abrechnung {self.abrechnungsjahr}" @property def einnahmen_gesamt(self): """Berechnet die Gesamteinnahmen""" from decimal import Decimal return (self.pacht_vereinnahmt + self.umlagen_vereinnahmt + self.sonstige_einnahmen) @property def ausgaben_gesamt(self): """Berechnet die Gesamtausgaben""" from decimal import Decimal return ( self.grundsteuer_betrag + self.versicherungen_betrag + self.verbandsbeitraege_betrag + self.sonstige_abgaben_betrag + self.instandhaltung_betrag + self.verwaltung_recht_betrag ) @property def nettoergebnis(self): """Berechnet das Nettoergebnis""" return self.einnahmen_gesamt - self.ausgaben_gesamt @property def ust_pacht_betrag(self): """Berechnet die USt auf Pacht (falls optiert)""" from decimal import Decimal, ROUND_HALF_UP if self.land.ust_option and self.pacht_vereinnahmt: ust = self.pacht_vereinnahmt * (self.land.ust_satz / Decimal('100')) return ust.quantize(Decimal('0.01'), rounding=ROUND_HALF_UP) return Decimal('0.00') class DokumentLink(models.Model): KONTEXT_CHOICES = [ ('pachtvertrag', 'Pachtvertrag'), ('antrag', 'Antrag'), ('verwendungsnachweis', 'Verwendungsnachweis'), ('rechnung', 'Rechnung'), ('vertrag', 'Vertrag'), ('bericht', 'Bericht'), ('landkarte', 'Landkarte'), ('kataster', 'Kataster'), ('anderes', 'Anderes'), ] id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False) paperless_document_id = models.IntegerField() kontext = models.CharField(max_length=30, choices=KONTEXT_CHOICES, default='anderes') titel = models.CharField(max_length=255) beschreibung = models.TextField(null=True, blank=True) # Verknüpfungen zu anderen Modellen (als Strings für Flexibilität) verpachtung_id = models.UUIDField(null=True, blank=True, verbose_name="Verpachtung ID (Legacy)") land_verpachtung_id = models.UUIDField(null=True, blank=True, verbose_name="Landverpachtung ID (Neu)") land_id = models.UUIDField(null=True, blank=True, verbose_name="Länderei ID") paechter_id = models.UUIDField(null=True, blank=True, verbose_name="Pächter ID") destinataer_id = models.UUIDField(null=True, blank=True, verbose_name="Destinatär ID") foerderung_id = models.UUIDField(null=True, blank=True, verbose_name="Förderung ID") rentmeister_id = models.UUIDField(null=True, blank=True, verbose_name="Rentmeister ID") abrechnung_id = models.UUIDField(null=True, blank=True, verbose_name="Abrechnung ID") class Meta: verbose_name = "Dokument" verbose_name_plural = "Dokumente" ordering = ['titel'] def __str__(self): return f"{self.titel} ({self.get_kontext_display()})" def get_paperless_url(self): """Gibt die URL zum Dokument in Paperless zurück (über Django Redirect)""" return f"/api/paperless/documents/{self.paperless_document_id}/" def get_paperless_thumbnail_url(self): """Gibt die URL zum Thumbnail in Paperless zurück""" from django.conf import settings if settings.PAPERLESS_API_URL: return f"{settings.PAPERLESS_API_URL}/api/paperless/documents/{self.paperless_document_id}/thumb/" return None def get_verpachtung(self): """Gibt die verknüpfte Verpachtung zurück""" if self.verpachtung_id: try: return LandVerpachtung.objects.get(pk=self.verpachtung_id) except LandVerpachtung.DoesNotExist: return None return None def get_land(self): """Gibt die verknüpfte Länderei zurück""" if self.land_id: try: return Land.objects.get(pk=self.land_id) except Land.DoesNotExist: return None return None def get_paechter(self): """Gibt den verknüpften Pächter zurück""" if self.paechter_id: try: return Paechter.objects.get(pk=self.paechter_id) except Paechter.DoesNotExist: return None return None def get_destinataer(self): """Gibt den verknüpften Destinatär zurück""" if self.destinataer_id: try: return Destinataer.objects.get(pk=self.destinataer_id) except Destinataer.DoesNotExist: return None return None def get_foerderung(self): """Gibt die verknüpfte Förderung zurück""" if self.foerderung_id: try: return Foerderung.objects.get(pk=self.foerderung_id) except Foerderung.DoesNotExist: return None return None def get_land_verpachtung(self): """Gibt die verknüpfte neue Landverpachtung zurück""" if self.land_verpachtung_id: try: return LandVerpachtung.objects.get(pk=self.land_verpachtung_id) except LandVerpachtung.DoesNotExist: return None return None class Foerderung(models.Model): KATEGORIE_CHOICES = [ ('bildung', 'Bildung'), ('forschung', 'Forschung'), ('kultur', 'Kultur'), ('soziales', 'Soziales'), ('umwelt', 'Umwelt'), ('anderes', 'Anderes'), ] STATUS_CHOICES = [ ('beantragt', 'Beantragt'), ('genehmigt', 'Genehmigt'), ('ausgezahlt', 'Ausgezahlt'), ('abgelehnt', 'Abgelehnt'), ('storniert', 'Storniert'), ] id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False) # Legacy field for migration - will be removed after data migration person = models.ForeignKey(Person, on_delete=models.CASCADE, verbose_name="Person (Legacy)", null=True, blank=True) destinataer = models.ForeignKey(Destinataer, on_delete=models.CASCADE, verbose_name="Destinatär", null=True, blank=True) jahr = models.IntegerField( validators=[MinValueValidator(1900), MaxValueValidator(2100)] ) betrag = models.DecimalField(max_digits=12, decimal_places=2) kategorie = models.CharField(max_length=20, choices=KATEGORIE_CHOICES, default='anderes') status = models.CharField(max_length=20, choices=STATUS_CHOICES, default='beantragt') verwendungsnachweis = models.ForeignKey(DokumentLink, null=True, blank=True, on_delete=models.SET_NULL, verbose_name="Verwendungsnachweis") bemerkungen = models.TextField(null=True, blank=True) antragsdatum = models.DateField(default=timezone.now) entscheidungsdatum = models.DateField(null=True, blank=True) class Meta: verbose_name = "Förderung" verbose_name_plural = "Förderungen" ordering = ['-jahr', '-betrag'] # Note: unique_together will be updated after migration def __str__(self): if self.destinataer: return f"{self.destinataer} - {self.jahr} - €{self.betrag}" elif self.person: return f"{self.person} (Legacy) - {self.jahr} - €{self.betrag}" return f"Unbekannt - {self.jahr} - €{self.betrag}" def get_status_color(self): colors = { 'beantragt': 'orange', 'genehmigt': 'blue', 'ausgezahlt': 'green', 'abgelehnt': 'red', 'storniert': 'gray', } return colors.get(self.status, 'black') class DestinataerUnterstuetzung(models.Model): """Geplante/ausgeführte Unterstützungszahlungen an Destinatäre""" STATUS_CHOICES = [ ('geplant', 'Geplant'), ('faellig', 'Fällig'), ('in_bearbeitung', 'In Bearbeitung'), ('ausgezahlt', 'Ausgezahlt'), ('storniert', 'Storniert'), ] id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False) destinataer = models.ForeignKey('Destinataer', on_delete=models.CASCADE, related_name='unterstuetzungen', verbose_name='Destinatär') konto = models.ForeignKey('StiftungsKonto', on_delete=models.PROTECT, verbose_name='Zahlungskonto') betrag = models.DecimalField(max_digits=12, decimal_places=2, verbose_name='Betrag (€)') faellig_am = models.DateField(verbose_name='Fällig am') status = models.CharField(max_length=20, choices=STATUS_CHOICES, default='geplant', verbose_name='Status') beschreibung = models.CharField(max_length=255, blank=True, verbose_name='Beschreibung') # Enhanced fields for recurrent payments and IBAN tracking empfaenger_iban = models.CharField(max_length=34, blank=True, verbose_name='Empfänger IBAN') empfaenger_name = models.CharField(max_length=200, blank=True, verbose_name='Empfänger Name') verwendungszweck = models.CharField(max_length=140, blank=True, verbose_name='Verwendungszweck') ausgezahlt_am = models.DateField(null=True, blank=True, verbose_name='Ausgezahlt am') ausgezahlt_von = models.ForeignKey('auth.User', on_delete=models.SET_NULL, null=True, blank=True, verbose_name='Ausgezahlt von') # Link to recurrent payment template if this was auto-generated wiederkehrend_von = models.ForeignKey('UnterstuetzungWiederkehrend', on_delete=models.SET_NULL, null=True, blank=True, verbose_name='Wiederkehrende Zahlung') erstellt_am = models.DateTimeField(auto_now_add=True) aktualisiert_am = models.DateTimeField(auto_now=True) class Meta: verbose_name = 'Destinatärunterstützung' verbose_name_plural = 'Destinatärunterstützungen' ordering = ['-faellig_am', '-erstellt_am'] indexes = [ models.Index(fields=['status', 'faellig_am']), models.Index(fields=['destinataer', 'status']), models.Index(fields=['wiederkehrend_von']), ] def __str__(self): return f"{self.destinataer.get_full_name()} – €{self.betrag} am {self.faellig_am} ({self.get_status_display()})" def is_overdue(self): """Check if payment is overdue""" from django.utils import timezone return self.faellig_am < timezone.now().date() and self.status in ['geplant', 'faellig'] def can_be_marked_paid(self): """Check if payment can be marked as paid""" return self.status in ['geplant', 'faellig', 'in_bearbeitung'] class UnterstuetzungWiederkehrend(models.Model): """Template for recurring support payments""" INTERVALL_CHOICES = [ ('monatlich', 'Monatlich'), ('quartalsweise', 'Vierteljährlich'), ('halbjaehrlich', 'Halbjährlich'), ('jaehrlich', 'Jährlich'), ] id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False) destinataer = models.ForeignKey('Destinataer', on_delete=models.CASCADE, related_name='wiederkehrende_unterstuetzungen', verbose_name='Destinatär') konto = models.ForeignKey('StiftungsKonto', on_delete=models.PROTECT, verbose_name='Zahlungskonto') betrag = models.DecimalField(max_digits=12, decimal_places=2, verbose_name='Betrag (€)') intervall = models.CharField(max_length=20, choices=INTERVALL_CHOICES, verbose_name='Intervall') beschreibung = models.CharField(max_length=255, blank=True, verbose_name='Beschreibung') # IBAN and payment details empfaenger_iban = models.CharField(max_length=34, verbose_name='Empfänger IBAN') empfaenger_name = models.CharField(max_length=200, verbose_name='Empfänger Name') verwendungszweck = models.CharField(max_length=140, blank=True, verbose_name='Verwendungszweck') # Schedule settings erste_zahlung_am = models.DateField(verbose_name='Erste Zahlung am') letzte_zahlung_am = models.DateField(null=True, blank=True, verbose_name='Letzte Zahlung am (optional)') naechste_generierung = models.DateField(verbose_name='Nächste Generierung') aktiv = models.BooleanField(default=True, verbose_name='Aktiv') erstellt_am = models.DateTimeField(auto_now_add=True) erstellt_von = models.ForeignKey('auth.User', on_delete=models.SET_NULL, null=True, blank=True, verbose_name='Erstellt von') class Meta: verbose_name = 'Wiederkehrende Unterstützung' verbose_name_plural = 'Wiederkehrende Unterstützungen' ordering = ['-erstellt_am'] indexes = [ models.Index(fields=['aktiv', 'naechste_generierung']), models.Index(fields=['destinataer', 'aktiv']), ] def __str__(self): return f"{self.destinataer.get_full_name()} – {self.get_intervall_display()} €{self.betrag}" def generiere_naechste_zahlung(self): """Generate the next scheduled payment""" from datetime import timedelta from dateutil.relativedelta import relativedelta if not self.aktiv: return None heute = timezone.now().date() if self.naechste_generierung > heute: return None # Not yet time to generate # Check if we've reached the end date if self.letzte_zahlung_am and self.naechste_generierung > self.letzte_zahlung_am: return None # Create the next payment neue_zahlung = DestinataerUnterstuetzung.objects.create( destinataer=self.destinataer, konto=self.konto, betrag=self.betrag, faellig_am=self.naechste_generierung, beschreibung=self.beschreibung or f"{self.get_intervall_display()} Unterstützung", empfaenger_iban=self.empfaenger_iban, empfaenger_name=self.empfaenger_name, verwendungszweck=self.verwendungszweck, wiederkehrend_von=self, status='geplant' ) # Calculate next generation date if self.intervall == 'monatlich': self.naechste_generierung = self.naechste_generierung + relativedelta(months=1) elif self.intervall == 'quartalsweise': self.naechste_generierung = self.naechste_generierung + relativedelta(months=3) elif self.intervall == 'halbjaehrlich': self.naechste_generierung = self.naechste_generierung + relativedelta(months=6) elif self.intervall == 'jaehrlich': self.naechste_generierung = self.naechste_generierung + relativedelta(years=1) self.save() return neue_zahlung class DestinataerNotiz(models.Model): """Zeitgestempelte Notizen/Telefonvermerke zu einem Destinatär, optional mit Datei.""" id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False) destinataer = models.ForeignKey('Destinataer', on_delete=models.CASCADE, related_name='notizen_eintraege', verbose_name='Destinatär') titel = models.CharField(max_length=200, blank=True, verbose_name='Titel') text = models.TextField(blank=True, verbose_name='Notiz') datei = models.FileField(upload_to='destinataer_notizen/', null=True, blank=True, verbose_name='Anhang') erstellt_von = models.ForeignKey('auth.User', on_delete=models.SET_NULL, null=True, blank=True, verbose_name='Erstellt von') erstellt_am = models.DateTimeField(auto_now_add=True, verbose_name='Erstellt am') class Meta: verbose_name = 'Destinatär-Notiz' verbose_name_plural = 'Destinatär-Notizen' ordering = ['-erstellt_am'] def __str__(self): return self.titel or f"Notiz {self.erstellt_am.strftime('%d.%m.%Y %H:%M')}" class Rentmeister(models.Model): """Geschäftsführer der Stiftung (natürliche Personen)""" ANREDE_CHOICES = [ ('herr', 'Herr'), ('frau', 'Frau'), ('dr', 'Dr.'), ('prof', 'Prof.'), ('prof_dr', 'Prof. Dr.'), ] id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False) anrede = models.CharField(max_length=10, choices=ANREDE_CHOICES, blank=True, verbose_name="Anrede") vorname = models.CharField(max_length=100, verbose_name="Vorname") nachname = models.CharField(max_length=100, verbose_name="Nachname") titel = models.CharField(max_length=50, blank=True, verbose_name="Titel") # Kontaktdaten email = models.EmailField(blank=True, verbose_name="E-Mail") telefon = models.CharField(max_length=20, blank=True, verbose_name="Telefon") mobil = models.CharField(max_length=20, blank=True, verbose_name="Mobil") # Adresse strasse = models.CharField(max_length=200, blank=True, verbose_name="Straße") plz = models.CharField(max_length=10, blank=True, verbose_name="PLZ") ort = models.CharField(max_length=100, blank=True, verbose_name="Ort") # Bankdaten für Abrechnungen iban = models.CharField(max_length=34, blank=True, verbose_name="IBAN") bic = models.CharField(max_length=11, blank=True, verbose_name="BIC") bank_name = models.CharField(max_length=100, blank=True, verbose_name="Bank") # Stiftungs-spezifisch seit_datum = models.DateField(verbose_name="Rentmeister seit") bis_datum = models.DateField(null=True, blank=True, verbose_name="Rentmeister bis") aktiv = models.BooleanField(default=True, verbose_name="Aktiv") # Vergütung/Aufwandsentschädigung monatliche_verguetung = models.DecimalField( max_digits=8, decimal_places=2, null=True, blank=True, verbose_name="Monatliche Vergütung (€)" ) km_pauschale = models.DecimalField( max_digits=4, decimal_places=2, default=0.30, verbose_name="Kilometerpauschale (€/km)" ) notizen = models.TextField(blank=True, verbose_name="Notizen") erstellt_am = models.DateTimeField(auto_now_add=True) aktualisiert_am = models.DateTimeField(auto_now=True) class Meta: verbose_name = "Rentmeister" verbose_name_plural = "Rentmeister" ordering = ['nachname', 'vorname'] def __str__(self): name_parts = [] if self.anrede: name_parts.append(self.get_anrede_display()) if self.vorname: name_parts.append(self.vorname) name_parts.append(self.nachname) if self.titel: name_parts.append(f"({self.titel})") return " ".join(name_parts) def get_full_name(self): """Vollständiger Name ohne Anrede""" if self.vorname: return f"{self.vorname} {self.nachname}" return self.nachname def get_address(self): """Vollständige Adresse als String""" parts = [] if self.strasse: parts.append(self.strasse) if self.plz and self.ort: parts.append(f"{self.plz} {self.ort}") elif self.ort: parts.append(self.ort) return ", ".join(parts) class StiftungsKonto(models.Model): """Bankkonten der Stiftung""" KONTO_TYP_CHOICES = [ ('girokonto', 'Girokonto'), ('sparkonto', 'Sparkonto'), ('festgeld', 'Festgeld'), ('tagesgeld', 'Tagesgeld'), ('depot', 'Depot'), ('sonstiges', 'Sonstiges'), ] id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False) kontoname = models.CharField(max_length=200, verbose_name="Kontoname") bank_name = models.CharField(max_length=200, verbose_name="Bank") iban = models.CharField(max_length=34, verbose_name="IBAN") bic = models.CharField(max_length=11, blank=True, verbose_name="BIC") konto_typ = models.CharField(max_length=20, choices=KONTO_TYP_CHOICES, default='girokonto', verbose_name="Kontotyp") saldo = models.DecimalField(max_digits=10, decimal_places=2, default=0.00, verbose_name="Aktueller Saldo") saldo_datum = models.DateField(null=True, blank=True, verbose_name="Saldo-Datum") zinssatz = models.DecimalField(max_digits=5, decimal_places=2, null=True, blank=True, verbose_name="Zinssatz (%)") laufzeit_bis = models.DateField(null=True, blank=True, verbose_name="Laufzeit bis") aktiv = models.BooleanField(default=True, verbose_name="Aktiv") notizen = models.TextField(blank=True, verbose_name="Notizen") erstellt_am = models.DateTimeField(auto_now_add=True) aktualisiert_am = models.DateTimeField(auto_now=True) class Meta: verbose_name = "Stiftungskonto" verbose_name_plural = "Stiftungskonten" ordering = ['bank_name', 'kontoname'] def __str__(self): return f"{self.bank_name} - {self.kontoname}" class BankTransaction(models.Model): """Banktransaktionen aus importierten Kontodaten""" TRANSACTION_TYPE_CHOICES = [ ('eingang', 'Eingang'), ('ausgang', 'Ausgang'), ('lastschrift', 'Lastschrift'), ('ueberweisung', 'Überweisung'), ('dauerauftrag', 'Dauerauftrag'), ('kartenzahlung', 'Kartenzahlung'), ('zinsen', 'Zinsen'), ('gebuehren', 'Gebühren'), ('sonstiges', 'Sonstiges'), ] STATUS_CHOICES = [ ('imported', 'Importiert'), ('verified', 'Geprüft'), ('assigned', 'Zugeordnet'), ('ignored', 'Ignoriert'), ] id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False) konto = models.ForeignKey(StiftungsKonto, on_delete=models.CASCADE, verbose_name="Konto") # Transaktionsdaten datum = models.DateField(verbose_name="Buchungsdatum") valuta = models.DateField(null=True, blank=True, verbose_name="Valutadatum") betrag = models.DecimalField(max_digits=12, decimal_places=2, verbose_name="Betrag (€)") waehrung = models.CharField(max_length=3, default='EUR', verbose_name="Währung") # Transaktionsdetails verwendungszweck = models.TextField(verbose_name="Verwendungszweck") empfaenger_zahlungspflichtiger = models.CharField(max_length=200, blank=True, verbose_name="Empfänger/Zahlungspflichtiger") iban_gegenpartei = models.CharField(max_length=34, blank=True, verbose_name="IBAN Gegenpartei") bic_gegenpartei = models.CharField(max_length=11, blank=True, verbose_name="BIC Gegenpartei") # Bankspezifische Daten referenz = models.CharField(max_length=100, blank=True, verbose_name="Referenz/Transaktions-ID") transaction_type = models.CharField(max_length=20, choices=TRANSACTION_TYPE_CHOICES, default='sonstiges', verbose_name="Transaktionsart") # Verwaltung status = models.CharField(max_length=20, choices=STATUS_CHOICES, default='imported', verbose_name="Status") kommentare = models.TextField(blank=True, verbose_name="Kommentare") verwaltungskosten = models.ForeignKey('Verwaltungskosten', null=True, blank=True, on_delete=models.SET_NULL, verbose_name="Zugeordnete Verwaltungskosten") # Import-Metadaten import_datei = models.CharField(max_length=255, blank=True, verbose_name="Import-Datei") importiert_am = models.DateTimeField(auto_now_add=True, verbose_name="Importiert am") saldo_nach_buchung = models.DecimalField(max_digits=12, decimal_places=2, null=True, blank=True, verbose_name="Saldo nach Buchung") class Meta: verbose_name = "Banktransaktion" verbose_name_plural = "Banktransaktionen" ordering = ['-datum', '-importiert_am'] unique_together = ['konto', 'datum', 'betrag', 'referenz'] # Prevent duplicates def __str__(self): return f"{self.datum} - {self.betrag}€ - {self.verwendungszweck[:50]}" def is_income(self): """Prüft ob es sich um einen Geldeingang handelt""" return self.betrag > 0 def get_absolute_amount(self): """Gibt den absoluten Betrag zurück""" return abs(self.betrag) class Verwaltungskosten(models.Model): """Administrative Kosten und Ausgaben der Stiftung""" KATEGORIE_CHOICES = [ ('rechnung_intern', 'Interne Rechnung'), ('bueroausstattung', 'Büroausstattung'), ('fahrtkosten', 'Fahrtkosten'), ('porto', 'Porto & Versand'), ('telefon_internet', 'Telefon & Internet'), ('software', 'Software & Lizenzen'), ('beratung', 'Beratung & Dienstleistungen'), ('versicherung', 'Versicherungen'), ('steuerberatung', 'Steuerberatung'), ('bankgebuehren', 'Bankgebühren'), ('sonstiges', 'Sonstiges'), ] STATUS_CHOICES = [ ('geplant', 'Geplant'), ('bestellt', 'Bestellt'), ('erhalten', 'Erhalten'), ('in_bearbeitung', 'In Bearbeitung'), ('bezahlt', 'Bezahlt'), ('storniert', 'Storniert'), ] id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False) bezeichnung = models.CharField(max_length=200, verbose_name="Bezeichnung") kategorie = models.CharField(max_length=30, choices=KATEGORIE_CHOICES, verbose_name="Kategorie") betrag = models.DecimalField(max_digits=10, decimal_places=2, verbose_name="Betrag (€)") datum = models.DateField(verbose_name="Datum") lieferant_firma = models.CharField(max_length=200, blank=True, verbose_name="Lieferant/Firma") rechnungsnummer = models.CharField(max_length=100, blank=True, verbose_name="Rechnungsnummer") status = models.CharField(max_length=20, choices=STATUS_CHOICES, default='geplant', verbose_name="Status") # Zuständigkeit und Zahlung rentmeister = models.ForeignKey(Rentmeister, on_delete=models.SET_NULL, null=True, blank=True, verbose_name="Zuständiger Rentmeister") zahlungskonto = models.ForeignKey(StiftungsKonto, on_delete=models.SET_NULL, null=True, blank=True, related_name='zahlungen', verbose_name="Zahlungskonto") quellkonto = models.ForeignKey(StiftungsKonto, on_delete=models.SET_NULL, null=True, blank=True, related_name='ausgaben', verbose_name="Quellkonto") # Legacy field für Rückwärtskompatibilität konto = models.ForeignKey(StiftungsKonto, on_delete=models.SET_NULL, null=True, blank=True, verbose_name="Konto (Legacy)", help_text="Veraltet - verwende Zahlungskonto und Quellkonto") # Fahrtkosten spezifisch km_anzahl = models.DecimalField(max_digits=8, decimal_places=1, null=True, blank=True, verbose_name="Kilometer") km_satz = models.DecimalField(max_digits=4, decimal_places=2, null=True, blank=True, verbose_name="€/km") von_ort = models.CharField(max_length=100, blank=True, verbose_name="Von (Ort)") nach_ort = models.CharField(max_length=100, blank=True, verbose_name="Nach (Ort)") zweck = models.CharField(max_length=200, blank=True, verbose_name="Zweck der Fahrt") beschreibung = models.TextField(blank=True, verbose_name="Beschreibung") notizen = models.TextField(blank=True, verbose_name="Notizen") erstellt_am = models.DateTimeField(auto_now_add=True) aktualisiert_am = models.DateTimeField(auto_now=True) class Meta: verbose_name = "Verwaltungskosten" verbose_name_plural = "Verwaltungskosten" ordering = ['-datum', '-erstellt_am'] def __str__(self): return f"{self.bezeichnung} - €{self.betrag} ({self.datum})" def get_status_color(self): colors = { 'geplant': 'secondary', 'bestellt': 'warning', 'erhalten': 'info', 'in_bearbeitung': 'primary', 'bezahlt': 'success', 'storniert': 'danger', } return colors.get(self.status, 'secondary') def get_effective_zahlungskonto(self): """Gibt das Zahlungskonto zurück, fallback auf Legacy-Konto""" return self.zahlungskonto or self.konto def get_effective_quellkonto(self): """Gibt das Quellkonto zurück, fallback auf Zahlungskonto oder Legacy-Konto""" return self.quellkonto or self.zahlungskonto or self.konto def is_fahrtkosten(self): """Prüft ob es sich um Fahrtkosten handelt""" return self.kategorie == 'fahrtkosten' def calculate_fahrtkosten(self): """Berechnet Fahrtkosten automatisch wenn km_anzahl und km_satz gesetzt sind""" if self.km_anzahl and self.km_satz: return self.km_anzahl * self.km_satz return None class ApplicationPermission(models.Model): """Custom permissions for application functions""" class Meta: managed = False # No database table creation default_permissions = () # Remove default Django permissions permissions = [ # Entity Management Permissions ('manage_destinataere', 'Kann Destinatäre verwalten'), ('view_destinataere', 'Kann Destinatäre anzeigen'), ('manage_land', 'Kann Ländereien verwalten'), ('view_land', 'Kann Ländereien anzeigen'), ('manage_paechter', 'Kann Pächter verwalten'), ('view_paechter', 'Kann Pächter anzeigen'), ('manage_verpachtungen', 'Kann Verpachtungen verwalten'), ('view_verpachtungen', 'Kann Verpachtungen anzeigen'), ('manage_foerderungen', 'Kann Förderungen verwalten'), ('view_foerderungen', 'Kann Förderungen anzeigen'), # Document Management Permissions ('manage_documents', 'Kann Dokumente verwalten'), ('view_documents', 'Kann Dokumente anzeigen'), ('link_documents', 'Kann Dokumente verknüpfen'), # Financial Management Permissions ('manage_verwaltungskosten', 'Kann Verwaltungskosten verwalten'), ('view_verwaltungskosten', 'Kann Verwaltungskosten anzeigen'), ('approve_payments', 'Kann Zahlungen genehmigen'), ('manage_konten', 'Kann Stiftungskonten verwalten'), ('view_konten', 'Kann Stiftungskonten anzeigen'), ('manage_rentmeister', 'Kann Rentmeister verwalten'), ('view_rentmeister', 'Kann Rentmeister anzeigen'), # Administration Permissions ('access_administration', 'Kann Administration aufrufen'), ('view_audit_logs', 'Kann Audit-Logs anzeigen'), ('manage_backups', 'Kann Backups erstellen und verwalten'), ('manage_users', 'Kann Benutzer verwalten'), ('manage_permissions', 'Kann Berechtigungen verwalten'), # Import/Export Permissions ('import_data', 'Kann Daten importieren'), ('export_data', 'Kann Daten exportieren'), # System Permissions ('access_django_admin', 'Kann Django Admin aufrufen'), ('view_system_stats', 'Kann Systemstatistiken anzeigen'), ] class AuditLog(models.Model): """Audit Log für alle Benutzeraktionen im System""" ACTION_TYPES = [ ('create', 'Erstellt'), ('update', 'Aktualisiert'), ('delete', 'Gelöscht'), ('link', 'Verknüpft'), ('unlink', 'Verknüpfung entfernt'), ('login', 'Anmeldung'), ('logout', 'Abmeldung'), ('backup', 'Backup erstellt'), ('restore', 'Wiederherstellung'), ('export', 'Export'), ('import', 'Import'), ] ENTITY_TYPES = [ ('destinataer', 'Destinatär'), ('land', 'Länderei'), ('paechter', 'Pächter'), ('verpachtung', 'Verpachtung'), ('foerderung', 'Förderung'), ('rentmeister', 'Rentmeister'), ('stiftungskonto', 'Stiftungskonto'), ('verwaltungskosten', 'Verwaltungskosten'), ('banktransaction', 'Bank-Transaktion'), ('dokumentlink', 'Dokument-Verknüpfung'), ('system', 'System'), ('user', 'Benutzer'), ] id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False) # Benutzer und Zeitpunkt user = models.ForeignKey('auth.User', on_delete=models.SET_NULL, null=True, verbose_name="Benutzer") username = models.CharField(max_length=150, verbose_name="Benutzername") # Fallback falls User gelöscht wird timestamp = models.DateTimeField(auto_now_add=True, verbose_name="Zeitpunkt") # Aktion action = models.CharField(max_length=20, choices=ACTION_TYPES, verbose_name="Aktion") entity_type = models.CharField(max_length=20, choices=ENTITY_TYPES, verbose_name="Entitätstyp") entity_id = models.CharField(max_length=100, blank=True, verbose_name="Entitäts-ID") entity_name = models.CharField(max_length=255, verbose_name="Entitätsname") # Details description = models.TextField(verbose_name="Beschreibung") changes = models.JSONField(null=True, blank=True, verbose_name="Änderungen") # Alte und neue Werte # Request-Informationen ip_address = models.GenericIPAddressField(null=True, blank=True, verbose_name="IP-Adresse") user_agent = models.TextField(blank=True, verbose_name="User Agent") session_key = models.CharField(max_length=40, blank=True, verbose_name="Session-Key") class Meta: verbose_name = "Audit Log Eintrag" verbose_name_plural = "Audit Log Einträge" ordering = ['-timestamp'] indexes = [ models.Index(fields=['timestamp']), models.Index(fields=['user', 'timestamp']), models.Index(fields=['entity_type', 'timestamp']), models.Index(fields=['action', 'timestamp']), ] def __str__(self): return f"{self.username} - {self.get_action_display()} {self.get_entity_type_display()} '{self.entity_name}' ({self.timestamp.strftime('%d.%m.%Y %H:%M')})" def get_changes_summary(self): """Erstellt eine lesbare Zusammenfassung der Änderungen""" if not self.changes: return "Keine Details verfügbar" if isinstance(self.changes, dict): summary = [] for field, values in self.changes.items(): if isinstance(values, dict) and 'old' in values and 'new' in values: old_val = values['old'] or 'Leer' new_val = values['new'] or 'Leer' summary.append(f"{field}: '{old_val}' → '{new_val}'") return "; ".join(summary) if summary else "Keine Änderungen dokumentiert" return str(self.changes) class BackupJob(models.Model): """Backup-Jobs und deren Status""" STATUS_CHOICES = [ ('pending', 'Wartend'), ('running', 'Läuft'), ('completed', 'Abgeschlossen'), ('failed', 'Fehlgeschlagen'), ] TYPE_CHOICES = [ ('full', 'Vollständiges Backup'), ('database', 'Nur Datenbank'), ('files', 'Nur Dateien'), ] id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False) # Job-Details backup_type = models.CharField(max_length=20, choices=TYPE_CHOICES, verbose_name="Backup-Typ") status = models.CharField(max_length=20, choices=STATUS_CHOICES, default='pending', verbose_name="Status") # Ausführung created_by = models.ForeignKey('auth.User', on_delete=models.SET_NULL, null=True, verbose_name="Erstellt von") created_at = models.DateTimeField(auto_now_add=True, verbose_name="Erstellt am") started_at = models.DateTimeField(null=True, blank=True, verbose_name="Gestartet am") completed_at = models.DateTimeField(null=True, blank=True, verbose_name="Abgeschlossen am") # Ergebnis backup_filename = models.CharField(max_length=255, blank=True, verbose_name="Backup-Dateiname") backup_size = models.BigIntegerField(null=True, blank=True, verbose_name="Backup-Größe (Bytes)") error_message = models.TextField(blank=True, verbose_name="Fehlermeldung") # Metadaten database_size = models.BigIntegerField(null=True, blank=True, verbose_name="Datenbankgröße (Bytes)") files_count = models.IntegerField(null=True, blank=True, verbose_name="Anzahl Dateien") class Meta: verbose_name = "Backup-Job" verbose_name_plural = "Backup-Jobs" ordering = ['-created_at'] def __str__(self): return f"{self.get_backup_type_display()} - {self.get_status_display()} ({self.created_at.strftime('%d.%m.%Y %H:%M')})" def get_duration(self): """Berechnet die Dauer des Backup-Jobs""" if self.started_at and self.completed_at: return self.completed_at - self.started_at elif self.started_at: from django.utils import timezone return timezone.now() - self.started_at return None def get_size_display(self): """Formatiert die Backup-Größe für die Anzeige""" if not self.backup_size: return "Unbekannt" size = self.backup_size for unit in ['B', 'KB', 'MB', 'GB']: if size < 1024: return f"{size:.1f} {unit}" size /= 1024 return f"{size:.1f} TB" class AppConfiguration(models.Model): """Application configuration settings that can be managed through the admin interface""" SETTING_TYPE_CHOICES = [ ('text', 'Text'), ('number', 'Number'), ('boolean', 'Boolean'), ('url', 'URL'), ('tag', 'Tag Name'), ('tag_id', 'Tag ID'), ] CATEGORY_CHOICES = [ ('paperless', 'Paperless Integration'), ('general', 'General Settings'), ('corporate', 'Corporate Identity'), ('notifications', 'Notifications'), ('system', 'System Settings'), ] id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False) key = models.CharField(max_length=100, unique=True, verbose_name="Setting Key") display_name = models.CharField(max_length=200, verbose_name="Display Name") description = models.TextField(blank=True, null=True, verbose_name="Description") value = models.TextField(verbose_name="Value") default_value = models.TextField(verbose_name="Default Value") setting_type = models.CharField(max_length=20, choices=SETTING_TYPE_CHOICES, default='text', verbose_name="Type") category = models.CharField(max_length=50, choices=CATEGORY_CHOICES, default='general', verbose_name="Category") is_active = models.BooleanField(default=True, verbose_name="Active") is_system = models.BooleanField(default=False, verbose_name="System Setting (read-only)") order = models.IntegerField(default=0, verbose_name="Display Order") # Metadata created_at = models.DateTimeField(auto_now_add=True) updated_at = models.DateTimeField(auto_now=True) class Meta: verbose_name = "App Configuration" verbose_name_plural = "App Configurations" ordering = ['category', 'order', 'display_name'] def __str__(self): return f"{self.display_name} ({self.key})" def get_typed_value(self): """Return the value converted to the appropriate type""" if self.setting_type == 'boolean': return self.value.lower() in ('true', '1', 'yes', 'on') elif self.setting_type == 'number': try: if '.' in self.value: return float(self.value) return int(self.value) except (ValueError, TypeError): return 0 return self.value @classmethod def get_setting(cls, key, default=None): """Get a setting value by key""" try: setting = cls.objects.get(key=key, is_active=True) return setting.get_typed_value() except cls.DoesNotExist: return default @classmethod def set_setting(cls, key, value, display_name=None, description=None, setting_type='text', category='general'): """Set or update a setting value""" setting, created = cls.objects.get_or_create( key=key, defaults={ 'display_name': display_name or key, 'description': description, 'value': str(value), 'default_value': str(value), 'setting_type': setting_type, 'category': category, } ) if not created: setting.value = str(value) setting.save() return setting class HelpBox(models.Model): """Editierbare Hilfe-Infoboxen für Formulare""" PAGE_CHOICES = [ ('destinataer_new', 'Neuer Destinatär'), ('unterstuetzung_new', 'Neue Unterstützung'), ('foerderung_new', 'Neue Förderung'), ('paechter_new', 'Neuer Pächter'), ('laenderei_new', 'Neue Länderei'), ('verpachtung_new', 'Neue Verpachtung'), ('land_abrechnung_new', 'Neue Landabrechnung'), ('person_new', 'Neue Person'), ('konto_new', 'Neues Konto'), ('verwaltungskosten_new', 'Neue Verwaltungskosten'), ('rentmeister_new', 'Neuer Rentmeister'), ('dokument_new', 'Neues Dokument'), ('user_new', 'Neuer Benutzer'), ('csv_import_new', 'CSV Import'), ('destinataer_notiz_new', 'Destinatär Notiz'), ] id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False) page_key = models.CharField( max_length=50, choices=PAGE_CHOICES, unique=True, verbose_name="Seite" ) title = models.CharField( max_length=200, verbose_name="Titel der Hilfsbox" ) content = models.TextField( verbose_name="Inhalt (Markdown unterstützt)", help_text="Sie können Markdown verwenden: **fett**, *kursiv*, `code`, [Link](url), etc." ) is_active = models.BooleanField( default=True, verbose_name="Aktiv" ) # Metadata created_at = models.DateTimeField(auto_now_add=True, verbose_name="Erstellt am") updated_at = models.DateTimeField(auto_now=True, verbose_name="Aktualisiert am") created_by = models.CharField(max_length=100, null=True, blank=True, verbose_name="Erstellt von") updated_by = models.CharField(max_length=100, null=True, blank=True, verbose_name="Aktualisiert von") class Meta: verbose_name = "Hilfs-Infobox" verbose_name_plural = "Hilfs-Infoboxen" ordering = ['page_key'] def __str__(self): return f"{self.get_page_key_display()}: {self.title}" @classmethod def get_help_for_page(cls, page_key): """Hole die aktive Hilfs-Infobox für eine bestimmte Seite""" try: return cls.objects.get(page_key=page_key, is_active=True) except cls.DoesNotExist: return None