Files
stiftung-management-system/app/stiftung/models.py
Stiftung Development 54bf468c53 fix: resolve Python code quality issues
- 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.
2025-09-06 20:54:20 +02:00

2030 lines
82 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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