- Fix undefined variable 'e' in PDF generator error handling - Replace undefined 'Verpachtung' model references with 'LandVerpachtung' - Fix all import and object access references to use correct model name - Resolve all flake8 F821 undefined name errors This addresses all code quality failures that were blocking CI pipeline.
2030 lines
82 KiB
Python
2030 lines
82 KiB
Python
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
|