Files
stiftung-management-system/app/stiftung/models.py
2025-09-06 18:31:54 +02:00

1668 lines
66 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
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.verpachtung_set.filter(status='aktiv')
def get_gesamt_pachtflaeche(self):
"""Calculate total leased area"""
return self.verpachtung_set.filter(status='aktiv').aggregate(
total=models.Sum('verpachtete_flaeche')
)['total'] or 0
def get_gesamt_pachtzins(self):
"""Calculate total annual rent"""
return self.verpachtung_set.filter(status='aktiv').aggregate(
total=models.Sum('pachtzins_jaehrlich')
)['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
# Fallback: Legacy Verpachtungen (für Rückwärtskompatibilität)
legacy_total = self.verpachtung_set.filter(status='aktiv').aggregate(
total=Sum('verpachtete_flaeche')
)['total'] or 0
return legacy_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()
if self.pachtende:
return self.pachtbeginn <= heute <= self.pachtende
return self.pachtbeginn <= heute # Unbefristet
def get_restlaufzeit_tage(self):
"""Berechnet die Restlaufzeit in Tagen"""
from datetime import date
heute = date.today()
if self.pachtende and self.pachtende > heute:
return (self.pachtende - 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 = self.pachtzins_pauschal * (self.ust_satz / Decimal('100'))
return ust.quantize(Decimal('0.01'), rounding=ROUND_HALF_UP)
return Decimal('0.00')
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 Verpachtung(models.Model):
"""Verpachtungsverträge für Ländereien"""
STATUS_CHOICES = [
('aktiv', 'Aktiv'),
('beendet', 'Beendet'),
('gekuendigt', 'Gekündigt'),
('verlängert', 'Verlängert'),
]
id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
# Verpachtete Ländereien
land = models.ForeignKey(Land, on_delete=models.CASCADE, verbose_name="Land")
paechter = models.ForeignKey(Paechter, on_delete=models.CASCADE, 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(verbose_name="Pachtende")
verlaengerung = models.DateField(null=True, blank=True, verbose_name="Verlängerung bis")
# Finanzielle Bedingungen
pachtzins_pro_qm = models.DecimalField(
max_digits=8,
decimal_places=4,
verbose_name="Pachtzins pro qm (€)"
)
pachtzins_jaehrlich = models.DecimalField(
max_digits=12,
decimal_places=2,
verbose_name="Jährlicher Pachtzins (€)"
)
# Flächenangaben
verpachtete_flaeche = models.DecimalField(
max_digits=12,
decimal_places=2,
verbose_name="Verpachtete Fläche (qm)"
)
# Status
status = models.CharField(max_length=20, choices=STATUS_CHOICES, default='aktiv')
# Dokumentation
verwendungsnachweis = models.ForeignKey('DokumentLink', null=True, blank=True, on_delete=models.SET_NULL, verbose_name="Verwendungsnachweis")
bemerkungen = 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 = "Verpachtung"
verbose_name_plural = "Verpachtungen"
ordering = ['-pachtbeginn']
def __str__(self):
return f"{self.land} - {self.paechter} ({self.vertragsnummer})"
def get_vertragsdauer_tage(self):
"""Berechnet die Vertragsdauer in Tagen"""
from datetime import date
if self.pachtende:
return (self.pachtende - self.pachtbeginn).days
return 0
def get_restlaufzeit_tage(self):
"""Berechnet die Restlaufzeit in Tagen"""
from datetime import date
heute = date.today()
if self.pachtende and self.pachtende > heute:
return (self.pachtende - heute).days
return 0
def is_aktiv(self):
"""Prüft ob der Vertrag noch aktiv ist"""
from datetime import date
heute = date.today()
return self.pachtbeginn <= heute <= self.pachtende
@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:
# Umrechnung: 1 Hektar = 10.000 qm
hektar = Decimal(str(self.verpachtete_flaeche)) / Decimal('10000')
# Runden auf 2 Nachkommastellen
return hektar.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")
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"""
from django.conf import settings
if settings.PAPERLESS_API_URL:
return f"{settings.PAPERLESS_API_URL}/documents/{self.paperless_document_id}/"
return None
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/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 Verpachtung.objects.get(pk=self.verpachtung_id)
except Verpachtung.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'),
('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')
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']),
]
def __str__(self):
return f"{self.destinataer.get_full_name()} {self.betrag} am {self.faellig_am} ({self.get_status_display()})"
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"