Files
stiftung-management-system/app/stiftung/models/land.py
SysAdmin Agent e6f4c5ba1b Generalize email system with invoice workflow and Stiftungsgeschichte category
- Rename DestinataerEmailEingang → EmailEingang with category support
  (destinataer, rechnung, land_pacht, stiftungsgeschichte, allgemein)
- Add invoice capture workflow: create Verwaltungskosten from email,
  link DMS documents as invoice attachments, track payment status
- Add Stiftungsgeschichte email category with auto-detection patterns
  (Ahnenforschung, Genealogie, Chronik, etc.) and DMS integration
- Update poll_emails task with category detection and DMS context mapping
- Show available history documents in Geschichte editor sidebar
- Consolidate DMS views, remove legacy dokument templates
- Update all detail/form templates for DMS document linking
- Add deploy.sh script and streamline compose.yml

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-12 10:17:14 +00:00

1083 lines
36 KiB
Python

import uuid
from django.core.validators import MinValueValidator
from django.db import models
from django.utils import timezone
from stiftung.utils.date_utils import ensure_date, get_year_from_date
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 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",
null=True,
blank=True,
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,
null=True,
blank=True,
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 and 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 ROUND_HALF_UP, Decimal
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 ROUND_HALF_UP, Decimal
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 ROUND_HALF_UP, Decimal
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 datetime import date
from decimal import Decimal
# 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 datetime import date
from decimal import Decimal
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 ROUND_HALF_UP, Decimal
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):
"""
DEPRECATED: Paperless-basierte Dokumentverknüpfung.
Wird durch DokumentDatei (Django-natives DMS) ersetzt.
Dieses Modell bleibt für historische Daten erhalten, wird aber nicht mehr aktiv genutzt.
"""
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_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:
from stiftung.models import Destinataer
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:
from stiftung.models import Foerderung
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