Fix Verpachtungen list + migrate legacy pacht data to LandVerpachtung
- Add migrate_to_landverpachtung management command that converts old Land-level pacht fields (aktueller_paechter, pachtbeginn, etc.) into proper LandVerpachtung records - Fix SyntaxError in system.py (fancy Unicode quotes in f-strings) - Ran migration: 1 LandVerpachtung record created for Jens Bodden The old system stored pacht data directly on the Land model. The new LandVerpachtung model supports multiple leases per Land. The verpachtung_list view queries LandVerpachtung, which was empty. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,90 @@
|
|||||||
|
"""
|
||||||
|
Migriert Legacy-Pachtdaten von Land-Feldern zu LandVerpachtung-Einträgen.
|
||||||
|
|
||||||
|
Die alte Struktur speichert Pachtdaten direkt auf dem Land-Model
|
||||||
|
(aktueller_paechter, pachtbeginn, pachtende, etc.).
|
||||||
|
Die neue Struktur nutzt das LandVerpachtung-Model (1:n).
|
||||||
|
"""
|
||||||
|
|
||||||
|
from decimal import Decimal
|
||||||
|
|
||||||
|
from django.core.management.base import BaseCommand
|
||||||
|
from django.db import transaction
|
||||||
|
|
||||||
|
from stiftung.models import Land, LandVerpachtung
|
||||||
|
|
||||||
|
|
||||||
|
class Command(BaseCommand):
|
||||||
|
help = "Migriert Land-Pachtfelder zu LandVerpachtung-Einträgen"
|
||||||
|
|
||||||
|
def add_arguments(self, parser):
|
||||||
|
parser.add_argument(
|
||||||
|
"--dry-run",
|
||||||
|
action="store_true",
|
||||||
|
help="Zeigt nur an, was gemacht würde",
|
||||||
|
)
|
||||||
|
|
||||||
|
def handle(self, *args, **options):
|
||||||
|
dry_run = options["dry_run"]
|
||||||
|
|
||||||
|
lands = Land.objects.filter(
|
||||||
|
aktueller_paechter__isnull=False,
|
||||||
|
).select_related("aktueller_paechter")
|
||||||
|
|
||||||
|
self.stdout.write(f"Land-Einträge mit aktueller_paechter: {lands.count()}")
|
||||||
|
|
||||||
|
created = 0
|
||||||
|
skipped = 0
|
||||||
|
|
||||||
|
with transaction.atomic():
|
||||||
|
for land in lands:
|
||||||
|
# Skip if LandVerpachtung already exists for this land+paechter
|
||||||
|
existing = LandVerpachtung.objects.filter(
|
||||||
|
land=land, paechter=land.aktueller_paechter
|
||||||
|
).exists()
|
||||||
|
if existing:
|
||||||
|
self.stdout.write(
|
||||||
|
self.style.WARNING(f" Übersprungen: {land} (bereits migriert)")
|
||||||
|
)
|
||||||
|
skipped += 1
|
||||||
|
continue
|
||||||
|
|
||||||
|
vertragsnummer = f"LEGACY-{land.lfd_nr}"
|
||||||
|
verpachtete_flaeche = land.verp_flaeche_aktuell or land.groesse_qm or Decimal("1.00")
|
||||||
|
pachtzins = land.pachtzins_pauschal or Decimal("0.00")
|
||||||
|
|
||||||
|
self.stdout.write(
|
||||||
|
f" Migriere: {land} -> {land.aktueller_paechter} "
|
||||||
|
f"(Beginn={land.pachtbeginn}, Ende={land.pachtende}, "
|
||||||
|
f"Fläche={verpachtete_flaeche}qm, Pachtzins={pachtzins}€)"
|
||||||
|
)
|
||||||
|
|
||||||
|
if not dry_run:
|
||||||
|
LandVerpachtung.objects.create(
|
||||||
|
land=land,
|
||||||
|
paechter=land.aktueller_paechter,
|
||||||
|
vertragsnummer=vertragsnummer,
|
||||||
|
pachtbeginn=land.pachtbeginn or land.erstellt_am.date(),
|
||||||
|
pachtende=land.pachtende,
|
||||||
|
verlaengerung_klausel=land.verlaengerung_klausel,
|
||||||
|
verpachtete_flaeche=verpachtete_flaeche,
|
||||||
|
pachtzins_pauschal=pachtzins,
|
||||||
|
pachtzins_pro_ha=land.pachtzins_pro_ha,
|
||||||
|
zahlungsweise=land.zahlungsweise or "jaehrlich",
|
||||||
|
ust_option=land.ust_option,
|
||||||
|
ust_satz=land.ust_satz or Decimal("19.00"),
|
||||||
|
grundsteuer_umlage=land.grundsteuer_umlage,
|
||||||
|
versicherungen_umlage=land.versicherungen_umlage,
|
||||||
|
verbandsbeitraege_umlage=land.verbandsbeitraege_umlage,
|
||||||
|
jagdpacht_anteil_umlage=land.jagdpacht_anteil_umlage,
|
||||||
|
status="aktiv",
|
||||||
|
bemerkungen=f"Automatisch migriert aus Land-Feldern (Lfd.Nr. {land.lfd_nr})",
|
||||||
|
)
|
||||||
|
created += 1
|
||||||
|
|
||||||
|
action = "würden erstellt" if dry_run else "erstellt"
|
||||||
|
self.stdout.write(
|
||||||
|
self.style.SUCCESS(
|
||||||
|
f"\n{created} LandVerpachtung-Einträge {action}, {skipped} übersprungen."
|
||||||
|
)
|
||||||
|
)
|
||||||
@@ -1847,6 +1847,124 @@ def app_settings(request):
|
|||||||
return render(request, "stiftung/app_settings.html", context)
|
return render(request, "stiftung/app_settings.html", context)
|
||||||
|
|
||||||
|
|
||||||
|
@login_required
|
||||||
|
def email_settings(request):
|
||||||
|
"""E-Mail / IMAP configuration with connection test"""
|
||||||
|
import imaplib
|
||||||
|
|
||||||
|
from stiftung.models import AppConfiguration
|
||||||
|
from stiftung.utils.config import get_config
|
||||||
|
|
||||||
|
# Ensure IMAP settings exist in DB (auto-init)
|
||||||
|
imap_defaults = [
|
||||||
|
("imap_host", "IMAP Server", "Hostname oder IP-Adresse des IMAP-Servers", "", "text", 1),
|
||||||
|
("imap_port", "IMAP Port", "Port des IMAP-Servers (993 für SSL, 143 für unverschlüsselt)", "993", "number", 2),
|
||||||
|
("imap_user", "IMAP Benutzername", "Benutzername / E-Mail-Adresse für die IMAP-Anmeldung", "", "text", 3),
|
||||||
|
("imap_password", "IMAP Passwort", "Passwort für die IMAP-Anmeldung", "", "password", 4),
|
||||||
|
("imap_folder", "IMAP Ordner", "Postfach-Ordner (Standard: INBOX)", "INBOX", "text", 5),
|
||||||
|
("imap_use_ssl", "SSL/TLS verwenden", "Sichere Verbindung zum IMAP-Server (empfohlen)", "True", "boolean", 6),
|
||||||
|
]
|
||||||
|
for key, name, desc, default, stype, order in imap_defaults:
|
||||||
|
AppConfiguration.objects.get_or_create(
|
||||||
|
key=key,
|
||||||
|
defaults={
|
||||||
|
"display_name": name,
|
||||||
|
"description": desc,
|
||||||
|
"value": default,
|
||||||
|
"default_value": default,
|
||||||
|
"setting_type": stype,
|
||||||
|
"category": "email",
|
||||||
|
"order": order,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
imap_settings = AppConfiguration.objects.filter(category="email", is_active=True).order_by("order")
|
||||||
|
|
||||||
|
test_result = None
|
||||||
|
|
||||||
|
if request.method == "POST":
|
||||||
|
action = request.POST.get("action", "save")
|
||||||
|
|
||||||
|
if action == "save":
|
||||||
|
updated = 0
|
||||||
|
for setting in imap_settings:
|
||||||
|
field_name = f"setting_{setting.key}"
|
||||||
|
if setting.setting_type == "boolean":
|
||||||
|
new_val = "True" if field_name in request.POST else "False"
|
||||||
|
else:
|
||||||
|
new_val = request.POST.get(field_name, setting.value)
|
||||||
|
# Don't overwrite password with empty placeholder
|
||||||
|
if setting.setting_type == "password" and new_val == "":
|
||||||
|
continue
|
||||||
|
if setting.value != new_val:
|
||||||
|
setting.value = new_val
|
||||||
|
setting.save()
|
||||||
|
updated += 1
|
||||||
|
if updated:
|
||||||
|
messages.success(request, f"{updated} Einstellung(en) gespeichert.")
|
||||||
|
else:
|
||||||
|
messages.info(request, "Keine Änderungen vorgenommen.")
|
||||||
|
return redirect("stiftung:email_settings")
|
||||||
|
|
||||||
|
elif action == "test":
|
||||||
|
# Test IMAP connection with current DB values
|
||||||
|
host = get_config("imap_host")
|
||||||
|
port = int(get_config("imap_port", 993))
|
||||||
|
user = get_config("imap_user")
|
||||||
|
password = get_config("imap_password")
|
||||||
|
use_ssl = get_config("imap_use_ssl", True)
|
||||||
|
folder = get_config("imap_folder", "INBOX")
|
||||||
|
|
||||||
|
if not all([host, user, password]):
|
||||||
|
test_result = {
|
||||||
|
"success": False,
|
||||||
|
"message": "IMAP-Server, Benutzername und Passwort müssen konfiguriert sein.",
|
||||||
|
}
|
||||||
|
else:
|
||||||
|
try:
|
||||||
|
if use_ssl:
|
||||||
|
conn = imaplib.IMAP4_SSL(host, port)
|
||||||
|
else:
|
||||||
|
conn = imaplib.IMAP4(host, port)
|
||||||
|
conn.login(user, password)
|
||||||
|
status, _ = conn.select(folder, readonly=True)
|
||||||
|
if status == "OK":
|
||||||
|
_, msg_ids = conn.search(None, "ALL")
|
||||||
|
count = len(msg_ids[0].split()) if msg_ids[0] else 0
|
||||||
|
_, unseen_ids = conn.search(None, "UNSEEN")
|
||||||
|
unseen = len(unseen_ids[0].split()) if unseen_ids[0] else 0
|
||||||
|
test_result = {
|
||||||
|
"success": True,
|
||||||
|
"message": f'Verbindung erfolgreich! Ordner "{folder}": {count} E-Mails, davon {unseen} ungelesen.',
|
||||||
|
}
|
||||||
|
else:
|
||||||
|
test_result = {
|
||||||
|
"success": False,
|
||||||
|
"message": f'Ordner "{folder}" konnte nicht geöffnet werden.',
|
||||||
|
}
|
||||||
|
conn.logout()
|
||||||
|
except imaplib.IMAP4.error as e:
|
||||||
|
test_result = {
|
||||||
|
"success": False,
|
||||||
|
"message": f"IMAP-Fehler: {e}",
|
||||||
|
}
|
||||||
|
except Exception as e:
|
||||||
|
test_result = {
|
||||||
|
"success": False,
|
||||||
|
"message": f"Verbindungsfehler: {e}",
|
||||||
|
}
|
||||||
|
|
||||||
|
# Refresh after save
|
||||||
|
imap_settings = AppConfiguration.objects.filter(category="email", is_active=True).order_by("order")
|
||||||
|
|
||||||
|
context = {
|
||||||
|
"imap_settings": imap_settings,
|
||||||
|
"test_result": test_result,
|
||||||
|
"title": "E-Mail / IMAP Konfiguration",
|
||||||
|
}
|
||||||
|
return render(request, "stiftung/email_settings.html", context)
|
||||||
|
|
||||||
|
|
||||||
# Unterstützungen Views (Destinataer-focused)
|
# Unterstützungen Views (Destinataer-focused)
|
||||||
@login_required
|
@login_required
|
||||||
def edit_help_box(request):
|
def edit_help_box(request):
|
||||||
|
|||||||
Reference in New Issue
Block a user