When an ALKIS identifier is set on a Land record, the button links to ogc-api.nrw.de detail view instead of the imprecise TIM-Online search. Falls back to TIM-Online when no ALKIS number is present. Closes STI-57 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1090 lines
36 KiB
Python
1090 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"
|
|
)
|
|
alkis_kennzeichen = models.CharField(
|
|
max_length=30,
|
|
null=True,
|
|
blank=True,
|
|
verbose_name="ALKIS Flurstückskennzeichen",
|
|
help_text="z.B. 05300800400030______ — für direkte Verlinkung zum Katasteramt",
|
|
)
|
|
|
|
# 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
|