diff --git a/app/core/__init__.py b/app/core/__init__.py
index fdbcb08..8e9c292 100644
--- a/app/core/__init__.py
+++ b/app/core/__init__.py
@@ -1,4 +1,3 @@
from .celery import app as celery
__all__ = ("celery",)
-
diff --git a/app/core/asgi.py b/app/core/asgi.py
index d94fb6d..07b7778 100644
--- a/app/core/asgi.py
+++ b/app/core/asgi.py
@@ -1,4 +1,6 @@
import os
+
from django.core.asgi import get_asgi_application
-os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'core.settings')
+
+os.environ.setdefault("DJANGO_SETTINGS_MODULE", "core.settings")
application = get_asgi_application()
diff --git a/app/core/celery.py b/app/core/celery.py
index fc0c6b2..62a4916 100644
--- a/app/core/celery.py
+++ b/app/core/celery.py
@@ -1,4 +1,5 @@
import os
+
from celery import Celery
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "core.settings")
@@ -6,5 +7,3 @@ os.environ.setdefault("DJANGO_SETTINGS_MODULE", "core.settings")
app = Celery("core")
app.config_from_object("django.conf:settings", namespace="CELERY")
app.autodiscover_tasks()
-
-
diff --git a/app/core/settings.py b/app/core/settings.py
index 8a7c45f..8f33930 100644
--- a/app/core/settings.py
+++ b/app/core/settings.py
@@ -1,5 +1,6 @@
import os
from pathlib import Path
+
from dotenv import load_dotenv
# Load environment variables from .env file
@@ -13,59 +14,59 @@ ALLOWED_HOSTS = os.getenv("DJANGO_ALLOWED_HOSTS", "*").split(",")
# CSRF settings for localhost development
CSRF_TRUSTED_ORIGINS = [
- 'http://localhost:8081',
- 'http://127.0.0.1:8081',
+ "http://localhost:8081",
+ "http://127.0.0.1:8081",
]
INSTALLED_APPS = [
- 'django.contrib.admin',
- 'django.contrib.auth',
- 'django.contrib.contenttypes',
- 'django.contrib.sessions',
- 'django.contrib.messages',
- 'django.contrib.staticfiles',
- 'django.contrib.humanize',
- 'rest_framework',
- 'stiftung',
+ "django.contrib.admin",
+ "django.contrib.auth",
+ "django.contrib.contenttypes",
+ "django.contrib.sessions",
+ "django.contrib.messages",
+ "django.contrib.staticfiles",
+ "django.contrib.humanize",
+ "rest_framework",
+ "stiftung",
]
MIDDLEWARE = [
- 'django.middleware.security.SecurityMiddleware',
- 'django.contrib.sessions.middleware.SessionMiddleware',
- 'django.middleware.common.CommonMiddleware',
- 'django.middleware.csrf.CsrfViewMiddleware',
- 'django.contrib.auth.middleware.AuthenticationMiddleware',
- 'django.contrib.messages.middleware.MessageMiddleware',
- 'django.middleware.clickjacking.XFrameOptionsMiddleware',
- 'stiftung.middleware.AuditMiddleware', # Audit logging middleware
+ "django.middleware.security.SecurityMiddleware",
+ "django.contrib.sessions.middleware.SessionMiddleware",
+ "django.middleware.common.CommonMiddleware",
+ "django.middleware.csrf.CsrfViewMiddleware",
+ "django.contrib.auth.middleware.AuthenticationMiddleware",
+ "django.contrib.messages.middleware.MessageMiddleware",
+ "django.middleware.clickjacking.XFrameOptionsMiddleware",
+ "stiftung.middleware.AuditMiddleware", # Audit logging middleware
]
-ROOT_URLCONF = 'core.urls'
+ROOT_URLCONF = "core.urls"
TEMPLATES = [
{
- 'BACKEND': 'django.template.backends.django.DjangoTemplates',
- 'DIRS': [BASE_DIR / 'templates'],
- 'APP_DIRS': True,
- 'OPTIONS': {
- 'context_processors': [
- 'django.template.context_processors.debug',
- 'django.template.context_processors.request',
- 'django.contrib.auth.context_processors.auth',
- 'django.contrib.messages.context_processors.messages',
+ "BACKEND": "django.template.backends.django.DjangoTemplates",
+ "DIRS": [BASE_DIR / "templates"],
+ "APP_DIRS": True,
+ "OPTIONS": {
+ "context_processors": [
+ "django.template.context_processors.debug",
+ "django.template.context_processors.request",
+ "django.contrib.auth.context_processors.auth",
+ "django.contrib.messages.context_processors.messages",
],
},
},
]
-WSGI_APPLICATION = 'core.wsgi.application'
+WSGI_APPLICATION = "core.wsgi.application"
DATABASES = {
- 'default': {
- 'ENGINE': 'django.db.backends.postgresql',
- 'NAME': os.getenv('POSTGRES_DB', 'stiftung'),
- 'USER': os.getenv('POSTGRES_USER', 'stiftung'),
- 'PASSWORD': os.getenv('POSTGRES_PASSWORD', 'stiftungpass'),
- 'HOST': os.getenv('DB_HOST', 'db'),
- 'PORT': os.getenv('DB_PORT', '5432'),
+ "default": {
+ "ENGINE": "django.db.backends.postgresql",
+ "NAME": os.getenv("POSTGRES_DB", "stiftung"),
+ "USER": os.getenv("POSTGRES_USER", "stiftung"),
+ "PASSWORD": os.getenv("POSTGRES_PASSWORD", "stiftungpass"),
+ "HOST": os.getenv("DB_HOST", "db"),
+ "PORT": os.getenv("DB_PORT", "5432"),
}
}
@@ -74,17 +75,17 @@ TIME_ZONE = os.getenv("TIME_ZONE", "Europe/Berlin")
USE_I18N = True
USE_TZ = True
-STATIC_URL = 'static/'
-STATIC_ROOT = BASE_DIR / 'staticfiles'
+STATIC_URL = "static/"
+STATIC_ROOT = BASE_DIR / "staticfiles"
# Additional locations of static files
STATICFILES_DIRS = [
BASE_DIR / "static",
]
-DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField'
-MEDIA_URL = '/media/'
-MEDIA_ROOT = BASE_DIR / 'media'
+DEFAULT_AUTO_FIELD = "django.db.models.BigAutoField"
+MEDIA_URL = "/media/"
+MEDIA_ROOT = BASE_DIR / "media"
# Celery
CELERY_BROKER_URL = os.getenv("REDIS_URL", "redis://redis:6379/0")
@@ -101,13 +102,13 @@ PAPERLESS_LAND_TAG_ID = os.getenv("PAPERLESS_LAND_TAG_ID", "204")
PAPERLESS_ADMIN_TAG_ID = os.getenv("PAPERLESS_ADMIN_TAG_ID", "216")
# Authentication
-LOGIN_URL = '/login/'
-LOGIN_REDIRECT_URL = '/'
-LOGOUT_REDIRECT_URL = '/login/'
+LOGIN_URL = "/login/"
+LOGIN_REDIRECT_URL = "/"
+LOGOUT_REDIRECT_URL = "/login/"
# Gramps integration
-GRAMPS_URL = os.environ.get('GRAMPS_URL', 'http://grampsweb:80')
-GRAMPS_API_TOKEN = os.environ.get('GRAMPS_API_TOKEN', '')
-GRAMPS_STIFTER_IDS = os.environ.get('GRAMPS_STIFTER_IDS', '') # comma-separated
-GRAMPS_USERNAME = os.environ.get('GRAMPS_USERNAME', '')
-GRAMPS_PASSWORD = os.environ.get('GRAMPS_PASSWORD', '')
+GRAMPS_URL = os.environ.get("GRAMPS_URL", "http://grampsweb:80")
+GRAMPS_API_TOKEN = os.environ.get("GRAMPS_API_TOKEN", "")
+GRAMPS_STIFTER_IDS = os.environ.get("GRAMPS_STIFTER_IDS", "") # comma-separated
+GRAMPS_USERNAME = os.environ.get("GRAMPS_USERNAME", "")
+GRAMPS_PASSWORD = os.environ.get("GRAMPS_PASSWORD", "")
diff --git a/app/core/urls.py b/app/core/urls.py
index 263950a..9811483 100644
--- a/app/core/urls.py
+++ b/app/core/urls.py
@@ -1,17 +1,21 @@
-from django.contrib import admin
-from django.urls import path, include
from django.conf import settings
from django.conf.urls.static import static
+from django.contrib import admin
from django.contrib.auth import views as auth_views
+from django.urls import include, path
+
from stiftung.views import home
urlpatterns = [
- path('', include('stiftung.urls')),
- path('admin/', admin.site.urls),
-
+ path("", include("stiftung.urls")),
+ path("admin/", admin.site.urls),
# Authentication URLs
- path('login/', auth_views.LoginView.as_view(template_name='registration/login.html'), name='login'),
- path('logout/', auth_views.LogoutView.as_view(), name='logout'),
+ path(
+ "login/",
+ auth_views.LoginView.as_view(template_name="registration/login.html"),
+ name="login",
+ ),
+ path("logout/", auth_views.LogoutView.as_view(), name="logout"),
]
if settings.DEBUG:
diff --git a/app/core/wsgi.py b/app/core/wsgi.py
index 4177f77..a8f1ede 100644
--- a/app/core/wsgi.py
+++ b/app/core/wsgi.py
@@ -1,4 +1,6 @@
import os
+
from django.core.wsgi import get_wsgi_application
-os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'core.settings')
+
+os.environ.setdefault("DJANGO_SETTINGS_MODULE", "core.settings")
application = get_wsgi_application()
diff --git a/app/stiftung/admin.py b/app/stiftung/admin.py
index dff21b6..911780b 100644
--- a/app/stiftung/admin.py
+++ b/app/stiftung/admin.py
@@ -1,42 +1,63 @@
from django.contrib import admin
-from django.utils.html import format_html
+from django.db.models import Count, Sum
from django.urls import reverse
-from django.db.models import Sum, Count
+from django.utils.html import format_html
from django.utils.safestring import mark_safe
+
from . import models
-from .models import (
- Person, Paechter, Destinataer, DokumentLink, Foerderung, Land, CSVImport,
- Rentmeister, StiftungsKonto, Verwaltungskosten, BankTransaction, AuditLog, BackupJob, AppConfiguration,
- DestinataerUnterstuetzung, UnterstuetzungWiederkehrend
-)
+from .models import (AppConfiguration, AuditLog, BackupJob, BankTransaction,
+ CSVImport, Destinataer, DestinataerUnterstuetzung,
+ DokumentLink, Foerderung, Land, Paechter, Person,
+ Rentmeister, StiftungsKonto, UnterstuetzungWiederkehrend,
+ Verwaltungskosten)
+
@admin.register(CSVImport)
class CSVImportAdmin(admin.ModelAdmin):
- list_display = ['import_type', 'filename', 'status', 'total_rows', 'imported_rows', 'failed_rows', 'created_by', 'started_at', 'duration_display']
- list_filter = ['import_type', 'status', 'started_at']
- search_fields = ['filename', 'created_by']
- readonly_fields = ['id', 'started_at', 'completed_at', 'get_success_rate']
- ordering = ['-started_at']
-
+ list_display = [
+ "import_type",
+ "filename",
+ "status",
+ "total_rows",
+ "imported_rows",
+ "failed_rows",
+ "created_by",
+ "started_at",
+ "duration_display",
+ ]
+ list_filter = ["import_type", "status", "started_at"]
+ search_fields = ["filename", "created_by"]
+ readonly_fields = ["id", "started_at", "completed_at", "get_success_rate"]
+ ordering = ["-started_at"]
+
fieldsets = (
- ('Grundinformationen', {
- 'fields': ('import_type', 'filename', 'file_size', 'status')
- }),
- ('Ergebnisse', {
- 'fields': ('total_rows', 'imported_rows', 'failed_rows', 'get_success_rate', 'error_log')
- }),
- ('Metadaten', {
- 'fields': ('created_by', 'started_at', 'completed_at')
- }),
+ (
+ "Grundinformationen",
+ {"fields": ("import_type", "filename", "file_size", "status")},
+ ),
+ (
+ "Ergebnisse",
+ {
+ "fields": (
+ "total_rows",
+ "imported_rows",
+ "failed_rows",
+ "get_success_rate",
+ "error_log",
+ )
+ },
+ ),
+ ("Metadaten", {"fields": ("created_by", "started_at", "completed_at")}),
)
-
+
def duration_display(self, obj):
duration = obj.get_duration()
if duration:
return f"{duration.total_seconds():.1f}s"
return "-"
+
duration_display.short_description = "Dauer"
-
+
def get_success_rate(self, obj):
rate = obj.get_success_rate()
if rate >= 90:
@@ -46,564 +67,836 @@ class CSVImportAdmin(admin.ModelAdmin):
else:
color = "danger"
return format_html('{:.1f}%', color, rate)
+
get_success_rate.short_description = "Erfolgsrate"
+
@admin.register(Person)
class PersonAdmin(admin.ModelAdmin):
- list_display = ['nachname', 'vorname', 'familienzweig', 'geburtsdatum', 'email', 'iban_display']
- list_filter = ['familienzweig', 'geburtsdatum']
- search_fields = ['nachname', 'vorname', 'email', 'familienzweig']
- ordering = ['nachname', 'vorname']
- readonly_fields = ['id']
-
+ list_display = [
+ "nachname",
+ "vorname",
+ "familienzweig",
+ "geburtsdatum",
+ "email",
+ "iban_display",
+ ]
+ list_filter = ["familienzweig", "geburtsdatum"]
+ search_fields = ["nachname", "vorname", "email", "familienzweig"]
+ ordering = ["nachname", "vorname"]
+ readonly_fields = ["id"]
+
fieldsets = (
- ('Persönliche Daten', {
- 'fields': ('vorname', 'nachname', 'geburtsdatum', 'email', 'telefon')
- }),
- ('Stiftungsdaten', {
- 'fields': ('familienzweig', 'iban', 'adresse')
- }),
- ('Zusätzlich', {
- 'fields': ('notizen', 'aktiv')
- }),
- ('System', {
- 'fields': ('id',),
- 'classes': ('collapse',)
- }),
+ (
+ "Persönliche Daten",
+ {"fields": ("vorname", "nachname", "geburtsdatum", "email", "telefon")},
+ ),
+ ("Stiftungsdaten", {"fields": ("familienzweig", "iban", "adresse")}),
+ ("Zusätzlich", {"fields": ("notizen", "aktiv")}),
+ ("System", {"fields": ("id",), "classes": ("collapse",)}),
)
-
+
def iban_display(self, obj):
if obj.iban:
- return format_html('{}', obj.iban)
- return '-'
- iban_display.short_description = 'IBAN'
-
+ return format_html(
+ '{}', obj.iban
+ )
+ return "-"
+
+ iban_display.short_description = "IBAN"
+
def get_queryset(self, request):
- return super().get_queryset(request).annotate(
- total_foerderungen=Sum('foerderung__betrag')
+ return (
+ super()
+ .get_queryset(request)
+ .annotate(total_foerderungen=Sum("foerderung__betrag"))
)
+
@admin.register(Paechter)
class PaechterAdmin(admin.ModelAdmin):
- list_display = ['nachname', 'vorname', 'pachtnummer', 'pachtzins_aktuell', 'landwirtschaftliche_ausbildung', 'aktiv']
- list_filter = ['landwirtschaftliche_ausbildung', 'aktiv']
- search_fields = ['nachname', 'vorname', 'email', 'pachtnummer']
- ordering = ['nachname', 'vorname']
- readonly_fields = ['id']
-
+ list_display = [
+ "nachname",
+ "vorname",
+ "pachtnummer",
+ "pachtzins_aktuell",
+ "landwirtschaftliche_ausbildung",
+ "aktiv",
+ ]
+ list_filter = ["landwirtschaftliche_ausbildung", "aktiv"]
+ search_fields = ["nachname", "vorname", "email", "pachtnummer"]
+ ordering = ["nachname", "vorname"]
+ readonly_fields = ["id"]
+
fieldsets = (
- ('Persönliche Daten', {
- 'fields': ('vorname', 'nachname', 'geburtsdatum', 'email', 'telefon')
- }),
- ('Pacht-Informationen', {
- 'fields': ('pachtnummer', 'pachtbeginn_erste', 'pachtende_letzte', 'pachtzins_aktuell')
- }),
- ('Landwirtschaftliche Qualifikation', {
- 'fields': ('landwirtschaftliche_ausbildung', 'berufserfahrung_jahre', 'spezialisierung')
- }),
- ('Kontaktdaten', {
- 'fields': ('iban', 'strasse', 'plz', 'ort')
- }),
- ('Pächter-Typ', {
- 'fields': ('personentyp',)
- }),
- ('Zusätzlich', {
- 'fields': ('notizen', 'aktiv')
- }),
- ('System', {
- 'fields': ('id',),
- 'classes': ('collapse',)
- }),
+ (
+ "Persönliche Daten",
+ {"fields": ("vorname", "nachname", "geburtsdatum", "email", "telefon")},
+ ),
+ (
+ "Pacht-Informationen",
+ {
+ "fields": (
+ "pachtnummer",
+ "pachtbeginn_erste",
+ "pachtende_letzte",
+ "pachtzins_aktuell",
+ )
+ },
+ ),
+ (
+ "Landwirtschaftliche Qualifikation",
+ {
+ "fields": (
+ "landwirtschaftliche_ausbildung",
+ "berufserfahrung_jahre",
+ "spezialisierung",
+ )
+ },
+ ),
+ ("Kontaktdaten", {"fields": ("iban", "strasse", "plz", "ort")}),
+ ("Pächter-Typ", {"fields": ("personentyp",)}),
+ ("Zusätzlich", {"fields": ("notizen", "aktiv")}),
+ ("System", {"fields": ("id",), "classes": ("collapse",)}),
)
-
+
def iban_display(self, obj):
if obj.iban:
- return format_html('{}', obj.iban)
- return '-'
- iban_display.short_description = 'IBAN'
+ return format_html(
+ '{}', obj.iban
+ )
+ return "-"
+
+ iban_display.short_description = "IBAN"
+
@admin.register(Destinataer)
class DestinataerAdmin(admin.ModelAdmin):
- list_display = ['nachname', 'vorname', 'familienzweig', 'berufsgruppe', 'institution', 'finanzielle_notlage', 'aktiv']
- list_filter = ['familienzweig', 'berufsgruppe', 'finanzielle_notlage', 'aktiv']
- search_fields = ['nachname', 'vorname', 'email', 'institution', 'familienzweig']
- ordering = ['nachname', 'vorname']
- readonly_fields = ['id']
-
+ list_display = [
+ "nachname",
+ "vorname",
+ "familienzweig",
+ "berufsgruppe",
+ "institution",
+ "finanzielle_notlage",
+ "aktiv",
+ ]
+ list_filter = ["familienzweig", "berufsgruppe", "finanzielle_notlage", "aktiv"]
+ search_fields = ["nachname", "vorname", "email", "institution", "familienzweig"]
+ ordering = ["nachname", "vorname"]
+ readonly_fields = ["id"]
+
fieldsets = (
- ('Persönliche Daten', {
- 'fields': ('vorname', 'nachname', 'geburtsdatum', 'email', 'telefon')
- }),
- ('Berufliche Informationen', {
- 'fields': ('berufsgruppe', 'ausbildungsstand', 'institution')
- }),
- ('Projekt & Finanzen', {
- 'fields': ('projekt_beschreibung', 'jaehrliches_einkommen', 'finanzielle_notlage')
- }),
- ('Stiftungsdaten', {
- 'fields': ('familienzweig', 'iban', 'strasse', 'plz', 'ort')
- }),
- ('Zusätzlich', {
- 'fields': ('notizen', 'aktiv')
- }),
- ('System', {
- 'fields': ('id',),
- 'classes': ('collapse',)
- }),
+ (
+ "Persönliche Daten",
+ {"fields": ("vorname", "nachname", "geburtsdatum", "email", "telefon")},
+ ),
+ (
+ "Berufliche Informationen",
+ {"fields": ("berufsgruppe", "ausbildungsstand", "institution")},
+ ),
+ (
+ "Projekt & Finanzen",
+ {
+ "fields": (
+ "projekt_beschreibung",
+ "jaehrliches_einkommen",
+ "finanzielle_notlage",
+ )
+ },
+ ),
+ (
+ "Stiftungsdaten",
+ {"fields": ("familienzweig", "iban", "strasse", "plz", "ort")},
+ ),
+ ("Zusätzlich", {"fields": ("notizen", "aktiv")}),
+ ("System", {"fields": ("id",), "classes": ("collapse",)}),
)
-
+
def iban_display(self, obj):
if obj.iban:
- return format_html('{}', obj.iban)
- return '-'
- iban_display.short_description = 'IBAN'
+ return format_html(
+ '{}', obj.iban
+ )
+ return "-"
+
+ iban_display.short_description = "IBAN"
+
@admin.register(Land)
class LandAdmin(admin.ModelAdmin):
list_display = [
- 'lfd_nr', 'gemeinde', 'gemarkung', 'flur', 'flurstueck',
- 'groesse_qm', 'verp_flaeche_aktuell', 'verpachtungsgrad_display', 'aktiv'
+ "lfd_nr",
+ "gemeinde",
+ "gemarkung",
+ "flur",
+ "flurstueck",
+ "groesse_qm",
+ "verp_flaeche_aktuell",
+ "verpachtungsgrad_display",
+ "aktiv",
]
- list_filter = ['gemeinde', 'gemarkung', 'aktiv']
- search_fields = ['lfd_nr', 'gemeinde', 'gemarkung', 'flur', 'flurstueck']
- ordering = ['gemeinde', 'gemarkung', 'flur', 'flurstueck']
- readonly_fields = ['id', 'gesamtflaeche_berechnet', 'verpachtungsgrad_berechnet']
-
+ list_filter = ["gemeinde", "gemarkung", "aktiv"]
+ search_fields = ["lfd_nr", "gemeinde", "gemarkung", "flur", "flurstueck"]
+ ordering = ["gemeinde", "gemarkung", "flur", "flurstueck"]
+ readonly_fields = ["id", "gesamtflaeche_berechnet", "verpachtungsgrad_berechnet"]
+
fieldsets = (
- ('Identifikation', {
- 'fields': ('lfd_nr', 'ew_nummer')
- }),
- ('Gerichtliche Zuständigkeit', {
- 'fields': ('amtsgericht',)
- }),
- ('Verwaltungsstruktur', {
- 'fields': ('gemeinde', 'gemarkung', 'flur', 'flurstueck')
- }),
- ('Flächenangaben', {
- 'fields': ('groesse_qm', 'gruenland_qm', 'acker_qm', 'wald_qm', 'sonstiges_qm')
- }),
- ('Verpachtung', {
- 'fields': ('verpachtete_gesamtflaeche', 'flaeche_alte_liste', 'verp_flaeche_aktuell')
- }),
- ('Steuern und Abgaben', {
- 'fields': ('anteil_grundsteuer', 'anteil_lwk')
- }),
- ('Status', {
- 'fields': ('aktiv', 'notizen')
- }),
- ('System', {
- 'fields': ('id', 'erstellt_am', 'aktualisiert_am'),
- 'classes': ('collapse',)
- }),
+ ("Identifikation", {"fields": ("lfd_nr", "ew_nummer")}),
+ ("Gerichtliche Zuständigkeit", {"fields": ("amtsgericht",)}),
+ (
+ "Verwaltungsstruktur",
+ {"fields": ("gemeinde", "gemarkung", "flur", "flurstueck")},
+ ),
+ (
+ "Flächenangaben",
+ {
+ "fields": (
+ "groesse_qm",
+ "gruenland_qm",
+ "acker_qm",
+ "wald_qm",
+ "sonstiges_qm",
+ )
+ },
+ ),
+ (
+ "Verpachtung",
+ {
+ "fields": (
+ "verpachtete_gesamtflaeche",
+ "flaeche_alte_liste",
+ "verp_flaeche_aktuell",
+ )
+ },
+ ),
+ ("Steuern und Abgaben", {"fields": ("anteil_grundsteuer", "anteil_lwk")}),
+ ("Status", {"fields": ("aktiv", "notizen")}),
+ (
+ "System",
+ {
+ "fields": ("id", "erstellt_am", "aktualisiert_am"),
+ "classes": ("collapse",),
+ },
+ ),
)
-
+
def verpachtungsgrad_display(self, obj):
grad = obj.get_verpachtungsgrad()
if grad > 90:
- color = 'green'
+ color = "green"
elif grad > 70:
- color = 'orange'
+ color = "orange"
else:
- color = 'red'
+ color = "red"
return format_html('{:.1f}%', color, grad)
- verpachtungsgrad_display.short_description = 'Verpachtungsgrad'
-
+
+ verpachtungsgrad_display.short_description = "Verpachtungsgrad"
+
def gesamtflaeche_berechnet(self, obj):
return f"{obj.get_gesamtflaeche():.2f} qm"
- gesamtflaeche_berechnet.short_description = 'Berechnete Gesamtfläche'
-
+
+ gesamtflaeche_berechnet.short_description = "Berechnete Gesamtfläche"
+
def verpachtungsgrad_berechnet(self, obj):
return f"{obj.get_verpachtungsgrad():.1f}%"
- verpachtungsgrad_berechnet.short_description = 'Verpachtungsgrad'
+
+ verpachtungsgrad_berechnet.short_description = "Verpachtungsgrad"
+
@admin.register(DokumentLink)
class DokumentLinkAdmin(admin.ModelAdmin):
- list_display = ['titel', 'kontext', 'paperless_document_id']
- list_filter = ['kontext']
- search_fields = ['titel', 'kontext']
- ordering = ['titel']
- readonly_fields = ['id']
-
+ list_display = ["titel", "kontext", "paperless_document_id"]
+ list_filter = ["kontext"]
+ search_fields = ["titel", "kontext"]
+ ordering = ["titel"]
+ readonly_fields = ["id"]
+
fieldsets = (
- ('Dokument', {
- 'fields': ('titel', 'kontext', 'paperless_document_id', 'beschreibung')
- }),
- ('System', {
- 'fields': ('id',),
- 'classes': ('collapse',)
- }),
+ (
+ "Dokument",
+ {"fields": ("titel", "kontext", "paperless_document_id", "beschreibung")},
+ ),
+ ("System", {"fields": ("id",), "classes": ("collapse",)}),
)
+
@admin.register(Foerderung)
class FoerderungAdmin(admin.ModelAdmin):
- list_display = ['destinataer', 'jahr', 'betrag', 'verwendungsnachweis_link', 'total_for_destinataer']
- list_filter = ['jahr', 'destinataer__familienzweig']
- search_fields = ['destinataer__nachname', 'destinataer__vorname', 'destinataer__familienzweig']
- ordering = ['-jahr', '-betrag']
- readonly_fields = ['id']
-
+ list_display = [
+ "destinataer",
+ "jahr",
+ "betrag",
+ "verwendungsnachweis_link",
+ "total_for_destinataer",
+ ]
+ list_filter = ["jahr", "destinataer__familienzweig"]
+ search_fields = [
+ "destinataer__nachname",
+ "destinataer__vorname",
+ "destinataer__familienzweig",
+ ]
+ ordering = ["-jahr", "-betrag"]
+ readonly_fields = ["id"]
+
fieldsets = (
- ('Förderung', {
- 'fields': ('destinataer', 'person', 'jahr', 'betrag', 'kategorie', 'status')
- }),
- ('Dokumentation', {
- 'fields': ('verwendungsnachweis', 'bemerkungen')
- }),
- ('Daten', {
- 'fields': ('antragsdatum', 'entscheidungsdatum')
- }),
- ('System', {
- 'fields': ('id',),
- 'classes': ('collapse',)
- }),
+ (
+ "Förderung",
+ {
+ "fields": (
+ "destinataer",
+ "person",
+ "jahr",
+ "betrag",
+ "kategorie",
+ "status",
+ )
+ },
+ ),
+ ("Dokumentation", {"fields": ("verwendungsnachweis", "bemerkungen")}),
+ ("Daten", {"fields": ("antragsdatum", "entscheidungsdatum")}),
+ ("System", {"fields": ("id",), "classes": ("collapse",)}),
)
-
+
def verwendungsnachweis_link(self, obj):
if obj.verwendungsnachweis:
return format_html(
'{}',
- reverse('admin:stiftung_dokumentlink_change', args=[obj.verwendungsnachweis.id]),
- obj.verwendungsnachweis.titel
+ reverse(
+ "admin:stiftung_dokumentlink_change",
+ args=[obj.verwendungsnachweis.id],
+ ),
+ obj.verwendungsnachweis.titel,
)
- return '-'
- verwendungsnachweis_link.short_description = 'Verwendungsnachweis'
-
+ return "-"
+
+ verwendungsnachweis_link.short_description = "Verwendungsnachweis"
+
def total_for_destinataer(self, obj):
- total = Foerderung.objects.filter(destinataer=obj.destinataer).aggregate(Sum('betrag'))['betrag__sum'] or 0
+ total = (
+ Foerderung.objects.filter(destinataer=obj.destinataer).aggregate(
+ Sum("betrag")
+ )["betrag__sum"]
+ or 0
+ )
return f"€{total:,.2f}"
- total_for_destinataer.short_description = 'Gesamt für Destinatär'
+
+ total_for_destinataer.short_description = "Gesamt für Destinatär"
@admin.register(Rentmeister)
class RentmeisterAdmin(admin.ModelAdmin):
- list_display = ['__str__', 'email', 'telefon', 'seit_datum', 'bis_datum', 'aktiv', 'monatliche_verguetung']
- list_filter = ['aktiv', 'seit_datum', 'anrede']
- search_fields = ['vorname', 'nachname', 'email', 'telefon', 'ort']
- ordering = ['nachname', 'vorname']
- readonly_fields = ['id', 'erstellt_am', 'aktualisiert_am']
-
+ list_display = [
+ "__str__",
+ "email",
+ "telefon",
+ "seit_datum",
+ "bis_datum",
+ "aktiv",
+ "monatliche_verguetung",
+ ]
+ list_filter = ["aktiv", "seit_datum", "anrede"]
+ search_fields = ["vorname", "nachname", "email", "telefon", "ort"]
+ ordering = ["nachname", "vorname"]
+ readonly_fields = ["id", "erstellt_am", "aktualisiert_am"]
+
fieldsets = (
- ('Persönliche Daten', {
- 'fields': ('anrede', 'vorname', 'nachname', 'titel')
- }),
- ('Kontaktdaten', {
- 'fields': ('email', 'telefon', 'mobil', 'strasse', 'plz', 'ort')
- }),
- ('Bankdaten', {
- 'fields': ('iban', 'bic', 'bank_name'),
- 'classes': ['collapse']
- }),
- ('Stiftungsdaten', {
- 'fields': ('seit_datum', 'bis_datum', 'aktiv', 'monatliche_verguetung', 'km_pauschale')
- }),
- ('Zusätzliche Informationen', {
- 'fields': ('notizen',),
- 'classes': ['collapse']
- }),
- ('System', {
- 'fields': ('id', 'erstellt_am', 'aktualisiert_am'),
- 'classes': ['collapse']
- })
+ ("Persönliche Daten", {"fields": ("anrede", "vorname", "nachname", "titel")}),
+ (
+ "Kontaktdaten",
+ {"fields": ("email", "telefon", "mobil", "strasse", "plz", "ort")},
+ ),
+ (
+ "Bankdaten",
+ {"fields": ("iban", "bic", "bank_name"), "classes": ["collapse"]},
+ ),
+ (
+ "Stiftungsdaten",
+ {
+ "fields": (
+ "seit_datum",
+ "bis_datum",
+ "aktiv",
+ "monatliche_verguetung",
+ "km_pauschale",
+ )
+ },
+ ),
+ (
+ "Zusätzliche Informationen",
+ {"fields": ("notizen",), "classes": ["collapse"]},
+ ),
+ (
+ "System",
+ {
+ "fields": ("id", "erstellt_am", "aktualisiert_am"),
+ "classes": ["collapse"],
+ },
+ ),
)
@admin.register(StiftungsKonto)
class StiftungsKontoAdmin(admin.ModelAdmin):
- list_display = ['kontoname', 'bank_name', 'konto_typ', 'saldo', 'saldo_datum', 'aktiv']
- list_filter = ['konto_typ', 'aktiv', 'bank_name']
- search_fields = ['kontoname', 'bank_name', 'iban']
- ordering = ['bank_name', 'kontoname']
- readonly_fields = ['id', 'erstellt_am', 'aktualisiert_am']
-
+ list_display = [
+ "kontoname",
+ "bank_name",
+ "konto_typ",
+ "saldo",
+ "saldo_datum",
+ "aktiv",
+ ]
+ list_filter = ["konto_typ", "aktiv", "bank_name"]
+ search_fields = ["kontoname", "bank_name", "iban"]
+ ordering = ["bank_name", "kontoname"]
+ readonly_fields = ["id", "erstellt_am", "aktualisiert_am"]
+
fieldsets = (
- ('Kontodaten', {
- 'fields': ('kontoname', 'bank_name', 'iban', 'bic', 'konto_typ')
- }),
- ('Finanzdaten', {
- 'fields': ('saldo', 'saldo_datum', 'zinssatz', 'laufzeit_bis')
- }),
- ('Status', {
- 'fields': ('aktiv', 'notizen')
- }),
- ('System', {
- 'fields': ('id', 'erstellt_am', 'aktualisiert_am'),
- 'classes': ['collapse']
- })
+ (
+ "Kontodaten",
+ {"fields": ("kontoname", "bank_name", "iban", "bic", "konto_typ")},
+ ),
+ (
+ "Finanzdaten",
+ {"fields": ("saldo", "saldo_datum", "zinssatz", "laufzeit_bis")},
+ ),
+ ("Status", {"fields": ("aktiv", "notizen")}),
+ (
+ "System",
+ {
+ "fields": ("id", "erstellt_am", "aktualisiert_am"),
+ "classes": ["collapse"],
+ },
+ ),
)
@admin.register(Verwaltungskosten)
class VerwaltungskostenAdmin(admin.ModelAdmin):
- list_display = ['bezeichnung', 'kategorie', 'betrag', 'datum', 'status', 'rentmeister', 'konto']
- list_filter = ['kategorie', 'status', 'datum', 'rentmeister', 'konto']
- search_fields = ['bezeichnung', 'lieferant_firma', 'rechnungsnummer', 'beschreibung']
- ordering = ['-datum', '-erstellt_am']
- readonly_fields = ['id', 'erstellt_am', 'aktualisiert_am']
- date_hierarchy = 'datum'
-
+ list_display = [
+ "bezeichnung",
+ "kategorie",
+ "betrag",
+ "datum",
+ "status",
+ "rentmeister",
+ "konto",
+ ]
+ list_filter = ["kategorie", "status", "datum", "rentmeister", "konto"]
+ search_fields = [
+ "bezeichnung",
+ "lieferant_firma",
+ "rechnungsnummer",
+ "beschreibung",
+ ]
+ ordering = ["-datum", "-erstellt_am"]
+ readonly_fields = ["id", "erstellt_am", "aktualisiert_am"]
+ date_hierarchy = "datum"
+
fieldsets = (
- ('Grunddaten', {
- 'fields': ('bezeichnung', 'kategorie', 'betrag', 'datum', 'status')
- }),
- ('Zuordnung', {
- 'fields': ('rentmeister', 'konto')
- }),
- ('Lieferant/Rechnung', {
- 'fields': ('lieferant_firma', 'rechnungsnummer'),
- 'classes': ['collapse']
- }),
- ('Fahrtkosten', {
- 'fields': ('km_anzahl', 'km_satz', 'von_ort', 'nach_ort', 'zweck'),
- 'classes': ['collapse'],
- 'description': 'Nur für Kategorie "Fahrtkosten" relevant'
- }),
- ('Zusätzliche Informationen', {
- 'fields': ('beschreibung', 'notizen'),
- 'classes': ['collapse']
- }),
- ('System', {
- 'fields': ('id', 'erstellt_am', 'aktualisiert_am'),
- 'classes': ['collapse']
- })
+ (
+ "Grunddaten",
+ {"fields": ("bezeichnung", "kategorie", "betrag", "datum", "status")},
+ ),
+ ("Zuordnung", {"fields": ("rentmeister", "konto")}),
+ (
+ "Lieferant/Rechnung",
+ {"fields": ("lieferant_firma", "rechnungsnummer"), "classes": ["collapse"]},
+ ),
+ (
+ "Fahrtkosten",
+ {
+ "fields": ("km_anzahl", "km_satz", "von_ort", "nach_ort", "zweck"),
+ "classes": ["collapse"],
+ "description": 'Nur für Kategorie "Fahrtkosten" relevant',
+ },
+ ),
+ (
+ "Zusätzliche Informationen",
+ {"fields": ("beschreibung", "notizen"), "classes": ["collapse"]},
+ ),
+ (
+ "System",
+ {
+ "fields": ("id", "erstellt_am", "aktualisiert_am"),
+ "classes": ["collapse"],
+ },
+ ),
)
@admin.register(BankTransaction)
class BankTransactionAdmin(admin.ModelAdmin):
list_display = [
- 'datum', 'konto', 'betrag', 'empfaenger_zahlungspflichtiger',
- 'transaction_type', 'status', 'verwaltungskosten'
+ "datum",
+ "konto",
+ "betrag",
+ "empfaenger_zahlungspflichtiger",
+ "transaction_type",
+ "status",
+ "verwaltungskosten",
]
- list_filter = [
- 'konto', 'transaction_type', 'status', 'datum', 'importiert_am'
- ]
- search_fields = [
- 'verwendungszweck', 'empfaenger_zahlungspflichtiger', 'referenz'
- ]
- readonly_fields = ['importiert_am', 'import_datei']
- ordering = ['-datum', '-importiert_am']
-
+ list_filter = ["konto", "transaction_type", "status", "datum", "importiert_am"]
+ search_fields = ["verwendungszweck", "empfaenger_zahlungspflichtiger", "referenz"]
+ readonly_fields = ["importiert_am", "import_datei"]
+ ordering = ["-datum", "-importiert_am"]
+
fieldsets = (
- ('Basisdaten', {
- 'fields': ('konto', 'datum', 'valuta', 'betrag', 'waehrung')
- }),
- ('Transaktionsdetails', {
- 'fields': ('verwendungszweck', 'empfaenger_zahlungspflichtiger', 'iban_gegenpartei', 'bic_gegenpartei', 'referenz', 'transaction_type')
- }),
- ('Verwaltung', {
- 'fields': ('status', 'kommentare', 'verwaltungskosten')
- }),
- ('Import-Information', {
- 'fields': ('import_datei', 'importiert_am', 'saldo_nach_buchung'),
- 'classes': ('collapse',)
- }),
+ ("Basisdaten", {"fields": ("konto", "datum", "valuta", "betrag", "waehrung")}),
+ (
+ "Transaktionsdetails",
+ {
+ "fields": (
+ "verwendungszweck",
+ "empfaenger_zahlungspflichtiger",
+ "iban_gegenpartei",
+ "bic_gegenpartei",
+ "referenz",
+ "transaction_type",
+ )
+ },
+ ),
+ ("Verwaltung", {"fields": ("status", "kommentare", "verwaltungskosten")}),
+ (
+ "Import-Information",
+ {
+ "fields": ("import_datei", "importiert_am", "saldo_nach_buchung"),
+ "classes": ("collapse",),
+ },
+ ),
)
-
+
def get_queryset(self, request):
- return super().get_queryset(request).select_related('konto', 'verwaltungskosten')
+ return (
+ super().get_queryset(request).select_related("konto", "verwaltungskosten")
+ )
@admin.register(AuditLog)
class AuditLogAdmin(admin.ModelAdmin):
- list_display = ['timestamp', 'username', 'action', 'entity_type', 'entity_name', 'ip_address']
- list_filter = ['action', 'entity_type', 'timestamp', 'username']
- search_fields = ['username', 'entity_name', 'description', 'ip_address']
- readonly_fields = ['id', 'timestamp', 'user', 'username', 'action', 'entity_type', 'entity_id', 'entity_name', 'description', 'changes', 'ip_address', 'user_agent', 'session_key']
- ordering = ['-timestamp']
- date_hierarchy = 'timestamp'
-
+ list_display = [
+ "timestamp",
+ "username",
+ "action",
+ "entity_type",
+ "entity_name",
+ "ip_address",
+ ]
+ list_filter = ["action", "entity_type", "timestamp", "username"]
+ search_fields = ["username", "entity_name", "description", "ip_address"]
+ readonly_fields = [
+ "id",
+ "timestamp",
+ "user",
+ "username",
+ "action",
+ "entity_type",
+ "entity_id",
+ "entity_name",
+ "description",
+ "changes",
+ "ip_address",
+ "user_agent",
+ "session_key",
+ ]
+ ordering = ["-timestamp"]
+ date_hierarchy = "timestamp"
+
fieldsets = (
- ('Benutzer und Zeit', {
- 'fields': ('timestamp', 'user', 'username', 'session_key')
- }),
- ('Aktion', {
- 'fields': ('action', 'entity_type', 'entity_id', 'entity_name', 'description')
- }),
- ('Änderungsdetails', {
- 'fields': ('changes',),
- 'classes': ['collapse']
- }),
- ('Request-Informationen', {
- 'fields': ('ip_address', 'user_agent'),
- 'classes': ['collapse']
- }),
- ('System', {
- 'fields': ('id',),
- 'classes': ['collapse']
- })
+ (
+ "Benutzer und Zeit",
+ {"fields": ("timestamp", "user", "username", "session_key")},
+ ),
+ (
+ "Aktion",
+ {
+ "fields": (
+ "action",
+ "entity_type",
+ "entity_id",
+ "entity_name",
+ "description",
+ )
+ },
+ ),
+ ("Änderungsdetails", {"fields": ("changes",), "classes": ["collapse"]}),
+ (
+ "Request-Informationen",
+ {"fields": ("ip_address", "user_agent"), "classes": ["collapse"]},
+ ),
+ ("System", {"fields": ("id",), "classes": ["collapse"]}),
)
-
+
def has_add_permission(self, request):
return False # Don't allow manual creation
-
+
def has_change_permission(self, request, obj=None):
return False # Don't allow editing
@admin.register(BackupJob)
class BackupJobAdmin(admin.ModelAdmin):
- list_display = ['created_at', 'backup_type', 'status', 'backup_size_display', 'duration_display', 'created_by']
- list_filter = ['backup_type', 'status', 'created_at']
- search_fields = ['backup_filename', 'created_by__username']
- readonly_fields = ['id', 'created_at', 'started_at', 'completed_at', 'backup_size', 'get_duration']
- ordering = ['-created_at']
-
+ list_display = [
+ "created_at",
+ "backup_type",
+ "status",
+ "backup_size_display",
+ "duration_display",
+ "created_by",
+ ]
+ list_filter = ["backup_type", "status", "created_at"]
+ search_fields = ["backup_filename", "created_by__username"]
+ readonly_fields = [
+ "id",
+ "created_at",
+ "started_at",
+ "completed_at",
+ "backup_size",
+ "get_duration",
+ ]
+ ordering = ["-created_at"]
+
fieldsets = (
- ('Job-Details', {
- 'fields': ('backup_type', 'status', 'created_by')
- }),
- ('Zeitpunkte', {
- 'fields': ('created_at', 'started_at', 'completed_at', 'get_duration')
- }),
- ('Ergebnis', {
- 'fields': ('backup_filename', 'backup_size', 'database_size', 'files_count')
- }),
- ('Fehlerbehandlung', {
- 'fields': ('error_message',),
- 'classes': ['collapse']
- }),
- ('System', {
- 'fields': ('id',),
- 'classes': ['collapse']
- })
+ ("Job-Details", {"fields": ("backup_type", "status", "created_by")}),
+ (
+ "Zeitpunkte",
+ {"fields": ("created_at", "started_at", "completed_at", "get_duration")},
+ ),
+ (
+ "Ergebnis",
+ {
+ "fields": (
+ "backup_filename",
+ "backup_size",
+ "database_size",
+ "files_count",
+ )
+ },
+ ),
+ ("Fehlerbehandlung", {"fields": ("error_message",), "classes": ["collapse"]}),
+ ("System", {"fields": ("id",), "classes": ["collapse"]}),
)
-
+
def backup_size_display(self, obj):
return obj.get_size_display()
- backup_size_display.short_description = 'Backup-Größe'
-
+
+ backup_size_display.short_description = "Backup-Größe"
+
def duration_display(self, obj):
duration = obj.get_duration()
if duration:
return f"{duration.total_seconds():.1f}s"
return "-"
+
duration_display.short_description = "Dauer"
-
+
def has_add_permission(self, request):
return False # Use the web interface for creating backups
@admin.register(AppConfiguration)
class AppConfigurationAdmin(admin.ModelAdmin):
- list_display = ['display_name', 'key', 'value_display', 'category', 'setting_type', 'is_active', 'updated_at']
- list_filter = ['category', 'setting_type', 'is_active']
- search_fields = ['key', 'display_name', 'description']
- readonly_fields = ['id', 'created_at', 'updated_at']
- ordering = ['category', 'order', 'display_name']
-
+ list_display = [
+ "display_name",
+ "key",
+ "value_display",
+ "category",
+ "setting_type",
+ "is_active",
+ "updated_at",
+ ]
+ list_filter = ["category", "setting_type", "is_active"]
+ search_fields = ["key", "display_name", "description"]
+ readonly_fields = ["id", "created_at", "updated_at"]
+ ordering = ["category", "order", "display_name"]
+
fieldsets = (
- ('Basic Information', {
- 'fields': ('key', 'display_name', 'description', 'category', 'setting_type')
- }),
- ('Value Configuration', {
- 'fields': ('value', 'default_value')
- }),
- ('Options', {
- 'fields': ('is_active', 'is_system', 'order')
- }),
- ('Metadata', {
- 'fields': ('created_at', 'updated_at'),
- 'classes': ('collapse',)
- }),
+ (
+ "Basic Information",
+ {
+ "fields": (
+ "key",
+ "display_name",
+ "description",
+ "category",
+ "setting_type",
+ )
+ },
+ ),
+ ("Value Configuration", {"fields": ("value", "default_value")}),
+ ("Options", {"fields": ("is_active", "is_system", "order")}),
+ (
+ "Metadata",
+ {"fields": ("created_at", "updated_at"), "classes": ("collapse",)},
+ ),
)
-
+
def value_display(self, obj):
"""Display value with type formatting"""
value = obj.value
- if obj.setting_type == 'boolean':
- icon = '✅' if obj.get_typed_value() else '❌'
- return format_html('{} {}', icon, value)
- elif obj.setting_type == 'url':
- return format_html('{}', value, value[:50] + '...' if len(value) > 50 else value)
+ if obj.setting_type == "boolean":
+ icon = "✅" if obj.get_typed_value() else "❌"
+ return format_html("{} {}", icon, value)
+ elif obj.setting_type == "url":
+ return format_html(
+ '{}',
+ value,
+ value[:50] + "..." if len(value) > 50 else value,
+ )
elif len(value) > 100:
- return value[:100] + '...'
+ return value[:100] + "..."
return value
- value_display.short_description = 'Current Value'
-
+
+ value_display.short_description = "Current Value"
+
def get_readonly_fields(self, request, obj=None):
readonly = list(self.readonly_fields)
if obj and obj.is_system:
- readonly.extend(['key', 'setting_type', 'is_system'])
+ readonly.extend(["key", "setting_type", "is_system"])
return readonly
@admin.register(DestinataerUnterstuetzung)
class DestinataerUnterstuetzungAdmin(admin.ModelAdmin):
- list_display = ['__str__', 'destinataer', 'betrag', 'faellig_am', 'status', 'wiederkehrend_von', 'ausgezahlt_am']
- list_filter = ['status', 'faellig_am', 'erstellt_am', 'konto']
- search_fields = ['destinataer__vorname', 'destinataer__nachname', 'beschreibung', 'empfaenger_name']
- readonly_fields = ['id', 'erstellt_am', 'aktualisiert_am']
-
+ list_display = [
+ "__str__",
+ "destinataer",
+ "betrag",
+ "faellig_am",
+ "status",
+ "wiederkehrend_von",
+ "ausgezahlt_am",
+ ]
+ list_filter = ["status", "faellig_am", "erstellt_am", "konto"]
+ search_fields = [
+ "destinataer__vorname",
+ "destinataer__nachname",
+ "beschreibung",
+ "empfaenger_name",
+ ]
+ readonly_fields = ["id", "erstellt_am", "aktualisiert_am"]
+
fieldsets = (
- ('Grundinformationen', {
- 'fields': ('destinataer', 'konto', 'betrag', 'faellig_am', 'status', 'beschreibung')
- }),
- ('Überweisungsdaten', {
- 'fields': ('empfaenger_iban', 'empfaenger_name', 'verwendungszweck')
- }),
- ('Zahlungsinformationen', {
- 'fields': ('ausgezahlt_am', 'ausgezahlt_von')
- }),
- ('Wiederkehrend', {
- 'fields': ('wiederkehrend_von',)
- }),
- ('Metadaten', {
- 'fields': ('id', 'erstellt_am', 'aktualisiert_am'),
- 'classes': ('collapse',)
- }),
+ (
+ "Grundinformationen",
+ {
+ "fields": (
+ "destinataer",
+ "konto",
+ "betrag",
+ "faellig_am",
+ "status",
+ "beschreibung",
+ )
+ },
+ ),
+ (
+ "Überweisungsdaten",
+ {"fields": ("empfaenger_iban", "empfaenger_name", "verwendungszweck")},
+ ),
+ ("Zahlungsinformationen", {"fields": ("ausgezahlt_am", "ausgezahlt_von")}),
+ ("Wiederkehrend", {"fields": ("wiederkehrend_von",)}),
+ (
+ "Metadaten",
+ {
+ "fields": ("id", "erstellt_am", "aktualisiert_am"),
+ "classes": ("collapse",),
+ },
+ ),
)
-@admin.register(UnterstuetzungWiederkehrend)
+@admin.register(UnterstuetzungWiederkehrend)
class UnterstuetzungWiederkehrendAdmin(admin.ModelAdmin):
- list_display = ['__str__', 'destinataer', 'betrag', 'intervall', 'aktiv', 'naechste_generierung']
- list_filter = ['intervall', 'aktiv', 'erstellt_am']
- search_fields = ['destinataer__vorname', 'destinataer__nachname', 'beschreibung', 'empfaenger_name']
- readonly_fields = ['id', 'erstellt_am']
-
+ list_display = [
+ "__str__",
+ "destinataer",
+ "betrag",
+ "intervall",
+ "aktiv",
+ "naechste_generierung",
+ ]
+ list_filter = ["intervall", "aktiv", "erstellt_am"]
+ search_fields = [
+ "destinataer__vorname",
+ "destinataer__nachname",
+ "beschreibung",
+ "empfaenger_name",
+ ]
+ readonly_fields = ["id", "erstellt_am"]
+
fieldsets = (
- ('Grundinformationen', {
- 'fields': ('destinataer', 'konto', 'betrag', 'intervall', 'beschreibung', 'aktiv')
- }),
- ('Überweisungsdaten', {
- 'fields': ('empfaenger_iban', 'empfaenger_name', 'verwendungszweck')
- }),
- ('Zeitplanung', {
- 'fields': ('erste_zahlung_am', 'letzte_zahlung_am', 'naechste_generierung')
- }),
- ('Metadaten', {
- 'fields': ('id', 'erstellt_von', 'erstellt_am'),
- 'classes': ('collapse',)
- }),
+ (
+ "Grundinformationen",
+ {
+ "fields": (
+ "destinataer",
+ "konto",
+ "betrag",
+ "intervall",
+ "beschreibung",
+ "aktiv",
+ )
+ },
+ ),
+ (
+ "Überweisungsdaten",
+ {"fields": ("empfaenger_iban", "empfaenger_name", "verwendungszweck")},
+ ),
+ (
+ "Zeitplanung",
+ {
+ "fields": (
+ "erste_zahlung_am",
+ "letzte_zahlung_am",
+ "naechste_generierung",
+ )
+ },
+ ),
+ (
+ "Metadaten",
+ {"fields": ("id", "erstellt_von", "erstellt_am"), "classes": ("collapse",)},
+ ),
)
@admin.register(models.HelpBox)
class HelpBoxAdmin(admin.ModelAdmin):
- list_display = ['get_page_display', 'title', 'is_active', 'updated_at', 'updated_by']
- list_filter = ['page_key', 'is_active', 'updated_at']
- search_fields = ['title', 'content']
-
+ list_display = [
+ "get_page_display",
+ "title",
+ "is_active",
+ "updated_at",
+ "updated_by",
+ ]
+ list_filter = ["page_key", "is_active", "updated_at"]
+ search_fields = ["title", "content"]
+
fieldsets = (
- ('Grundinformationen', {
- 'fields': ('page_key', 'title', 'is_active')
- }),
- ('Inhalt', {
- 'fields': ('content',),
- 'description': 'Markdown wird unterstützt: **fett**, *kursiv*, `code`, [Link](url), Listen mit - oder 1.'
- }),
- ('Metadaten', {
- 'fields': ('created_at', 'updated_at', 'created_by', 'updated_by'),
- 'classes': ('collapse',)
- }),
+ ("Grundinformationen", {"fields": ("page_key", "title", "is_active")}),
+ (
+ "Inhalt",
+ {
+ "fields": ("content",),
+ "description": "Markdown wird unterstützt: **fett**, *kursiv*, `code`, [Link](url), Listen mit - oder 1.",
+ },
+ ),
+ (
+ "Metadaten",
+ {
+ "fields": ("created_at", "updated_at", "created_by", "updated_by"),
+ "classes": ("collapse",),
+ },
+ ),
)
-
- readonly_fields = ['created_at', 'updated_at']
-
+
+ readonly_fields = ["created_at", "updated_at"]
+
def get_page_display(self, obj):
return obj.get_page_key_display()
+
get_page_display.short_description = "Seite"
-
+
def save_model(self, request, obj, form, change):
if not change: # Neues Objekt
obj.created_by = request.user.username
diff --git a/app/stiftung/apps.py b/app/stiftung/apps.py
index a72a263..677c8e7 100644
--- a/app/stiftung/apps.py
+++ b/app/stiftung/apps.py
@@ -1,5 +1,6 @@
from django.apps import AppConfig
+
class StiftungConfig(AppConfig):
- default_auto_field = 'django.db.models.BigAutoField'
- name = 'stiftung'
+ default_auto_field = "django.db.models.BigAutoField"
+ name = "stiftung"
diff --git a/app/stiftung/audit.py b/app/stiftung/audit.py
index d6b5b6b..6e0a7ba 100644
--- a/app/stiftung/audit.py
+++ b/app/stiftung/audit.py
@@ -4,8 +4,10 @@ Provides functions to log user actions throughout the application
"""
import json
-from django.utils import timezone
+
from django.contrib.auth import get_user_model
+from django.utils import timezone
+
from stiftung.models import AuditLog
User = get_user_model()
@@ -13,18 +15,20 @@ User = get_user_model()
def get_client_ip(request):
"""Extract the client IP address from the request"""
- x_forwarded_for = request.META.get('HTTP_X_FORWARDED_FOR')
+ x_forwarded_for = request.META.get("HTTP_X_FORWARDED_FOR")
if x_forwarded_for:
- ip = x_forwarded_for.split(',')[0]
+ ip = x_forwarded_for.split(",")[0]
else:
- ip = request.META.get('REMOTE_ADDR')
+ ip = request.META.get("REMOTE_ADDR")
return ip
-def log_action(request, action, entity_type, entity_id, entity_name, description, changes=None):
+def log_action(
+ request, action, entity_type, entity_id, entity_name, description, changes=None
+):
"""
Log a user action to the audit log
-
+
Args:
request: Django request object
action: Action type (create, update, delete, etc.)
@@ -35,28 +39,28 @@ def log_action(request, action, entity_type, entity_id, entity_name, description
changes: Dictionary of field changes (optional)
"""
user = request.user if request.user.is_authenticated else None
- username = user.username if user else 'Anonymous'
-
+ username = user.username if user else "Anonymous"
+
# Get request metadata
ip_address = get_client_ip(request)
- user_agent = request.META.get('HTTP_USER_AGENT', '')
- session_key = request.session.session_key if hasattr(request, 'session') else ''
-
+ user_agent = request.META.get("HTTP_USER_AGENT", "")
+ session_key = request.session.session_key if hasattr(request, "session") else ""
+
# Create audit log entry
audit_entry = AuditLog.objects.create(
user=user,
username=username,
action=action,
entity_type=entity_type,
- entity_id=str(entity_id) if entity_id else '',
+ entity_id=str(entity_id) if entity_id else "",
entity_name=entity_name,
description=description,
changes=changes,
ip_address=ip_address,
user_agent=user_agent[:500], # Truncate to avoid very long user agents
- session_key=session_key
+ session_key=session_key,
)
-
+
return audit_entry
@@ -64,14 +68,14 @@ def log_create(request, entity_type, entity_id, entity_name, description=None):
"""Log entity creation"""
if not description:
description = f"Neue {entity_type.replace('_', ' ').title()} '{entity_name}' wurde erstellt"
-
+
return log_action(
request=request,
- action='create',
+ action="create",
entity_type=entity_type,
entity_id=entity_id,
entity_name=entity_name,
- description=description
+ description=description,
)
@@ -81,60 +85,78 @@ def log_update(request, entity_type, entity_id, entity_name, changes, descriptio
changed_fields = list(changes.keys()) if changes else []
fields_str = ", ".join(changed_fields)
description = f"{entity_type.replace('_', ' ').title()} '{entity_name}' wurde aktualisiert: {fields_str}"
-
+
return log_action(
request=request,
- action='update',
+ action="update",
entity_type=entity_type,
entity_id=entity_id,
entity_name=entity_name,
description=description,
- changes=changes
+ changes=changes,
)
def log_delete(request, entity_type, entity_id, entity_name, description=None):
"""Log entity deletion"""
if not description:
- description = f"{entity_type.replace('_', ' ').title()} '{entity_name}' wurde gelöscht"
-
+ description = (
+ f"{entity_type.replace('_', ' ').title()} '{entity_name}' wurde gelöscht"
+ )
+
return log_action(
request=request,
- action='delete',
+ action="delete",
entity_type=entity_type,
entity_id=entity_id,
entity_name=entity_name,
- description=description
+ description=description,
)
-def log_link(request, entity_type, entity_id, entity_name, target_type, target_name, description=None):
+def log_link(
+ request,
+ entity_type,
+ entity_id,
+ entity_name,
+ target_type,
+ target_name,
+ description=None,
+):
"""Log entity linking"""
if not description:
description = f"{entity_type.replace('_', ' ').title()} '{entity_name}' wurde mit {target_type.replace('_', ' ')} '{target_name}' verknüpft"
-
+
return log_action(
request=request,
- action='link',
+ action="link",
entity_type=entity_type,
entity_id=entity_id,
entity_name=entity_name,
- description=description
+ description=description,
)
-def log_unlink(request, entity_type, entity_id, entity_name, target_type, target_name, description=None):
+def log_unlink(
+ request,
+ entity_type,
+ entity_id,
+ entity_name,
+ target_type,
+ target_name,
+ description=None,
+):
"""Log entity unlinking"""
if not description:
description = f"Verknüpfung zwischen {entity_type.replace('_', ' ').title()} '{entity_name}' und {target_type.replace('_', ' ')} '{target_name}' wurde entfernt"
-
+
return log_action(
request=request,
- action='unlink',
+ action="unlink",
entity_type=entity_type,
entity_id=entity_id,
entity_name=entity_name,
- description=description
+ description=description,
)
@@ -143,51 +165,54 @@ def log_system_action(request, action, description, details=None):
return log_action(
request=request,
action=action,
- entity_type='system',
- entity_id='',
- entity_name='System',
+ entity_type="system",
+ entity_id="",
+ entity_name="System",
description=description,
- changes=details
+ changes=details,
)
def track_model_changes(old_instance, new_instance, exclude_fields=None):
"""
Track changes between model instances
-
+
Args:
old_instance: Original model instance
- new_instance: Updated model instance
+ new_instance: Updated model instance
exclude_fields: List of fields to exclude from tracking
-
+
Returns:
Dictionary of changes in format {field: {'old': old_value, 'new': new_value}}
"""
if exclude_fields is None:
- exclude_fields = ['id', 'erstellt_am', 'aktualisiert_am', 'created_at', 'updated_at']
-
+ exclude_fields = [
+ "id",
+ "erstellt_am",
+ "aktualisiert_am",
+ "created_at",
+ "updated_at",
+ ]
+
changes = {}
-
+
if old_instance and new_instance:
for field in new_instance._meta.fields:
field_name = field.name
-
+
if field_name in exclude_fields:
continue
-
+
old_value = getattr(old_instance, field_name, None)
new_value = getattr(new_instance, field_name, None)
-
+
# Convert to string for comparison
old_str = str(old_value) if old_value is not None else None
new_str = str(new_value) if new_value is not None else None
-
+
if old_str != new_str:
- changes[field_name] = {
- 'old': old_str,
- 'new': new_str
- }
-
+ changes[field_name] = {"old": old_str, "new": new_str}
+
return changes
@@ -195,38 +220,39 @@ class AuditLogMixin:
"""
Mixin for views that provides audit logging functionality
"""
+
audit_entity_type = None
- audit_entity_name_field = 'name'
-
+ audit_entity_name_field = "name"
+
def get_audit_entity_type(self):
"""Get the entity type for audit logging"""
if self.audit_entity_type:
return self.audit_entity_type
-
+
# Try to derive from model name
- if hasattr(self, 'model') and self.model:
+ if hasattr(self, "model") and self.model:
return self.model.__name__.lower()
-
- return 'unknown'
-
+
+ return "unknown"
+
def get_audit_entity_name(self, instance):
"""Get the entity name for audit logging"""
if hasattr(instance, self.audit_entity_name_field):
return str(getattr(instance, self.audit_entity_name_field))
- elif hasattr(instance, '__str__'):
+ elif hasattr(instance, "__str__"):
return str(instance)
else:
return f"{self.get_audit_entity_type()} #{instance.pk}"
-
+
def log_create_action(self, instance):
"""Log creation of an instance"""
log_create(
request=self.request,
entity_type=self.get_audit_entity_type(),
entity_id=instance.pk,
- entity_name=self.get_audit_entity_name(instance)
+ entity_name=self.get_audit_entity_name(instance),
)
-
+
def log_update_action(self, old_instance, new_instance):
"""Log update of an instance"""
changes = track_model_changes(old_instance, new_instance)
@@ -236,16 +262,16 @@ class AuditLogMixin:
entity_type=self.get_audit_entity_type(),
entity_id=new_instance.pk,
entity_name=self.get_audit_entity_name(new_instance),
- changes=changes
+ changes=changes,
)
-
+
def log_delete_action(self, instance):
"""Log deletion of an instance"""
log_delete(
request=self.request,
entity_type=self.get_audit_entity_type(),
entity_id=instance.pk,
- entity_name=self.get_audit_entity_name(instance)
+ entity_name=self.get_audit_entity_name(instance),
)
@@ -255,11 +281,11 @@ def log_login(request, user):
try:
return log_action(
request=request,
- action='login',
- entity_type='user',
+ action="login",
+ entity_type="user",
entity_id=user.pk,
entity_name=user.get_username(),
- description=f"User '{user.get_username()}' logged in"
+ description=f"User '{user.get_username()}' logged in",
)
except Exception:
return None
@@ -268,14 +294,14 @@ def log_login(request, user):
def log_logout(request, user):
"""Log a successful user logout."""
try:
- username = user.get_username() if user else 'Unknown'
+ username = user.get_username() if user else "Unknown"
return log_action(
request=request,
- action='logout',
- entity_type='user',
- entity_id=getattr(user, 'pk', ''),
+ action="logout",
+ entity_type="user",
+ entity_id=getattr(user, "pk", ""),
entity_name=username,
- description=f"User '{username}' logged out"
+ description=f"User '{username}' logged out",
)
except Exception:
return None
diff --git a/app/stiftung/backup_utils.py b/app/stiftung/backup_utils.py
index cffcb54..910a8bf 100644
--- a/app/stiftung/backup_utils.py
+++ b/app/stiftung/backup_utils.py
@@ -6,17 +6,19 @@ Handles creation and restoration of complete system backups
import os
import shutil
import subprocess
-import tempfile
import tarfile
+import tempfile
from datetime import datetime
+
from django.conf import settings
from django.utils import timezone
+
from stiftung.models import BackupJob
def get_backup_directory():
"""Get or create the backup directory"""
- backup_dir = '/app/backups'
+ backup_dir = "/app/backups"
os.makedirs(backup_dir, exist_ok=True)
return backup_dir
@@ -28,48 +30,48 @@ def run_backup(backup_job_id):
"""
try:
backup_job = BackupJob.objects.get(id=backup_job_id)
- backup_job.status = 'running'
+ backup_job.status = "running"
backup_job.started_at = timezone.now()
backup_job.save()
-
+
backup_dir = get_backup_directory()
- timestamp = datetime.now().strftime('%Y%m%d_%H%M%S')
+ timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
backup_filename = f"stiftung_backup_{timestamp}.tar.gz"
backup_path = os.path.join(backup_dir, backup_filename)
-
+
# Create temporary directory for backup staging
with tempfile.TemporaryDirectory() as temp_dir:
- staging_dir = os.path.join(temp_dir, 'backup_staging')
+ staging_dir = os.path.join(temp_dir, "backup_staging")
os.makedirs(staging_dir)
-
+
# 1. Database backup
- if backup_job.backup_type in ['full', 'database']:
+ if backup_job.backup_type in ["full", "database"]:
db_backup_path = create_database_backup(staging_dir)
if not db_backup_path:
raise Exception("Database backup failed")
-
+
# 2. Files backup
- if backup_job.backup_type in ['full', 'files']:
+ if backup_job.backup_type in ["full", "files"]:
files_backup_path = create_files_backup(staging_dir)
if not files_backup_path:
raise Exception("Files backup failed")
-
+
# 3. Create metadata file
create_backup_metadata(staging_dir, backup_job)
-
+
# 4. Create compressed archive
create_compressed_backup(staging_dir, backup_path)
-
+
# 5. Update job status
backup_size = os.path.getsize(backup_path)
- backup_job.status = 'completed'
+ backup_job.status = "completed"
backup_job.completed_at = timezone.now()
backup_job.backup_filename = backup_filename
backup_job.backup_size = backup_size
backup_job.save()
-
+
except Exception as e:
- backup_job.status = 'failed'
+ backup_job.status = "failed"
backup_job.error_message = str(e)
backup_job.completed_at = timezone.now()
backup_job.save()
@@ -78,37 +80,42 @@ def run_backup(backup_job_id):
def create_database_backup(staging_dir):
"""Create a database backup using pg_dump"""
try:
- db_backup_file = os.path.join(staging_dir, 'database.sql')
-
+ db_backup_file = os.path.join(staging_dir, "database.sql")
+
# Get database settings
- db_settings = settings.DATABASES['default']
-
+ db_settings = settings.DATABASES["default"]
+
# Build pg_dump command
cmd = [
- 'pg_dump',
- '--host', db_settings.get('HOST', 'localhost'),
- '--port', str(db_settings.get('PORT', 5432)),
- '--username', db_settings.get('USER', 'postgres'),
- '--format', 'custom',
- '--no-owner', # portability across environments
- '--no-privileges', # skip GRANT/REVOKE
- '--no-password',
- '--file', db_backup_file,
- db_settings.get('NAME', 'stiftung')
+ "pg_dump",
+ "--host",
+ db_settings.get("HOST", "localhost"),
+ "--port",
+ str(db_settings.get("PORT", 5432)),
+ "--username",
+ db_settings.get("USER", "postgres"),
+ "--format",
+ "custom",
+ "--no-owner", # portability across environments
+ "--no-privileges", # skip GRANT/REVOKE
+ "--no-password",
+ "--file",
+ db_backup_file,
+ db_settings.get("NAME", "stiftung"),
]
-
+
# Set environment variables for authentication
env = os.environ.copy()
- env['PGPASSWORD'] = db_settings.get('PASSWORD', '')
-
+ env["PGPASSWORD"] = db_settings.get("PASSWORD", "")
+
# Run pg_dump
result = subprocess.run(cmd, env=env, capture_output=True, text=True)
-
+
if result.returncode != 0:
raise Exception(f"pg_dump failed: {result.stderr}")
-
+
return db_backup_file
-
+
except Exception as e:
print(f"Database backup failed: {e}")
return None
@@ -117,28 +124,28 @@ def create_database_backup(staging_dir):
def create_files_backup(staging_dir):
"""Create backup of application files"""
try:
- files_dir = os.path.join(staging_dir, 'files')
+ files_dir = os.path.join(staging_dir, "files")
os.makedirs(files_dir)
-
+
# Files to backup
backup_paths = [
- '/app/media', # User uploads
- '/app/static', # Static files
- '/app/.env', # Environment configuration
+ "/app/media", # User uploads
+ "/app/static", # Static files
+ "/app/.env", # Environment configuration
]
-
+
for source_path in backup_paths:
if os.path.exists(source_path):
basename = os.path.basename(source_path)
dest_path = os.path.join(files_dir, basename)
-
+
if os.path.isdir(source_path):
shutil.copytree(source_path, dest_path)
else:
shutil.copy2(source_path, dest_path)
-
+
return files_dir
-
+
except Exception as e:
print(f"Files backup failed: {e}")
return None
@@ -147,26 +154,28 @@ def create_files_backup(staging_dir):
def create_backup_metadata(staging_dir, backup_job):
"""Create metadata file with backup information"""
import json
-
+
metadata = {
- 'backup_id': str(backup_job.id),
- 'backup_type': backup_job.backup_type,
- 'created_at': backup_job.created_at.isoformat(),
- 'created_by': backup_job.created_by.username if backup_job.created_by else 'system',
- 'django_version': '5.0.6',
- 'app_version': '1.0.0',
- 'python_version': '3.12',
+ "backup_id": str(backup_job.id),
+ "backup_type": backup_job.backup_type,
+ "created_at": backup_job.created_at.isoformat(),
+ "created_by": (
+ backup_job.created_by.username if backup_job.created_by else "system"
+ ),
+ "django_version": "5.0.6",
+ "app_version": "1.0.0",
+ "python_version": "3.12",
}
-
- metadata_file = os.path.join(staging_dir, 'backup_metadata.json')
- with open(metadata_file, 'w') as f:
+
+ metadata_file = os.path.join(staging_dir, "backup_metadata.json")
+ with open(metadata_file, "w") as f:
json.dump(metadata, f, indent=2)
def create_compressed_backup(staging_dir, backup_path):
"""Create compressed tar.gz archive"""
- with tarfile.open(backup_path, 'w:gz') as tar:
- tar.add(staging_dir, arcname='.')
+ with tarfile.open(backup_path, "w:gz") as tar:
+ tar.add(staging_dir, arcname=".")
def run_restore(restore_job_id, backup_file_path):
@@ -176,46 +185,47 @@ def run_restore(restore_job_id, backup_file_path):
"""
try:
restore_job = BackupJob.objects.get(id=restore_job_id)
- restore_job.status = 'running'
+ restore_job.status = "running"
restore_job.started_at = timezone.now()
restore_job.save()
-
+
# Extract backup
with tempfile.TemporaryDirectory() as temp_dir:
- extract_dir = os.path.join(temp_dir, 'restore')
+ extract_dir = os.path.join(temp_dir, "restore")
os.makedirs(extract_dir)
-
+
# Extract tar.gz
- with tarfile.open(backup_file_path, 'r:gz') as tar:
+ with tarfile.open(backup_file_path, "r:gz") as tar:
tar.extractall(extract_dir)
-
+
# Validate backup
- metadata_file = os.path.join(extract_dir, 'backup_metadata.json')
+ metadata_file = os.path.join(extract_dir, "backup_metadata.json")
if not os.path.exists(metadata_file):
raise Exception("Invalid backup: missing metadata")
-
+
# Read metadata
import json
- with open(metadata_file, 'r') as f:
+
+ with open(metadata_file, "r") as f:
metadata = json.load(f)
-
+
# Restore database
- db_backup_file = os.path.join(extract_dir, 'database.sql')
+ db_backup_file = os.path.join(extract_dir, "database.sql")
if os.path.exists(db_backup_file):
restore_database(db_backup_file)
-
+
# Restore files
- files_dir = os.path.join(extract_dir, 'files')
+ files_dir = os.path.join(extract_dir, "files")
if os.path.exists(files_dir):
restore_files(files_dir)
-
+
# Update job status
- restore_job.status = 'completed'
+ restore_job.status = "completed"
restore_job.completed_at = timezone.now()
restore_job.save()
-
+
except Exception as e:
- restore_job.status = 'failed'
+ restore_job.status = "failed"
restore_job.error_message = str(e)
restore_job.completed_at = timezone.now()
restore_job.save()
@@ -225,42 +235,47 @@ def restore_database(db_backup_file):
"""Restore database from backup"""
try:
# Get database settings
- db_settings = settings.DATABASES['default']
-
+ db_settings = settings.DATABASES["default"]
+
# Build pg_restore command
cmd = [
- 'pg_restore',
- '--host', db_settings.get('HOST', 'localhost'),
- '--port', str(db_settings.get('PORT', 5432)),
- '--username', db_settings.get('USER', 'postgres'),
- '--dbname', db_settings.get('NAME', 'stiftung'),
- '--clean', # Drop existing objects first
- '--if-exists', # Don't error if objects don't exist
- '--no-owner', # don't attempt to set original owners
- '--role', db_settings.get('USER', 'postgres'), # set target owner
- '--single-transaction', # restore atomically when possible
- '--disable-triggers', # avoid FK issues during data load
- '--no-password',
- '--verbose',
- db_backup_file
+ "pg_restore",
+ "--host",
+ db_settings.get("HOST", "localhost"),
+ "--port",
+ str(db_settings.get("PORT", 5432)),
+ "--username",
+ db_settings.get("USER", "postgres"),
+ "--dbname",
+ db_settings.get("NAME", "stiftung"),
+ "--clean", # Drop existing objects first
+ "--if-exists", # Don't error if objects don't exist
+ "--no-owner", # don't attempt to set original owners
+ "--role",
+ db_settings.get("USER", "postgres"), # set target owner
+ "--single-transaction", # restore atomically when possible
+ "--disable-triggers", # avoid FK issues during data load
+ "--no-password",
+ "--verbose",
+ db_backup_file,
]
-
+
# Set environment variables for authentication
env = os.environ.copy()
- env['PGPASSWORD'] = db_settings.get('PASSWORD', '')
-
+ env["PGPASSWORD"] = db_settings.get("PASSWORD", "")
+
# Run pg_restore
result = subprocess.run(cmd, env=env, capture_output=True, text=True)
-
+
# Fail if there are real errors
if result.returncode != 0:
- stderr = result.stderr or ''
+ stderr = result.stderr or ""
# escalate only if we see ERROR
- if 'ERROR' in stderr.upper():
+ if "ERROR" in stderr.upper():
raise Exception(f"pg_restore failed: {stderr}")
else:
print(f"pg_restore completed with warnings: {stderr}")
-
+
except Exception as e:
raise Exception(f"Database restore failed: {e}")
@@ -270,29 +285,31 @@ def restore_files(files_dir):
try:
# Restore paths
restore_mappings = {
- 'media': '/app/media',
- 'static': '/app/static',
- '.env': '/app/.env',
+ "media": "/app/media",
+ "static": "/app/static",
+ ".env": "/app/.env",
}
-
+
for source_name, dest_path in restore_mappings.items():
source_path = os.path.join(files_dir, source_name)
-
+
if os.path.exists(source_path):
# Backup existing files first
if os.path.exists(dest_path):
- backup_path = f"{dest_path}.backup.{datetime.now().strftime('%Y%m%d_%H%M%S')}"
+ backup_path = (
+ f"{dest_path}.backup.{datetime.now().strftime('%Y%m%d_%H%M%S')}"
+ )
if os.path.isdir(dest_path):
shutil.move(dest_path, backup_path)
else:
shutil.copy2(dest_path, backup_path)
-
+
# Restore files
if os.path.isdir(source_path):
shutil.copytree(source_path, dest_path)
else:
shutil.copy2(source_path, dest_path)
-
+
except Exception as e:
raise Exception(f"Files restore failed: {e}")
@@ -302,19 +319,19 @@ def cleanup_old_backups(keep_count=10):
try:
backup_dir = get_backup_directory()
backup_files = []
-
+
for filename in os.listdir(backup_dir):
- if filename.startswith('stiftung_backup_') and filename.endswith('.tar.gz'):
+ if filename.startswith("stiftung_backup_") and filename.endswith(".tar.gz"):
filepath = os.path.join(backup_dir, filename)
backup_files.append((filepath, os.path.getmtime(filepath)))
-
+
# Sort by modification time (newest first)
backup_files.sort(key=lambda x: x[1], reverse=True)
-
+
# Remove old backups
for filepath, _ in backup_files[keep_count:]:
os.remove(filepath)
print(f"Removed old backup: {os.path.basename(filepath)}")
-
+
except Exception as e:
print(f"Cleanup failed: {e}")
diff --git a/app/stiftung/forms.py b/app/stiftung/forms.py
index 7cdff86..9be429d 100644
--- a/app/stiftung/forms.py
+++ b/app/stiftung/forms.py
@@ -1,117 +1,143 @@
+import re
+
from django import forms
from django.core.exceptions import ValidationError
from django.utils import timezone
-from .models import (
- Rentmeister, StiftungsKonto, Verwaltungskosten, Person,
- Paechter, Destinataer, Land, DokumentLink, Foerderung, BankTransaction,
- DestinataerUnterstuetzung, UnterstuetzungWiederkehrend, DestinataerNotiz, LandAbrechnung,
-)
-import re
+
+from .models import (BankTransaction, Destinataer, DestinataerNotiz,
+ DestinataerUnterstuetzung, DokumentLink, Foerderung, Land,
+ LandAbrechnung, Paechter, Person, Rentmeister,
+ StiftungsKonto, UnterstuetzungWiederkehrend,
+ Verwaltungskosten)
class RentmeisterForm(forms.ModelForm):
"""Form für das Erstellen und Bearbeiten von Rentmeistern"""
-
+
class Meta:
model = Rentmeister
fields = [
- 'anrede', 'vorname', 'nachname', 'titel',
- 'email', 'telefon', 'mobil',
- 'strasse', 'plz', 'ort',
- 'iban', 'bic', 'bank_name',
- 'seit_datum', 'bis_datum', 'aktiv',
- 'monatliche_verguetung', 'km_pauschale',
- 'notizen'
+ "anrede",
+ "vorname",
+ "nachname",
+ "titel",
+ "email",
+ "telefon",
+ "mobil",
+ "strasse",
+ "plz",
+ "ort",
+ "iban",
+ "bic",
+ "bank_name",
+ "seit_datum",
+ "bis_datum",
+ "aktiv",
+ "monatliche_verguetung",
+ "km_pauschale",
+ "notizen",
]
-
+
widgets = {
- 'anrede': forms.Select(attrs={'class': 'form-select'}),
- 'vorname': forms.TextInput(attrs={'class': 'form-control'}),
- 'nachname': forms.TextInput(attrs={'class': 'form-control'}),
- 'titel': forms.TextInput(attrs={'class': 'form-control'}),
- 'email': forms.EmailInput(attrs={'class': 'form-control'}),
- 'telefon': forms.TextInput(attrs={'class': 'form-control'}),
- 'mobil': forms.TextInput(attrs={'class': 'form-control'}),
- 'strasse': forms.TextInput(attrs={'class': 'form-control'}),
- 'plz': forms.TextInput(attrs={'class': 'form-control'}),
- 'ort': forms.TextInput(attrs={'class': 'form-control'}),
- 'iban': forms.TextInput(attrs={'class': 'form-control', 'placeholder': 'DE89370400440532013000'}),
- 'bic': forms.TextInput(attrs={'class': 'form-control', 'placeholder': 'COBADEFFXXX'}),
- 'bank_name': forms.TextInput(attrs={'class': 'form-control'}),
- 'seit_datum': forms.DateInput(attrs={'class': 'form-control', 'type': 'date'}),
- 'bis_datum': forms.DateInput(attrs={'class': 'form-control', 'type': 'date'}),
- 'aktiv': forms.CheckboxInput(attrs={'class': 'form-check-input'}),
- 'monatliche_verguetung': forms.NumberInput(attrs={'class': 'form-control', 'step': '0.01'}),
- 'km_pauschale': forms.NumberInput(attrs={'class': 'form-control', 'step': '0.01', 'value': '0.30'}),
- 'notizen': forms.Textarea(attrs={'class': 'form-control', 'rows': 4}),
+ "anrede": forms.Select(attrs={"class": "form-select"}),
+ "vorname": forms.TextInput(attrs={"class": "form-control"}),
+ "nachname": forms.TextInput(attrs={"class": "form-control"}),
+ "titel": forms.TextInput(attrs={"class": "form-control"}),
+ "email": forms.EmailInput(attrs={"class": "form-control"}),
+ "telefon": forms.TextInput(attrs={"class": "form-control"}),
+ "mobil": forms.TextInput(attrs={"class": "form-control"}),
+ "strasse": forms.TextInput(attrs={"class": "form-control"}),
+ "plz": forms.TextInput(attrs={"class": "form-control"}),
+ "ort": forms.TextInput(attrs={"class": "form-control"}),
+ "iban": forms.TextInput(
+ attrs={"class": "form-control", "placeholder": "DE89370400440532013000"}
+ ),
+ "bic": forms.TextInput(
+ attrs={"class": "form-control", "placeholder": "COBADEFFXXX"}
+ ),
+ "bank_name": forms.TextInput(attrs={"class": "form-control"}),
+ "seit_datum": forms.DateInput(
+ attrs={"class": "form-control", "type": "date"}
+ ),
+ "bis_datum": forms.DateInput(
+ attrs={"class": "form-control", "type": "date"}
+ ),
+ "aktiv": forms.CheckboxInput(attrs={"class": "form-check-input"}),
+ "monatliche_verguetung": forms.NumberInput(
+ attrs={"class": "form-control", "step": "0.01"}
+ ),
+ "km_pauschale": forms.NumberInput(
+ attrs={"class": "form-control", "step": "0.01", "value": "0.30"}
+ ),
+ "notizen": forms.Textarea(attrs={"class": "form-control", "rows": 4}),
}
-
+
labels = {
- 'anrede': 'Anrede',
- 'vorname': 'Vorname *',
- 'nachname': 'Nachname *',
- 'titel': 'Titel',
- 'email': 'E-Mail',
- 'telefon': 'Telefon',
- 'mobil': 'Mobil',
- 'strasse': 'Straße',
- 'plz': 'PLZ',
- 'ort': 'Ort',
- 'iban': 'IBAN',
- 'bic': 'BIC',
- 'bank_name': 'Bank',
- 'seit_datum': 'Rentmeister seit *',
- 'bis_datum': 'Rentmeister bis',
- 'aktiv': 'Aktiv',
- 'monatliche_verguetung': 'Monatliche Vergütung (€)',
- 'km_pauschale': 'Kilometerpauschale (€/km)',
- 'notizen': 'Notizen',
+ "anrede": "Anrede",
+ "vorname": "Vorname *",
+ "nachname": "Nachname *",
+ "titel": "Titel",
+ "email": "E-Mail",
+ "telefon": "Telefon",
+ "mobil": "Mobil",
+ "strasse": "Straße",
+ "plz": "PLZ",
+ "ort": "Ort",
+ "iban": "IBAN",
+ "bic": "BIC",
+ "bank_name": "Bank",
+ "seit_datum": "Rentmeister seit *",
+ "bis_datum": "Rentmeister bis",
+ "aktiv": "Aktiv",
+ "monatliche_verguetung": "Monatliche Vergütung (€)",
+ "km_pauschale": "Kilometerpauschale (€/km)",
+ "notizen": "Notizen",
}
-
+
help_texts = {
- 'iban': 'Internationale Bankkontonummer für Abrechnungen',
- 'km_pauschale': 'Standard: 0,30 € pro Kilometer',
- 'seit_datum': 'Datum des Amtsantritts als Rentmeister',
- 'bis_datum': 'Leer lassen für aktive Rentmeister',
+ "iban": "Internationale Bankkontonummer für Abrechnungen",
+ "km_pauschale": "Standard: 0,30 € pro Kilometer",
+ "seit_datum": "Datum des Amtsantritts als Rentmeister",
+ "bis_datum": "Leer lassen für aktive Rentmeister",
}
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
# Markiere Pflichtfelder
- self.fields['vorname'].required = True
- self.fields['nachname'].required = True
- self.fields['seit_datum'].required = True
+ self.fields["vorname"].required = True
+ self.fields["nachname"].required = True
+ self.fields["seit_datum"].required = True
def clean_iban(self):
"""Validierung der IBAN"""
- iban = self.cleaned_data.get('iban')
+ iban = self.cleaned_data.get("iban")
if iban:
# Entferne Leerzeichen und konvertiere zu Großbuchstaben
- iban = re.sub(r'\s+', '', iban.upper())
-
+ iban = re.sub(r"\s+", "", iban.upper())
+
# Einfache IBAN-Längenvalidierung für deutsche IBANs
- if iban.startswith('DE') and len(iban) != 22:
- raise ValidationError('Deutsche IBANs müssen 22 Zeichen lang sein.')
-
+ if iban.startswith("DE") and len(iban) != 22:
+ raise ValidationError("Deutsche IBANs müssen 22 Zeichen lang sein.")
+
# Speichere die bereinigte IBAN
return iban
return iban
def clean_plz(self):
"""Validierung der PLZ"""
- plz = self.cleaned_data.get('plz')
- if plz and not re.match(r'^\d{5}$', plz):
- raise ValidationError('PLZ muss aus 5 Ziffern bestehen.')
+ plz = self.cleaned_data.get("plz")
+ if plz and not re.match(r"^\d{5}$", plz):
+ raise ValidationError("PLZ muss aus 5 Ziffern bestehen.")
return plz
def clean(self):
"""Übergreifende Validierung"""
from django.utils.dateparse import parse_date
-
+
cleaned_data = super().clean()
- seit_datum = cleaned_data.get('seit_datum')
- bis_datum = cleaned_data.get('bis_datum')
-
+ seit_datum = cleaned_data.get("seit_datum")
+ bis_datum = cleaned_data.get("bis_datum")
+
# Helper function to ensure we have date objects
def ensure_date(date_value):
if not date_value:
@@ -119,595 +145,861 @@ class RentmeisterForm(forms.ModelForm):
if isinstance(date_value, str):
return parse_date(date_value)
return date_value
-
+
# Convert to date objects if they're strings
seit_datum = ensure_date(seit_datum)
bis_datum = ensure_date(bis_datum)
-
+
# Prüfe Datum-Logik
if seit_datum and bis_datum and bis_datum <= seit_datum:
- raise ValidationError('Das End-Datum muss nach dem Start-Datum liegen.')
-
+ raise ValidationError("Das End-Datum muss nach dem Start-Datum liegen.")
+
return cleaned_data
class StiftungsKontoForm(forms.ModelForm):
"""Form für das Erstellen und Bearbeiten von Stiftungskonten"""
-
+
class Meta:
model = StiftungsKonto
fields = [
- 'kontoname', 'bank_name', 'iban', 'bic', 'konto_typ',
- 'saldo', 'saldo_datum', 'zinssatz', 'laufzeit_bis',
- 'aktiv', 'notizen'
+ "kontoname",
+ "bank_name",
+ "iban",
+ "bic",
+ "konto_typ",
+ "saldo",
+ "saldo_datum",
+ "zinssatz",
+ "laufzeit_bis",
+ "aktiv",
+ "notizen",
]
-
+
widgets = {
- 'kontoname': forms.TextInput(attrs={'class': 'form-control'}),
- 'bank_name': forms.TextInput(attrs={'class': 'form-control'}),
- 'iban': forms.TextInput(attrs={'class': 'form-control', 'placeholder': 'DE89370400440532013000'}),
- 'bic': forms.TextInput(attrs={'class': 'form-control', 'placeholder': 'COBADEFFXXX'}),
- 'konto_typ': forms.Select(attrs={'class': 'form-select'}),
- 'saldo': forms.NumberInput(attrs={'class': 'form-control', 'step': '0.01'}),
- 'saldo_datum': forms.DateInput(attrs={'class': 'form-control', 'type': 'date'}),
- 'zinssatz': forms.NumberInput(attrs={'class': 'form-control', 'step': '0.01'}),
- 'laufzeit_bis': forms.DateInput(attrs={'class': 'form-control', 'type': 'date'}),
- 'aktiv': forms.CheckboxInput(attrs={'class': 'form-check-input'}),
- 'notizen': forms.Textarea(attrs={'class': 'form-control', 'rows': 3}),
+ "kontoname": forms.TextInput(attrs={"class": "form-control"}),
+ "bank_name": forms.TextInput(attrs={"class": "form-control"}),
+ "iban": forms.TextInput(
+ attrs={"class": "form-control", "placeholder": "DE89370400440532013000"}
+ ),
+ "bic": forms.TextInput(
+ attrs={"class": "form-control", "placeholder": "COBADEFFXXX"}
+ ),
+ "konto_typ": forms.Select(attrs={"class": "form-select"}),
+ "saldo": forms.NumberInput(attrs={"class": "form-control", "step": "0.01"}),
+ "saldo_datum": forms.DateInput(
+ attrs={"class": "form-control", "type": "date"}
+ ),
+ "zinssatz": forms.NumberInput(
+ attrs={"class": "form-control", "step": "0.01"}
+ ),
+ "laufzeit_bis": forms.DateInput(
+ attrs={"class": "form-control", "type": "date"}
+ ),
+ "aktiv": forms.CheckboxInput(attrs={"class": "form-check-input"}),
+ "notizen": forms.Textarea(attrs={"class": "form-control", "rows": 3}),
}
class VerwaltungskostenForm(forms.ModelForm):
"""Form für das Erstellen und Bearbeiten von Verwaltungskosten"""
-
+
class Meta:
model = Verwaltungskosten
fields = [
- 'bezeichnung', 'kategorie', 'betrag', 'datum', 'status',
- 'rentmeister', 'zahlungskonto', 'quellkonto', 'lieferant_firma', 'rechnungsnummer',
- 'km_anzahl', 'km_satz', 'von_ort', 'nach_ort', 'zweck',
- 'beschreibung', 'notizen'
+ "bezeichnung",
+ "kategorie",
+ "betrag",
+ "datum",
+ "status",
+ "rentmeister",
+ "zahlungskonto",
+ "quellkonto",
+ "lieferant_firma",
+ "rechnungsnummer",
+ "km_anzahl",
+ "km_satz",
+ "von_ort",
+ "nach_ort",
+ "zweck",
+ "beschreibung",
+ "notizen",
]
-
+
widgets = {
- 'bezeichnung': forms.TextInput(attrs={'class': 'form-control'}),
- 'kategorie': forms.Select(attrs={'class': 'form-select'}),
- 'betrag': forms.NumberInput(attrs={'class': 'form-control', 'step': '0.01'}),
- 'datum': forms.DateInput(attrs={'class': 'form-control', 'type': 'date'}),
- 'status': forms.Select(attrs={'class': 'form-select'}),
- 'rentmeister': forms.Select(attrs={'class': 'form-select'}),
- 'zahlungskonto': forms.Select(attrs={'class': 'form-select'}),
- 'quellkonto': forms.Select(attrs={'class': 'form-select'}),
- 'lieferant_firma': forms.TextInput(attrs={'class': 'form-control'}),
- 'rechnungsnummer': forms.TextInput(attrs={'class': 'form-control'}),
- 'km_anzahl': forms.NumberInput(attrs={'class': 'form-control', 'step': '0.1'}),
- 'km_satz': forms.NumberInput(attrs={'class': 'form-control', 'step': '0.01'}),
- 'von_ort': forms.TextInput(attrs={'class': 'form-control'}),
- 'nach_ort': forms.TextInput(attrs={'class': 'form-control'}),
- 'zweck': forms.TextInput(attrs={'class': 'form-control'}),
- 'beschreibung': forms.Textarea(attrs={'class': 'form-control', 'rows': 3}),
- 'notizen': forms.Textarea(attrs={'class': 'form-control', 'rows': 2}),
+ "bezeichnung": forms.TextInput(attrs={"class": "form-control"}),
+ "kategorie": forms.Select(attrs={"class": "form-select"}),
+ "betrag": forms.NumberInput(
+ attrs={"class": "form-control", "step": "0.01"}
+ ),
+ "datum": forms.DateInput(attrs={"class": "form-control", "type": "date"}),
+ "status": forms.Select(attrs={"class": "form-select"}),
+ "rentmeister": forms.Select(attrs={"class": "form-select"}),
+ "zahlungskonto": forms.Select(attrs={"class": "form-select"}),
+ "quellkonto": forms.Select(attrs={"class": "form-select"}),
+ "lieferant_firma": forms.TextInput(attrs={"class": "form-control"}),
+ "rechnungsnummer": forms.TextInput(attrs={"class": "form-control"}),
+ "km_anzahl": forms.NumberInput(
+ attrs={"class": "form-control", "step": "0.1"}
+ ),
+ "km_satz": forms.NumberInput(
+ attrs={"class": "form-control", "step": "0.01"}
+ ),
+ "von_ort": forms.TextInput(attrs={"class": "form-control"}),
+ "nach_ort": forms.TextInput(attrs={"class": "form-control"}),
+ "zweck": forms.TextInput(attrs={"class": "form-control"}),
+ "beschreibung": forms.Textarea(attrs={"class": "form-control", "rows": 3}),
+ "notizen": forms.Textarea(attrs={"class": "form-control", "rows": 2}),
}
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
# Filtere nur aktive Rentmeister und Konten
- self.fields['rentmeister'].queryset = Rentmeister.objects.filter(aktiv=True)
- self.fields['zahlungskonto'].queryset = StiftungsKonto.objects.filter(aktiv=True)
- self.fields['quellkonto'].queryset = StiftungsKonto.objects.filter(aktiv=True)
-
+ self.fields["rentmeister"].queryset = Rentmeister.objects.filter(aktiv=True)
+ self.fields["zahlungskonto"].queryset = StiftungsKonto.objects.filter(
+ aktiv=True
+ )
+ self.fields["quellkonto"].queryset = StiftungsKonto.objects.filter(aktiv=True)
+
# Standardwerte setzen
if not self.instance.pk: # Nur bei neuen Objekten
# Standard km_satz auf 0.30 Euro setzen
- self.fields['km_satz'].initial = 0.30
+ self.fields["km_satz"].initial = 0.30
class PersonForm(forms.ModelForm):
"""Form für das Erstellen und Bearbeiten von Personen (Legacy)"""
-
+
class Meta:
model = Person
fields = [
- 'familienzweig', 'vorname', 'nachname', 'geburtsdatum',
- 'email', 'telefon', 'iban', 'adresse', 'notizen', 'aktiv'
+ "familienzweig",
+ "vorname",
+ "nachname",
+ "geburtsdatum",
+ "email",
+ "telefon",
+ "iban",
+ "adresse",
+ "notizen",
+ "aktiv",
]
-
+
widgets = {
- 'familienzweig': forms.Select(attrs={'class': 'form-select'}),
- 'vorname': forms.TextInput(attrs={'class': 'form-control'}),
- 'nachname': forms.TextInput(attrs={'class': 'form-control'}),
- 'geburtsdatum': forms.DateInput(attrs={'class': 'form-control', 'type': 'date'}),
- 'email': forms.EmailInput(attrs={'class': 'form-control'}),
- 'telefon': forms.TextInput(attrs={'class': 'form-control'}),
- 'iban': forms.TextInput(attrs={'class': 'form-control', 'placeholder': 'DE89370400440532013000'}),
- 'adresse': forms.Textarea(attrs={'class': 'form-control', 'rows': 3}),
- 'notizen': forms.Textarea(attrs={'class': 'form-control', 'rows': 3}),
- 'aktiv': forms.CheckboxInput(attrs={'class': 'form-check-input'}),
+ "familienzweig": forms.Select(attrs={"class": "form-select"}),
+ "vorname": forms.TextInput(attrs={"class": "form-control"}),
+ "nachname": forms.TextInput(attrs={"class": "form-control"}),
+ "geburtsdatum": forms.DateInput(
+ attrs={"class": "form-control", "type": "date"}
+ ),
+ "email": forms.EmailInput(attrs={"class": "form-control"}),
+ "telefon": forms.TextInput(attrs={"class": "form-control"}),
+ "iban": forms.TextInput(
+ attrs={"class": "form-control", "placeholder": "DE89370400440532013000"}
+ ),
+ "adresse": forms.Textarea(attrs={"class": "form-control", "rows": 3}),
+ "notizen": forms.Textarea(attrs={"class": "form-control", "rows": 3}),
+ "aktiv": forms.CheckboxInput(attrs={"class": "form-check-input"}),
}
-
+
labels = {
- 'familienzweig': 'Familienzweig',
- 'vorname': 'Vorname *',
- 'nachname': 'Nachname *',
- 'geburtsdatum': 'Geburtsdatum',
- 'email': 'E-Mail',
- 'telefon': 'Telefon',
- 'iban': 'IBAN',
- 'adresse': 'Adresse',
- 'notizen': 'Notizen',
- 'aktiv': 'Aktiv',
+ "familienzweig": "Familienzweig",
+ "vorname": "Vorname *",
+ "nachname": "Nachname *",
+ "geburtsdatum": "Geburtsdatum",
+ "email": "E-Mail",
+ "telefon": "Telefon",
+ "iban": "IBAN",
+ "adresse": "Adresse",
+ "notizen": "Notizen",
+ "aktiv": "Aktiv",
}
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
# Markiere Pflichtfelder
- self.fields['vorname'].required = True
- self.fields['nachname'].required = True
+ self.fields["vorname"].required = True
+ self.fields["nachname"].required = True
class PaechterForm(forms.ModelForm):
"""Form für das Erstellen und Bearbeiten von Pächtern"""
-
+
class Meta:
model = Paechter
- fields = '__all__'
+ fields = "__all__"
widgets = {
- 'anrede': forms.Select(attrs={'class': 'form-select'}),
- 'vorname': forms.TextInput(attrs={'class': 'form-control'}),
- 'nachname': forms.TextInput(attrs={'class': 'form-control'}),
- 'email': forms.EmailInput(attrs={'class': 'form-control'}),
- 'telefon': forms.TextInput(attrs={'class': 'form-control'}),
- 'mobil': forms.TextInput(attrs={'class': 'form-control'}),
- 'geburtsdatum': forms.DateInput(attrs={'class': 'form-control', 'type': 'date'}),
- 'strasse': forms.TextInput(attrs={'class': 'form-control'}),
- 'plz': forms.TextInput(attrs={'class': 'form-control'}),
- 'ort': forms.TextInput(attrs={'class': 'form-control'}),
- 'aktiv': forms.CheckboxInput(attrs={'class': 'form-check-input'}),
- 'notizen': forms.Textarea(attrs={'class': 'form-control', 'rows': 3}),
+ "anrede": forms.Select(attrs={"class": "form-select"}),
+ "vorname": forms.TextInput(attrs={"class": "form-control"}),
+ "nachname": forms.TextInput(attrs={"class": "form-control"}),
+ "email": forms.EmailInput(attrs={"class": "form-control"}),
+ "telefon": forms.TextInput(attrs={"class": "form-control"}),
+ "mobil": forms.TextInput(attrs={"class": "form-control"}),
+ "geburtsdatum": forms.DateInput(
+ attrs={"class": "form-control", "type": "date"}
+ ),
+ "strasse": forms.TextInput(attrs={"class": "form-control"}),
+ "plz": forms.TextInput(attrs={"class": "form-control"}),
+ "ort": forms.TextInput(attrs={"class": "form-control"}),
+ "aktiv": forms.CheckboxInput(attrs={"class": "form-check-input"}),
+ "notizen": forms.Textarea(attrs={"class": "form-control", "rows": 3}),
}
class DestinataerForm(forms.ModelForm):
"""Form für das Erstellen und Bearbeiten von Destinatären"""
-
+
class Meta:
model = Destinataer
- fields = '__all__'
+ fields = "__all__"
widgets = {
- 'anrede': forms.Select(attrs={'class': 'form-select'}),
- 'vorname': forms.TextInput(attrs={'class': 'form-control'}),
- 'nachname': forms.TextInput(attrs={'class': 'form-control'}),
- 'titel': forms.TextInput(attrs={'class': 'form-control'}),
- 'strasse': forms.TextInput(attrs={'class': 'form-control'}),
- 'plz': forms.TextInput(attrs={'class': 'form-control'}),
- 'ort': forms.TextInput(attrs={'class': 'form-control'}),
- 'telefon': forms.TextInput(attrs={'class': 'form-control'}),
- 'mobil': forms.TextInput(attrs={'class': 'form-control'}),
- 'email': forms.EmailInput(attrs={'class': 'form-control'}),
- 'aktiv': forms.CheckboxInput(attrs={'class': 'form-check-input'}),
- 'notizen': forms.Textarea(attrs={'class': 'form-control', 'rows': 3}),
- 'ist_abkoemmling': forms.CheckboxInput(attrs={'class': 'form-check-input'}),
- 'haushaltsgroesse': forms.NumberInput(attrs={'class': 'form-control', 'min': 1}),
+ "anrede": forms.Select(attrs={"class": "form-select"}),
+ "vorname": forms.TextInput(attrs={"class": "form-control"}),
+ "nachname": forms.TextInput(attrs={"class": "form-control"}),
+ "titel": forms.TextInput(attrs={"class": "form-control"}),
+ "strasse": forms.TextInput(attrs={"class": "form-control"}),
+ "plz": forms.TextInput(attrs={"class": "form-control"}),
+ "ort": forms.TextInput(attrs={"class": "form-control"}),
+ "telefon": forms.TextInput(attrs={"class": "form-control"}),
+ "mobil": forms.TextInput(attrs={"class": "form-control"}),
+ "email": forms.EmailInput(attrs={"class": "form-control"}),
+ "aktiv": forms.CheckboxInput(attrs={"class": "form-check-input"}),
+ "notizen": forms.Textarea(attrs={"class": "form-control", "rows": 3}),
+ "ist_abkoemmling": forms.CheckboxInput(attrs={"class": "form-check-input"}),
+ "haushaltsgroesse": forms.NumberInput(
+ attrs={"class": "form-control", "min": 1}
+ ),
# renamed in UI: use vierteljaehrlicher_betrag field
- 'vermoegen': forms.NumberInput(attrs={'class': 'form-control', 'step': '0.01'}),
- 'unterstuetzung_bestaetigt': forms.CheckboxInput(attrs={'class': 'form-check-input'}),
- 'standard_konto': forms.Select(attrs={'class': 'form-select'}),
- 'vierteljaehrlicher_betrag': forms.NumberInput(attrs={'class': 'form-control', 'step': '0.01'}),
- 'studiennachweis_erforderlich': forms.CheckboxInput(attrs={'class': 'form-check-input'}),
- 'letzter_studiennachweis': forms.DateInput(attrs={'class': 'form-control', 'type': 'date'}),
+ "vermoegen": forms.NumberInput(
+ attrs={"class": "form-control", "step": "0.01"}
+ ),
+ "unterstuetzung_bestaetigt": forms.CheckboxInput(
+ attrs={"class": "form-check-input"}
+ ),
+ "standard_konto": forms.Select(attrs={"class": "form-select"}),
+ "vierteljaehrlicher_betrag": forms.NumberInput(
+ attrs={"class": "form-control", "step": "0.01"}
+ ),
+ "studiennachweis_erforderlich": forms.CheckboxInput(
+ attrs={"class": "form-check-input"}
+ ),
+ "letzter_studiennachweis": forms.DateInput(
+ attrs={"class": "form-control", "type": "date"}
+ ),
}
class LandForm(forms.ModelForm):
"""Form für das Erstellen und Bearbeiten von Ländern"""
-
+
class Meta:
model = Land
fields = [
# Grundlegende Identifikation
- 'lfd_nr', 'ew_nummer', 'grundbuchblatt',
+ "lfd_nr",
+ "ew_nummer",
+ "grundbuchblatt",
# Gerichtliche Zuständigkeit
- 'amtsgericht',
- # Verwaltungsstruktur
- 'gemeinde', 'gemarkung', 'flur', 'flurstueck', 'adresse',
+ "amtsgericht",
+ # Verwaltungsstruktur
+ "gemeinde",
+ "gemarkung",
+ "flur",
+ "flurstueck",
+ "adresse",
# Flächenangaben
- 'groesse_qm', 'gruenland_qm', 'acker_qm', 'wald_qm', 'sonstiges_qm',
+ "groesse_qm",
+ "gruenland_qm",
+ "acker_qm",
+ "wald_qm",
+ "sonstiges_qm",
# Legacy Verpachtung (für Kompatibilität)
- 'verpachtete_gesamtflaeche', 'flaeche_alte_liste', 'verp_flaeche_aktuell',
+ "verpachtete_gesamtflaeche",
+ "flaeche_alte_liste",
+ "verp_flaeche_aktuell",
# Aktuelle Verpachtung
- 'aktueller_paechter', 'paechter_name', 'paechter_anschrift',
- 'pachtbeginn', 'pachtende', 'verlaengerung_klausel',
- 'zahlungsweise', 'pachtzins_pro_ha', 'pachtzins_pauschal',
+ "aktueller_paechter",
+ "paechter_name",
+ "paechter_anschrift",
+ "pachtbeginn",
+ "pachtende",
+ "verlaengerung_klausel",
+ "zahlungsweise",
+ "pachtzins_pro_ha",
+ "pachtzins_pauschal",
# Umsatzsteuer
- 'ust_option', 'ust_satz',
+ "ust_option",
+ "ust_satz",
# Umlagen
- 'grundsteuer_umlage', 'versicherungen_umlage', 'verbandsbeitraege_umlage', 'jagdpacht_anteil_umlage',
+ "grundsteuer_umlage",
+ "versicherungen_umlage",
+ "verbandsbeitraege_umlage",
+ "jagdpacht_anteil_umlage",
# Legacy Steuern
- 'anteil_grundsteuer', 'anteil_lwk',
+ "anteil_grundsteuer",
+ "anteil_lwk",
# Status
- 'aktiv', 'notizen',
+ "aktiv",
+ "notizen",
]
widgets = {
# Grundlegende Identifikation
- 'lfd_nr': forms.TextInput(attrs={'class': 'form-control'}),
- 'ew_nummer': forms.TextInput(attrs={'class': 'form-control'}),
- 'grundbuchblatt': forms.TextInput(attrs={'class': 'form-control'}),
+ "lfd_nr": forms.TextInput(attrs={"class": "form-control"}),
+ "ew_nummer": forms.TextInput(attrs={"class": "form-control"}),
+ "grundbuchblatt": forms.TextInput(attrs={"class": "form-control"}),
# Gerichtliche Zuständigkeit
- 'amtsgericht': forms.TextInput(attrs={'class': 'form-control'}),
+ "amtsgericht": forms.TextInput(attrs={"class": "form-control"}),
# Verwaltungsstruktur
- 'gemeinde': forms.TextInput(attrs={'class': 'form-control'}),
- 'gemarkung': forms.TextInput(attrs={'class': 'form-control'}),
- 'flur': forms.TextInput(attrs={'class': 'form-control'}),
- 'flurstueck': forms.TextInput(attrs={'class': 'form-control'}),
- 'adresse': forms.TextInput(attrs={'class': 'form-control'}),
+ "gemeinde": forms.TextInput(attrs={"class": "form-control"}),
+ "gemarkung": forms.TextInput(attrs={"class": "form-control"}),
+ "flur": forms.TextInput(attrs={"class": "form-control"}),
+ "flurstueck": forms.TextInput(attrs={"class": "form-control"}),
+ "adresse": forms.TextInput(attrs={"class": "form-control"}),
# Flächenangaben
- 'groesse_qm': forms.NumberInput(attrs={'class': 'form-control', 'step': '0.01'}),
- 'gruenland_qm': forms.NumberInput(attrs={'class': 'form-control', 'step': '0.01'}),
- 'acker_qm': forms.NumberInput(attrs={'class': 'form-control', 'step': '0.01'}),
- 'wald_qm': forms.NumberInput(attrs={'class': 'form-control', 'step': '0.01'}),
- 'sonstiges_qm': forms.NumberInput(attrs={'class': 'form-control', 'step': '0.01'}),
+ "groesse_qm": forms.NumberInput(
+ attrs={"class": "form-control", "step": "0.01"}
+ ),
+ "gruenland_qm": forms.NumberInput(
+ attrs={"class": "form-control", "step": "0.01"}
+ ),
+ "acker_qm": forms.NumberInput(
+ attrs={"class": "form-control", "step": "0.01"}
+ ),
+ "wald_qm": forms.NumberInput(
+ attrs={"class": "form-control", "step": "0.01"}
+ ),
+ "sonstiges_qm": forms.NumberInput(
+ attrs={"class": "form-control", "step": "0.01"}
+ ),
# Legacy Verpachtung
- 'verpachtete_gesamtflaeche': forms.NumberInput(attrs={'class': 'form-control', 'step': '0.01'}),
- 'flaeche_alte_liste': forms.NumberInput(attrs={'class': 'form-control', 'step': '0.01'}),
- 'verp_flaeche_aktuell': forms.NumberInput(attrs={'class': 'form-control', 'step': '0.01'}),
+ "verpachtete_gesamtflaeche": forms.NumberInput(
+ attrs={"class": "form-control", "step": "0.01"}
+ ),
+ "flaeche_alte_liste": forms.NumberInput(
+ attrs={"class": "form-control", "step": "0.01"}
+ ),
+ "verp_flaeche_aktuell": forms.NumberInput(
+ attrs={"class": "form-control", "step": "0.01"}
+ ),
# Aktuelle Verpachtung
- 'aktueller_paechter': forms.Select(attrs={'class': 'form-select'}),
- 'paechter_name': forms.TextInput(attrs={'class': 'form-control'}),
- 'paechter_anschrift': forms.Textarea(attrs={'class': 'form-control', 'rows': 3}),
- 'pachtbeginn': forms.DateInput(attrs={'class': 'form-control', 'type': 'date'}),
- 'pachtende': forms.DateInput(attrs={'class': 'form-control', 'type': 'date'}),
- 'verlaengerung_klausel': forms.CheckboxInput(attrs={'class': 'form-check-input'}),
- 'zahlungsweise': forms.Select(attrs={'class': 'form-select'}),
- 'pachtzins_pro_ha': forms.NumberInput(attrs={'class': 'form-control', 'step': '0.01'}),
- 'pachtzins_pauschal': forms.NumberInput(attrs={'class': 'form-control', 'step': '0.01'}),
+ "aktueller_paechter": forms.Select(attrs={"class": "form-select"}),
+ "paechter_name": forms.TextInput(attrs={"class": "form-control"}),
+ "paechter_anschrift": forms.Textarea(
+ attrs={"class": "form-control", "rows": 3}
+ ),
+ "pachtbeginn": forms.DateInput(
+ attrs={"class": "form-control", "type": "date"}
+ ),
+ "pachtende": forms.DateInput(
+ attrs={"class": "form-control", "type": "date"}
+ ),
+ "verlaengerung_klausel": forms.CheckboxInput(
+ attrs={"class": "form-check-input"}
+ ),
+ "zahlungsweise": forms.Select(attrs={"class": "form-select"}),
+ "pachtzins_pro_ha": forms.NumberInput(
+ attrs={"class": "form-control", "step": "0.01"}
+ ),
+ "pachtzins_pauschal": forms.NumberInput(
+ attrs={"class": "form-control", "step": "0.01"}
+ ),
# Umsatzsteuer
- 'ust_option': forms.CheckboxInput(attrs={'class': 'form-check-input'}),
- 'ust_satz': forms.NumberInput(attrs={'class': 'form-control', 'step': '0.01'}),
+ "ust_option": forms.CheckboxInput(attrs={"class": "form-check-input"}),
+ "ust_satz": forms.NumberInput(
+ attrs={"class": "form-control", "step": "0.01"}
+ ),
# Umlagen
- 'grundsteuer_umlage': forms.CheckboxInput(attrs={'class': 'form-check-input'}),
- 'versicherungen_umlage': forms.CheckboxInput(attrs={'class': 'form-check-input'}),
- 'verbandsbeitraege_umlage': forms.CheckboxInput(attrs={'class': 'form-check-input'}),
- 'jagdpacht_anteil_umlage': forms.CheckboxInput(attrs={'class': 'form-check-input'}),
+ "grundsteuer_umlage": forms.CheckboxInput(
+ attrs={"class": "form-check-input"}
+ ),
+ "versicherungen_umlage": forms.CheckboxInput(
+ attrs={"class": "form-check-input"}
+ ),
+ "verbandsbeitraege_umlage": forms.CheckboxInput(
+ attrs={"class": "form-check-input"}
+ ),
+ "jagdpacht_anteil_umlage": forms.CheckboxInput(
+ attrs={"class": "form-check-input"}
+ ),
# Legacy
- 'anteil_grundsteuer': forms.NumberInput(attrs={'class': 'form-control', 'step': '0.01'}),
- 'anteil_lwk': forms.NumberInput(attrs={'class': 'form-control', 'step': '0.01'}),
+ "anteil_grundsteuer": forms.NumberInput(
+ attrs={"class": "form-control", "step": "0.01"}
+ ),
+ "anteil_lwk": forms.NumberInput(
+ attrs={"class": "form-control", "step": "0.01"}
+ ),
# Status
- 'aktiv': forms.CheckboxInput(attrs={'class': 'form-check-input'}),
- 'notizen': forms.Textarea(attrs={'class': 'form-control', 'rows': 3}),
+ "aktiv": forms.CheckboxInput(attrs={"class": "form-check-input"}),
+ "notizen": forms.Textarea(attrs={"class": "form-control", "rows": 3}),
}
class LandAbrechnungForm(forms.ModelForm):
"""Form für das Erstellen und Bearbeiten von Landabrechnungen"""
-
+
class Meta:
model = LandAbrechnung
fields = [
- 'land', 'abrechnungsjahr',
+ "land",
+ "abrechnungsjahr",
# Einnahmen
- 'pacht_vereinnahmt', 'umlagen_vereinnahmt', 'sonstige_einnahmen',
+ "pacht_vereinnahmt",
+ "umlagen_vereinnahmt",
+ "sonstige_einnahmen",
# Ausgaben
- 'grundsteuer_bescheid_nr', 'grundsteuer_betrag',
- 'versicherungen_betrag', 'verbandsbeitraege_betrag',
- 'sonstige_abgaben_betrag', 'instandhaltung_betrag', 'verwaltung_recht_betrag',
+ "grundsteuer_bescheid_nr",
+ "grundsteuer_betrag",
+ "versicherungen_betrag",
+ "verbandsbeitraege_betrag",
+ "sonstige_abgaben_betrag",
+ "instandhaltung_betrag",
+ "verwaltung_recht_betrag",
# Umsatzsteuer
- 'vorsteuer_aus_umlagen',
+ "vorsteuer_aus_umlagen",
# Sonstiges
- 'offene_posten', 'bemerkungen',
+ "offene_posten",
+ "bemerkungen",
# Dokumente werden über Paperless verknüpft, nicht hochgeladen
]
widgets = {
- 'land': forms.Select(attrs={'class': 'form-select'}),
- 'abrechnungsjahr': forms.NumberInput(attrs={'class': 'form-control', 'min': '2000', 'max': '2050'}),
+ "land": forms.Select(attrs={"class": "form-select"}),
+ "abrechnungsjahr": forms.NumberInput(
+ attrs={"class": "form-control", "min": "2000", "max": "2050"}
+ ),
# Einnahmen
- 'pacht_vereinnahmt': forms.NumberInput(attrs={'class': 'form-control', 'step': '0.01'}),
- 'umlagen_vereinnahmt': forms.NumberInput(attrs={'class': 'form-control', 'step': '0.01'}),
- 'sonstige_einnahmen': forms.NumberInput(attrs={'class': 'form-control', 'step': '0.01'}),
+ "pacht_vereinnahmt": forms.NumberInput(
+ attrs={"class": "form-control", "step": "0.01"}
+ ),
+ "umlagen_vereinnahmt": forms.NumberInput(
+ attrs={"class": "form-control", "step": "0.01"}
+ ),
+ "sonstige_einnahmen": forms.NumberInput(
+ attrs={"class": "form-control", "step": "0.01"}
+ ),
# Ausgaben
- 'grundsteuer_bescheid_nr': forms.TextInput(attrs={'class': 'form-control'}),
- 'grundsteuer_betrag': forms.NumberInput(attrs={'class': 'form-control', 'step': '0.01'}),
- 'versicherungen_betrag': forms.NumberInput(attrs={'class': 'form-control', 'step': '0.01'}),
- 'verbandsbeitraege_betrag': forms.NumberInput(attrs={'class': 'form-control', 'step': '0.01'}),
- 'sonstige_abgaben_betrag': forms.NumberInput(attrs={'class': 'form-control', 'step': '0.01'}),
- 'instandhaltung_betrag': forms.NumberInput(attrs={'class': 'form-control', 'step': '0.01'}),
- 'verwaltung_recht_betrag': forms.NumberInput(attrs={'class': 'form-control', 'step': '0.01'}),
+ "grundsteuer_bescheid_nr": forms.TextInput(attrs={"class": "form-control"}),
+ "grundsteuer_betrag": forms.NumberInput(
+ attrs={"class": "form-control", "step": "0.01"}
+ ),
+ "versicherungen_betrag": forms.NumberInput(
+ attrs={"class": "form-control", "step": "0.01"}
+ ),
+ "verbandsbeitraege_betrag": forms.NumberInput(
+ attrs={"class": "form-control", "step": "0.01"}
+ ),
+ "sonstige_abgaben_betrag": forms.NumberInput(
+ attrs={"class": "form-control", "step": "0.01"}
+ ),
+ "instandhaltung_betrag": forms.NumberInput(
+ attrs={"class": "form-control", "step": "0.01"}
+ ),
+ "verwaltung_recht_betrag": forms.NumberInput(
+ attrs={"class": "form-control", "step": "0.01"}
+ ),
# Umsatzsteuer
- 'vorsteuer_aus_umlagen': forms.NumberInput(attrs={'class': 'form-control', 'step': '0.01'}),
+ "vorsteuer_aus_umlagen": forms.NumberInput(
+ attrs={"class": "form-control", "step": "0.01"}
+ ),
# Sonstiges
- 'offene_posten': forms.NumberInput(attrs={'class': 'form-control', 'step': '0.01'}),
- 'bemerkungen': forms.Textarea(attrs={'class': 'form-control', 'rows': 4}),
+ "offene_posten": forms.NumberInput(
+ attrs={"class": "form-control", "step": "0.01"}
+ ),
+ "bemerkungen": forms.Textarea(attrs={"class": "form-control", "rows": 4}),
}
class DokumentLinkForm(forms.ModelForm):
"""Form für das Erstellen und Bearbeiten von Dokumentverknüpfungen"""
-
+
class Meta:
model = DokumentLink
- fields = '__all__'
+ fields = "__all__"
widgets = {
- 'paperless_id': forms.NumberInput(attrs={'class': 'form-control'}),
- 'content_type': forms.Select(attrs={'class': 'form-select'}),
- 'object_id': forms.TextInput(attrs={'class': 'form-control'}),
- 'verknuepft_am': forms.DateTimeInput(attrs={'class': 'form-control', 'type': 'datetime-local'}),
+ "paperless_id": forms.NumberInput(attrs={"class": "form-control"}),
+ "content_type": forms.Select(attrs={"class": "form-select"}),
+ "object_id": forms.TextInput(attrs={"class": "form-control"}),
+ "verknuepft_am": forms.DateTimeInput(
+ attrs={"class": "form-control", "type": "datetime-local"}
+ ),
}
class FoerderungForm(forms.ModelForm):
"""Form für das Erstellen und Bearbeiten von Förderungen"""
-
+
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
# Add empty option for optional fields
- self.fields['verwendungsnachweis'].empty_label = "--- Kein Dokument verknüpfen ---"
+ self.fields["verwendungsnachweis"].empty_label = (
+ "--- Kein Dokument verknüpfen ---"
+ )
# Ensure destinataer has proper choices
- from .models import Destinataer, DokumentLink
from django.utils import timezone
- self.fields['destinataer'].queryset = Destinataer.objects.all().order_by('nachname', 'vorname')
- self.fields['verwendungsnachweis'].queryset = DokumentLink.objects.all().order_by('titel')
+
+ from .models import Destinataer, DokumentLink
+
+ self.fields["destinataer"].queryset = Destinataer.objects.all().order_by(
+ "nachname", "vorname"
+ )
+ self.fields["verwendungsnachweis"].queryset = (
+ DokumentLink.objects.all().order_by("titel")
+ )
# Set current year as default for new forms
if not self.instance.pk:
- self.fields['jahr'].initial = timezone.now().year
-
+ self.fields["jahr"].initial = timezone.now().year
+
class Meta:
model = Foerderung
fields = [
- 'destinataer', 'jahr', 'betrag', 'kategorie', 'status',
- 'antragsdatum', 'entscheidungsdatum', 'verwendungsnachweis', 'bemerkungen'
+ "destinataer",
+ "jahr",
+ "betrag",
+ "kategorie",
+ "status",
+ "antragsdatum",
+ "entscheidungsdatum",
+ "verwendungsnachweis",
+ "bemerkungen",
]
widgets = {
- 'destinataer': forms.Select(attrs={'class': 'form-select'}),
- 'jahr': forms.NumberInput(attrs={'class': 'form-control'}),
- 'betrag': forms.NumberInput(attrs={'class': 'form-control', 'step': '0.01'}),
- 'kategorie': forms.Select(attrs={'class': 'form-select'}),
- 'status': forms.Select(attrs={'class': 'form-select'}),
- 'antragsdatum': forms.DateInput(attrs={'class': 'form-control', 'type': 'date'}),
- 'entscheidungsdatum': forms.DateInput(attrs={'class': 'form-control', 'type': 'date'}),
- 'verwendungsnachweis': forms.Select(attrs={'class': 'form-select'}),
- 'bemerkungen': forms.Textarea(attrs={'class': 'form-control', 'rows': 3}),
+ "destinataer": forms.Select(attrs={"class": "form-select"}),
+ "jahr": forms.NumberInput(attrs={"class": "form-control"}),
+ "betrag": forms.NumberInput(
+ attrs={"class": "form-control", "step": "0.01"}
+ ),
+ "kategorie": forms.Select(attrs={"class": "form-select"}),
+ "status": forms.Select(attrs={"class": "form-select"}),
+ "antragsdatum": forms.DateInput(
+ attrs={"class": "form-control", "type": "date"}
+ ),
+ "entscheidungsdatum": forms.DateInput(
+ attrs={"class": "form-control", "type": "date"}
+ ),
+ "verwendungsnachweis": forms.Select(attrs={"class": "form-select"}),
+ "bemerkungen": forms.Textarea(attrs={"class": "form-control", "rows": 3}),
}
-
+
labels = {
- 'destinataer': 'Destinatär',
- 'verwendungsnachweis': 'Verknüpftes Dokument',
- 'bemerkungen': 'Bemerkungen/Beschreibung',
- 'antragsdatum': 'Antragsdatum',
- 'entscheidungsdatum': 'Entscheidungsdatum',
+ "destinataer": "Destinatär",
+ "verwendungsnachweis": "Verknüpftes Dokument",
+ "bemerkungen": "Bemerkungen/Beschreibung",
+ "antragsdatum": "Antragsdatum",
+ "entscheidungsdatum": "Entscheidungsdatum",
}
-
+
help_texts = {
- 'verwendungsnachweis': 'Optionale Verknüpfung zu einem Dokument aus dem Paperless-System',
- 'entscheidungsdatum': 'Datum der Bewilligung/Ablehnung (optional)',
- 'bemerkungen': 'Zusätzliche Informationen zur Förderung',
+ "verwendungsnachweis": "Optionale Verknüpfung zu einem Dokument aus dem Paperless-System",
+ "entscheidungsdatum": "Datum der Bewilligung/Ablehnung (optional)",
+ "bemerkungen": "Zusätzliche Informationen zur Förderung",
}
class UnterstuetzungForm(forms.ModelForm):
"""Form für das Erstellen und Bearbeiten von Unterstützungen"""
-
+
# Special field for creating recurring payments
ist_wiederkehrend = forms.BooleanField(
- required=False,
- label='Wiederkehrende Zahlung',
- help_text='Aktivieren Sie diese Option um automatisch wiederkehrende Zahlungen zu erstellen'
+ required=False,
+ label="Wiederkehrende Zahlung",
+ help_text="Aktivieren Sie diese Option um automatisch wiederkehrende Zahlungen zu erstellen",
)
intervall = forms.ChoiceField(
- choices=[('', '--- Wählen Sie ein Intervall ---')] + UnterstuetzungWiederkehrend.INTERVALL_CHOICES,
+ choices=[("", "--- Wählen Sie ein Intervall ---")]
+ + UnterstuetzungWiederkehrend.INTERVALL_CHOICES,
required=False,
- widget=forms.Select(attrs={'class': 'form-select'}),
- label='Zahlungsintervall'
+ widget=forms.Select(attrs={"class": "form-select"}),
+ label="Zahlungsintervall",
)
letzte_zahlung_am = forms.DateField(
required=False,
- widget=forms.DateInput(attrs={'class': 'form-control', 'type': 'date'}),
- label='Letzte Zahlung am (optional)',
- help_text='Leer lassen für unbegrenzte Wiederholung'
+ widget=forms.DateInput(attrs={"class": "form-control", "type": "date"}),
+ label="Letzte Zahlung am (optional)",
+ help_text="Leer lassen für unbegrenzte Wiederholung",
)
-
+
class Meta:
model = DestinataerUnterstuetzung
fields = [
- 'destinataer', 'konto', 'faellig_am', 'betrag', 'status',
- 'beschreibung', 'empfaenger_iban', 'empfaenger_name', 'verwendungszweck'
+ "destinataer",
+ "konto",
+ "faellig_am",
+ "betrag",
+ "status",
+ "beschreibung",
+ "empfaenger_iban",
+ "empfaenger_name",
+ "verwendungszweck",
]
-
+
widgets = {
- 'destinataer': forms.Select(attrs={'class': 'form-select'}),
- 'konto': forms.Select(attrs={'class': 'form-select'}),
- 'faellig_am': forms.DateInput(attrs={'class': 'form-control', 'type': 'date'}),
- 'betrag': forms.NumberInput(attrs={'class': 'form-control', 'step': '0.01'}),
- 'status': forms.Select(attrs={'class': 'form-select'}),
- 'beschreibung': forms.TextInput(attrs={'class': 'form-control'}),
- 'empfaenger_iban': forms.TextInput(attrs={'class': 'form-control', 'placeholder': 'DE89 3704 0044 0532 0130 00'}),
- 'empfaenger_name': forms.TextInput(attrs={'class': 'form-control'}),
- 'verwendungszweck': forms.TextInput(attrs={'class': 'form-control', 'maxlength': '140'}),
+ "destinataer": forms.Select(attrs={"class": "form-select"}),
+ "konto": forms.Select(attrs={"class": "form-select"}),
+ "faellig_am": forms.DateInput(
+ attrs={"class": "form-control", "type": "date"}
+ ),
+ "betrag": forms.NumberInput(
+ attrs={"class": "form-control", "step": "0.01"}
+ ),
+ "status": forms.Select(attrs={"class": "form-select"}),
+ "beschreibung": forms.TextInput(attrs={"class": "form-control"}),
+ "empfaenger_iban": forms.TextInput(
+ attrs={
+ "class": "form-control",
+ "placeholder": "DE89 3704 0044 0532 0130 00",
+ }
+ ),
+ "empfaenger_name": forms.TextInput(attrs={"class": "form-control"}),
+ "verwendungszweck": forms.TextInput(
+ attrs={"class": "form-control", "maxlength": "140"}
+ ),
}
-
+
labels = {
- 'destinataer': 'Destinatär',
- 'konto': 'Zahlungskonto',
- 'faellig_am': 'Fällig am',
- 'betrag': 'Betrag (€)',
- 'status': 'Status',
- 'beschreibung': 'Beschreibung',
- 'empfaenger_iban': 'Empfänger IBAN',
- 'empfaenger_name': 'Empfänger Name',
- 'verwendungszweck': 'Verwendungszweck',
+ "destinataer": "Destinatär",
+ "konto": "Zahlungskonto",
+ "faellig_am": "Fällig am",
+ "betrag": "Betrag (€)",
+ "status": "Status",
+ "beschreibung": "Beschreibung",
+ "empfaenger_iban": "Empfänger IBAN",
+ "empfaenger_name": "Empfänger Name",
+ "verwendungszweck": "Verwendungszweck",
}
-
+
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
# Add onchange event to destinataer field for AJAX IBAN fetching
- self.fields['destinataer'].widget.attrs['onchange'] = 'updateDestinataerInfo()'
-
+ self.fields["destinataer"].widget.attrs["onchange"] = "updateDestinataerInfo()"
+
def clean(self):
cleaned_data = super().clean()
- ist_wiederkehrend = cleaned_data.get('ist_wiederkehrend')
- intervall = cleaned_data.get('intervall')
-
+ ist_wiederkehrend = cleaned_data.get("ist_wiederkehrend")
+ intervall = cleaned_data.get("intervall")
+
if ist_wiederkehrend and not intervall:
- raise forms.ValidationError('Bitte wählen Sie ein Zahlungsintervall für wiederkehrende Zahlungen.')
-
+ raise forms.ValidationError(
+ "Bitte wählen Sie ein Zahlungsintervall für wiederkehrende Zahlungen."
+ )
+
return cleaned_data
class UnterstuetzungWiederkehrendForm(forms.ModelForm):
"""Form für das Bearbeiten von wiederkehrenden Unterstützungsvorlagen"""
-
+
class Meta:
model = UnterstuetzungWiederkehrend
fields = [
- 'destinataer', 'konto', 'betrag', 'intervall', 'beschreibung',
- 'empfaenger_iban', 'empfaenger_name', 'verwendungszweck',
- 'erste_zahlung_am', 'letzte_zahlung_am', 'aktiv'
+ "destinataer",
+ "konto",
+ "betrag",
+ "intervall",
+ "beschreibung",
+ "empfaenger_iban",
+ "empfaenger_name",
+ "verwendungszweck",
+ "erste_zahlung_am",
+ "letzte_zahlung_am",
+ "aktiv",
]
-
+
widgets = {
- 'destinataer': forms.Select(attrs={'class': 'form-select'}),
- 'konto': forms.Select(attrs={'class': 'form-select'}),
- 'betrag': forms.NumberInput(attrs={'class': 'form-control', 'step': '0.01'}),
- 'intervall': forms.Select(attrs={'class': 'form-select'}),
- 'beschreibung': forms.TextInput(attrs={'class': 'form-control'}),
- 'empfaenger_iban': forms.TextInput(attrs={'class': 'form-control', 'placeholder': 'DE89 3704 0044 0532 0130 00'}),
- 'empfaenger_name': forms.TextInput(attrs={'class': 'form-control'}),
- 'verwendungszweck': forms.TextInput(attrs={'class': 'form-control', 'maxlength': '140'}),
- 'erste_zahlung_am': forms.DateInput(attrs={'class': 'form-control', 'type': 'date'}),
- 'letzte_zahlung_am': forms.DateInput(attrs={'class': 'form-control', 'type': 'date'}),
- 'aktiv': forms.CheckboxInput(attrs={'class': 'form-check-input'}),
+ "destinataer": forms.Select(attrs={"class": "form-select"}),
+ "konto": forms.Select(attrs={"class": "form-select"}),
+ "betrag": forms.NumberInput(
+ attrs={"class": "form-control", "step": "0.01"}
+ ),
+ "intervall": forms.Select(attrs={"class": "form-select"}),
+ "beschreibung": forms.TextInput(attrs={"class": "form-control"}),
+ "empfaenger_iban": forms.TextInput(
+ attrs={
+ "class": "form-control",
+ "placeholder": "DE89 3704 0044 0532 0130 00",
+ }
+ ),
+ "empfaenger_name": forms.TextInput(attrs={"class": "form-control"}),
+ "verwendungszweck": forms.TextInput(
+ attrs={"class": "form-control", "maxlength": "140"}
+ ),
+ "erste_zahlung_am": forms.DateInput(
+ attrs={"class": "form-control", "type": "date"}
+ ),
+ "letzte_zahlung_am": forms.DateInput(
+ attrs={"class": "form-control", "type": "date"}
+ ),
+ "aktiv": forms.CheckboxInput(attrs={"class": "form-check-input"}),
}
class UnterstuetzungMarkAsPaidForm(forms.Form):
"""Simple form to mark an Unterstützung as paid"""
-
+
ausgezahlt_am = forms.DateField(
- widget=forms.DateInput(attrs={'class': 'form-control', 'type': 'date'}),
- label='Ausgezahlt am',
- initial=timezone.now().date()
+ widget=forms.DateInput(attrs={"class": "form-control", "type": "date"}),
+ label="Ausgezahlt am",
+ initial=timezone.now().date(),
)
-
+
bemerkung = forms.CharField(
- widget=forms.Textarea(attrs={'class': 'form-control', 'rows': 3}),
- label='Bemerkung (optional)',
+ widget=forms.Textarea(attrs={"class": "form-control", "rows": 3}),
+ label="Bemerkung (optional)",
required=False,
- help_text='Optionale Notiz zur Zahlung'
+ help_text="Optionale Notiz zur Zahlung",
)
-
-
class BankTransactionForm(forms.ModelForm):
"""Form für das Bearbeiten von Banktransaktionen"""
-
+
class Meta:
model = BankTransaction
fields = [
- 'konto', 'datum', 'valuta', 'betrag', 'waehrung',
- 'verwendungszweck', 'empfaenger_zahlungspflichtiger',
- 'iban_gegenpartei', 'bic_gegenpartei', 'transaction_type',
- 'status', 'kommentare', 'verwaltungskosten'
+ "konto",
+ "datum",
+ "valuta",
+ "betrag",
+ "waehrung",
+ "verwendungszweck",
+ "empfaenger_zahlungspflichtiger",
+ "iban_gegenpartei",
+ "bic_gegenpartei",
+ "transaction_type",
+ "status",
+ "kommentare",
+ "verwaltungskosten",
]
-
+
widgets = {
- 'konto': forms.Select(attrs={'class': 'form-select'}),
- 'datum': forms.DateInput(attrs={'class': 'form-control', 'type': 'date'}),
- 'valuta': forms.DateInput(attrs={'class': 'form-control', 'type': 'date'}),
- 'betrag': forms.NumberInput(attrs={'class': 'form-control', 'step': '0.01'}),
- 'waehrung': forms.TextInput(attrs={'class': 'form-control'}),
- 'verwendungszweck': forms.Textarea(attrs={'class': 'form-control', 'rows': 3}),
- 'empfaenger_zahlungspflichtiger': forms.TextInput(attrs={'class': 'form-control'}),
- 'iban_gegenpartei': forms.TextInput(attrs={'class': 'form-control'}),
- 'bic_gegenpartei': forms.TextInput(attrs={'class': 'form-control'}),
- 'transaction_type': forms.Select(attrs={'class': 'form-select'}),
- 'status': forms.Select(attrs={'class': 'form-select'}),
- 'kommentare': forms.Textarea(attrs={'class': 'form-control', 'rows': 4}),
- 'verwaltungskosten': forms.Select(attrs={'class': 'form-select'}),
+ "konto": forms.Select(attrs={"class": "form-select"}),
+ "datum": forms.DateInput(attrs={"class": "form-control", "type": "date"}),
+ "valuta": forms.DateInput(attrs={"class": "form-control", "type": "date"}),
+ "betrag": forms.NumberInput(
+ attrs={"class": "form-control", "step": "0.01"}
+ ),
+ "waehrung": forms.TextInput(attrs={"class": "form-control"}),
+ "verwendungszweck": forms.Textarea(
+ attrs={"class": "form-control", "rows": 3}
+ ),
+ "empfaenger_zahlungspflichtiger": forms.TextInput(
+ attrs={"class": "form-control"}
+ ),
+ "iban_gegenpartei": forms.TextInput(attrs={"class": "form-control"}),
+ "bic_gegenpartei": forms.TextInput(attrs={"class": "form-control"}),
+ "transaction_type": forms.Select(attrs={"class": "form-select"}),
+ "status": forms.Select(attrs={"class": "form-select"}),
+ "kommentare": forms.Textarea(attrs={"class": "form-control", "rows": 4}),
+ "verwaltungskosten": forms.Select(attrs={"class": "form-select"}),
}
class DestinataerUnterstuetzungForm(forms.ModelForm):
"""Form für geplante/ausgeführte Destinatärunterstützungen"""
+
class Meta:
model = DestinataerUnterstuetzung
- fields = ['destinataer', 'konto', 'betrag', 'faellig_am', 'status', 'beschreibung']
+ fields = [
+ "destinataer",
+ "konto",
+ "betrag",
+ "faellig_am",
+ "status",
+ "beschreibung",
+ ]
widgets = {
- 'destinataer': forms.Select(attrs={'class': 'form-select'}),
- 'konto': forms.Select(attrs={'class': 'form-select'}),
- 'betrag': forms.NumberInput(attrs={'class': 'form-control', 'step': '0.01'}),
- 'faellig_am': forms.DateInput(attrs={'class': 'form-control', 'type': 'date'}),
- 'status': forms.Select(attrs={'class': 'form-select'}),
- 'beschreibung': forms.TextInput(attrs={'class': 'form-control'}),
+ "destinataer": forms.Select(attrs={"class": "form-select"}),
+ "konto": forms.Select(attrs={"class": "form-select"}),
+ "betrag": forms.NumberInput(
+ attrs={"class": "form-control", "step": "0.01"}
+ ),
+ "faellig_am": forms.DateInput(
+ attrs={"class": "form-control", "type": "date"}
+ ),
+ "status": forms.Select(attrs={"class": "form-select"}),
+ "beschreibung": forms.TextInput(attrs={"class": "form-control"}),
}
class DestinataerNotizForm(forms.ModelForm):
class Meta:
model = DestinataerNotiz
- fields = ['titel', 'text', 'datei']
+ fields = ["titel", "text", "datei"]
widgets = {
- 'titel': forms.TextInput(attrs={'class': 'form-control', 'placeholder': 'z.B. Telefonat vom 29.08.2025'}),
- 'text': forms.Textarea(attrs={'class': 'form-control', 'rows': 5, 'placeholder': 'Notiztext...'}),
- 'datei': forms.ClearableFileInput(attrs={'class': 'form-control'}),
+ "titel": forms.TextInput(
+ attrs={
+ "class": "form-control",
+ "placeholder": "z.B. Telefonat vom 29.08.2025",
+ }
+ ),
+ "text": forms.Textarea(
+ attrs={
+ "class": "form-control",
+ "rows": 5,
+ "placeholder": "Notiztext...",
+ }
+ ),
+ "datei": forms.ClearableFileInput(attrs={"class": "form-control"}),
}
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
# Make all fields optional
- self.fields['datei'].required = False
- self.fields['titel'].required = False
- self.fields['text'].required = False
+ self.fields["datei"].required = False
+ self.fields["titel"].required = False
+ self.fields["text"].required = False
def clean(self):
cleaned = super().clean()
- titel = cleaned.get('titel', '').strip()
- text = cleaned.get('text', '').strip()
+ titel = cleaned.get("titel", "").strip()
+ text = cleaned.get("text", "").strip()
if not (titel or text):
- raise forms.ValidationError('Bitte geben Sie einen Titel oder einen Text ein.')
+ raise forms.ValidationError(
+ "Bitte geben Sie einen Titel oder einen Text ein."
+ )
return cleaned
class BankImportForm(forms.Form):
"""Form für den Import von Bankdaten"""
-
+
konto = forms.ModelChoiceField(
queryset=StiftungsKonto.objects.filter(aktiv=True),
- widget=forms.Select(attrs={'class': 'form-select'}),
- label="Zielkonto"
+ widget=forms.Select(attrs={"class": "form-select"}),
+ label="Zielkonto",
)
-
+
datei = forms.FileField(
- widget=forms.FileInput(attrs={'class': 'form-control', 'accept': '.csv,.txt'}),
+ widget=forms.FileInput(attrs={"class": "form-control", "accept": ".csv,.txt"}),
label="Bankdatei",
- help_text="Unterstützte Formate: CSV, TXT (Sparkasse, Volksbank, etc.)"
+ help_text="Unterstützte Formate: CSV, TXT (Sparkasse, Volksbank, etc.)",
)
-
+
encoding = forms.ChoiceField(
choices=[
- ('utf-8', 'UTF-8'),
- ('latin1', 'Latin-1 / ISO-8859-1'),
- ('cp1252', 'Windows-1252'),
+ ("utf-8", "UTF-8"),
+ ("latin1", "Latin-1 / ISO-8859-1"),
+ ("cp1252", "Windows-1252"),
],
- initial='utf-8',
- widget=forms.Select(attrs={'class': 'form-select'}),
- label="Zeichenkodierung"
+ initial="utf-8",
+ widget=forms.Select(attrs={"class": "form-select"}),
+ label="Zeichenkodierung",
)
-
+
delimiter = forms.ChoiceField(
choices=[
- (';', 'Semikolon (;)'),
- (',', 'Komma (,)'),
- ('\t', 'Tab'),
+ (";", "Semikolon (;)"),
+ (",", "Komma (,)"),
+ ("\t", "Tab"),
],
- initial=';',
- widget=forms.Select(attrs={'class': 'form-select'}),
- label="Trennzeichen"
+ initial=";",
+ widget=forms.Select(attrs={"class": "form-select"}),
+ label="Trennzeichen",
)
-
+
skip_header = forms.BooleanField(
initial=True,
required=False,
- widget=forms.CheckboxInput(attrs={'class': 'form-check-input'}),
- label="Erste Zeile überspringen (Spaltenüberschriften)"
+ widget=forms.CheckboxInput(attrs={"class": "form-check-input"}),
+ label="Erste Zeile überspringen (Spaltenüberschriften)",
)
@@ -715,229 +1007,312 @@ class BankImportForm(forms.Form):
# USER MANAGEMENT FORMS
# =============================================================================
+
class UserCreationForm(forms.Form):
"""Form für die Erstellung neuer Benutzer"""
+
username = forms.CharField(
label="Benutzername",
max_length=150,
help_text="Eindeutiger Benutzername für die Anmeldung",
- widget=forms.TextInput(attrs={'class': 'form-control'})
+ widget=forms.TextInput(attrs={"class": "form-control"}),
)
-
+
email = forms.EmailField(
label="E-Mail-Adresse",
help_text="E-Mail-Adresse des Benutzers",
- widget=forms.EmailInput(attrs={'class': 'form-control'})
+ widget=forms.EmailInput(attrs={"class": "form-control"}),
)
-
+
first_name = forms.CharField(
label="Vorname",
max_length=30,
required=False,
- widget=forms.TextInput(attrs={'class': 'form-control'})
+ widget=forms.TextInput(attrs={"class": "form-control"}),
)
-
+
last_name = forms.CharField(
label="Nachname",
max_length=150,
required=False,
- widget=forms.TextInput(attrs={'class': 'form-control'})
+ widget=forms.TextInput(attrs={"class": "form-control"}),
)
-
+
password1 = forms.CharField(
label="Passwort",
- widget=forms.PasswordInput(attrs={'class': 'form-control'}),
- help_text="Mindestens 8 Zeichen"
+ widget=forms.PasswordInput(attrs={"class": "form-control"}),
+ help_text="Mindestens 8 Zeichen",
)
-
+
password2 = forms.CharField(
label="Passwort bestätigen",
- widget=forms.PasswordInput(attrs={'class': 'form-control'}),
- help_text="Geben Sie das Passwort zur Bestätigung erneut ein"
+ widget=forms.PasswordInput(attrs={"class": "form-control"}),
+ help_text="Geben Sie das Passwort zur Bestätigung erneut ein",
)
-
+
is_active = forms.BooleanField(
label="Aktiv",
required=False,
initial=True,
help_text="Benutzer kann sich anmelden",
- widget=forms.CheckboxInput(attrs={'class': 'form-check-input'})
+ widget=forms.CheckboxInput(attrs={"class": "form-check-input"}),
)
-
+
is_staff = forms.BooleanField(
label="Staff-Status",
required=False,
help_text="Benutzer kann auf Django Admin zugreifen",
- widget=forms.CheckboxInput(attrs={'class': 'form-check-input'})
+ widget=forms.CheckboxInput(attrs={"class": "form-check-input"}),
)
-
+
def clean_username(self):
- username = self.cleaned_data['username']
+ username = self.cleaned_data["username"]
from django.contrib.auth.models import User
+
if User.objects.filter(username=username).exists():
- raise forms.ValidationError("Ein Benutzer mit diesem Namen existiert bereits.")
+ raise forms.ValidationError(
+ "Ein Benutzer mit diesem Namen existiert bereits."
+ )
return username
-
+
def clean_email(self):
- email = self.cleaned_data['email']
+ email = self.cleaned_data["email"]
from django.contrib.auth.models import User
+
if User.objects.filter(email=email).exists():
- raise forms.ValidationError("Ein Benutzer mit dieser E-Mail-Adresse existiert bereits.")
+ raise forms.ValidationError(
+ "Ein Benutzer mit dieser E-Mail-Adresse existiert bereits."
+ )
return email
def clean(self):
cleaned_data = super().clean()
password1 = cleaned_data.get("password1")
password2 = cleaned_data.get("password2")
-
+
if password1 and password2:
if password1 != password2:
raise forms.ValidationError("Die Passwörter stimmen nicht überein.")
if len(password1) < 8:
- raise forms.ValidationError("Das Passwort muss mindestens 8 Zeichen lang sein.")
-
+ raise forms.ValidationError(
+ "Das Passwort muss mindestens 8 Zeichen lang sein."
+ )
+
return cleaned_data
class UserUpdateForm(forms.ModelForm):
"""Form für die Bearbeitung bestehender Benutzer"""
-
+
class Meta:
from django.contrib.auth.models import User
+
model = User
- fields = ['username', 'email', 'first_name', 'last_name', 'is_active', 'is_staff']
+ fields = [
+ "username",
+ "email",
+ "first_name",
+ "last_name",
+ "is_active",
+ "is_staff",
+ ]
widgets = {
- 'username': forms.TextInput(attrs={'class': 'form-control'}),
- 'email': forms.EmailInput(attrs={'class': 'form-control'}),
- 'first_name': forms.TextInput(attrs={'class': 'form-control'}),
- 'last_name': forms.TextInput(attrs={'class': 'form-control'}),
- 'is_active': forms.CheckboxInput(attrs={'class': 'form-check-input'}),
- 'is_staff': forms.CheckboxInput(attrs={'class': 'form-check-input'}),
+ "username": forms.TextInput(attrs={"class": "form-control"}),
+ "email": forms.EmailInput(attrs={"class": "form-control"}),
+ "first_name": forms.TextInput(attrs={"class": "form-control"}),
+ "last_name": forms.TextInput(attrs={"class": "form-control"}),
+ "is_active": forms.CheckboxInput(attrs={"class": "form-check-input"}),
+ "is_staff": forms.CheckboxInput(attrs={"class": "form-check-input"}),
}
labels = {
- 'username': 'Benutzername',
- 'email': 'E-Mail-Adresse',
- 'first_name': 'Vorname',
- 'last_name': 'Nachname',
- 'is_active': 'Aktiv',
- 'is_staff': 'Staff-Status',
+ "username": "Benutzername",
+ "email": "E-Mail-Adresse",
+ "first_name": "Vorname",
+ "last_name": "Nachname",
+ "is_active": "Aktiv",
+ "is_staff": "Staff-Status",
}
help_texts = {
- 'username': 'Eindeutiger Benutzername für die Anmeldung',
- 'email': 'E-Mail-Adresse des Benutzers',
- 'is_active': 'Benutzer kann sich anmelden',
- 'is_staff': 'Benutzer kann auf Django Admin zugreifen',
+ "username": "Eindeutiger Benutzername für die Anmeldung",
+ "email": "E-Mail-Adresse des Benutzers",
+ "is_active": "Benutzer kann sich anmelden",
+ "is_staff": "Benutzer kann auf Django Admin zugreifen",
}
class PasswordChangeForm(forms.Form):
"""Form für Passwort-Änderungen"""
+
new_password1 = forms.CharField(
label="Neues Passwort",
- widget=forms.PasswordInput(attrs={'class': 'form-control'}),
- help_text="Mindestens 8 Zeichen"
+ widget=forms.PasswordInput(attrs={"class": "form-control"}),
+ help_text="Mindestens 8 Zeichen",
)
-
+
new_password2 = forms.CharField(
label="Neues Passwort bestätigen",
- widget=forms.PasswordInput(attrs={'class': 'form-control'}),
- help_text="Geben Sie das neue Passwort zur Bestätigung erneut ein"
+ widget=forms.PasswordInput(attrs={"class": "form-control"}),
+ help_text="Geben Sie das neue Passwort zur Bestätigung erneut ein",
)
def clean(self):
cleaned_data = super().clean()
password1 = cleaned_data.get("new_password1")
password2 = cleaned_data.get("new_password2")
-
+
if password1 and password2:
if password1 != password2:
raise forms.ValidationError("Die Passwörter stimmen nicht überein.")
if len(password1) < 8:
- raise forms.ValidationError("Das Passwort muss mindestens 8 Zeichen lang sein.")
-
+ raise forms.ValidationError(
+ "Das Passwort muss mindestens 8 Zeichen lang sein."
+ )
+
return cleaned_data
class UserPermissionForm(forms.Form):
"""Form für die Zuweisung von Berechtigungen"""
-
+
def __init__(self, *args, **kwargs):
- user = kwargs.pop('user', None)
+ user = kwargs.pop("user", None)
super().__init__(*args, **kwargs)
-
+
from django.contrib.auth.models import Permission
-
+
# Get all custom permissions for stiftung app
- app_permissions = Permission.objects.filter(content_type__app_label='stiftung').order_by('name')
-
+ app_permissions = Permission.objects.filter(
+ content_type__app_label="stiftung"
+ ).order_by("name")
+
# Create checkbox fields for each permission
for perm in app_permissions:
- field_name = f'perm_{perm.id}'
+ field_name = f"perm_{perm.id}"
self.fields[field_name] = forms.BooleanField(
label=perm.name,
required=False,
- widget=forms.CheckboxInput(attrs={'class': 'form-check-input'})
+ widget=forms.CheckboxInput(attrs={"class": "form-check-input"}),
)
-
+
# Set initial values if user is provided
if user:
- self.fields[field_name].initial = user.has_perm(f'stiftung.{perm.codename}')
-
+ self.fields[field_name].initial = user.has_perm(
+ f"stiftung.{perm.codename}"
+ )
+
def get_permission_groups(self):
"""Group permissions by functionality for template rendering"""
from django.contrib.auth.models import Permission
-
+
groups = {
- 'entities': {
- 'name': 'Entitäten verwalten',
- 'permissions': [],
- 'icon': 'fas fa-users'
+ "entities": {
+ "name": "Entitäten verwalten",
+ "permissions": [],
+ "icon": "fas fa-users",
},
- 'documents': {
- 'name': 'Dokumentenverwaltung',
- 'permissions': [],
- 'icon': 'fas fa-folder-open'
+ "documents": {
+ "name": "Dokumentenverwaltung",
+ "permissions": [],
+ "icon": "fas fa-folder-open",
},
- 'financial': {
- 'name': 'Finanzverwaltung',
- 'permissions': [],
- 'icon': 'fas fa-euro-sign'
+ "financial": {
+ "name": "Finanzverwaltung",
+ "permissions": [],
+ "icon": "fas fa-euro-sign",
},
- 'administration': {
- 'name': 'Administration',
- 'permissions': [],
- 'icon': 'fas fa-cogs'
+ "administration": {
+ "name": "Administration",
+ "permissions": [],
+ "icon": "fas fa-cogs",
},
- 'system': {
- 'name': 'System',
- 'permissions': [],
- 'icon': 'fas fa-server'
- }
+ "system": {"name": "System", "permissions": [], "icon": "fas fa-server"},
}
-
+
# Get all permissions to properly categorize them
for field_name, field in self.fields.items():
- if field_name.startswith('perm_'):
+ if field_name.startswith("perm_"):
# Extract permission ID from field name
- perm_id = field_name.replace('perm_', '')
+ perm_id = field_name.replace("perm_", "")
try:
permission = Permission.objects.get(id=perm_id)
label = permission.name.lower()
codename = permission.codename.lower()
-
+
# More precise categorization based on both name and codename
- if any(word in codename for word in ['destinataer', 'land', 'paechter', 'verpachtung', 'foerderung']) and 'manage_' in codename or 'view_' in codename:
- groups['entities']['permissions'].append((field_name, field, permission))
- elif any(word in codename for word in ['documents', 'link_documents']) or 'dokument' in label:
- groups['documents']['permissions'].append((field_name, field, permission))
- elif any(word in codename for word in ['verwaltungskosten', 'konten', 'rentmeister', 'approve_payments']) or any(word in label for word in ['verwaltungskosten', 'konto', 'rentmeister', 'zahlung']):
- groups['financial']['permissions'].append((field_name, field, permission))
- elif any(word in codename for word in ['administration', 'audit', 'backup', 'manage_users', 'manage_permissions']) or any(word in label for word in ['administration', 'audit', 'backup', 'benutzer', 'berechtigung']):
- groups['administration']['permissions'].append((field_name, field, permission))
+ if (
+ any(
+ word in codename
+ for word in [
+ "destinataer",
+ "land",
+ "paechter",
+ "verpachtung",
+ "foerderung",
+ ]
+ )
+ and "manage_" in codename
+ or "view_" in codename
+ ):
+ groups["entities"]["permissions"].append(
+ (field_name, field, permission)
+ )
+ elif (
+ any(
+ word in codename for word in ["documents", "link_documents"]
+ )
+ or "dokument" in label
+ ):
+ groups["documents"]["permissions"].append(
+ (field_name, field, permission)
+ )
+ elif any(
+ word in codename
+ for word in [
+ "verwaltungskosten",
+ "konten",
+ "rentmeister",
+ "approve_payments",
+ ]
+ ) or any(
+ word in label
+ for word in [
+ "verwaltungskosten",
+ "konto",
+ "rentmeister",
+ "zahlung",
+ ]
+ ):
+ groups["financial"]["permissions"].append(
+ (field_name, field, permission)
+ )
+ elif any(
+ word in codename
+ for word in [
+ "administration",
+ "audit",
+ "backup",
+ "manage_users",
+ "manage_permissions",
+ ]
+ ) or any(
+ word in label
+ for word in [
+ "administration",
+ "audit",
+ "backup",
+ "benutzer",
+ "berechtigung",
+ ]
+ ):
+ groups["administration"]["permissions"].append(
+ (field_name, field, permission)
+ )
else:
- groups['system']['permissions'].append((field_name, field, permission))
+ groups["system"]["permissions"].append(
+ (field_name, field, permission)
+ )
except Permission.DoesNotExist:
# Fallback for permissions that don't exist
- groups['system']['permissions'].append((field_name, field, None))
-
- return groups
\ No newline at end of file
+ groups["system"]["permissions"].append((field_name, field, None))
+
+ return groups
diff --git a/app/stiftung/management/commands/generate_recurring_payments.py b/app/stiftung/management/commands/generate_recurring_payments.py
index 0e4149c..b8c35d1 100644
--- a/app/stiftung/management/commands/generate_recurring_payments.py
+++ b/app/stiftung/management/commands/generate_recurring_payments.py
@@ -3,60 +3,64 @@ Management command to generate due recurring support payments.
This command should be run daily via cron or similar scheduling system.
"""
+import logging
+from datetime import timedelta
+
from django.core.management.base import BaseCommand
from django.utils import timezone
-from datetime import timedelta
+
from stiftung.models import UnterstuetzungWiederkehrend
-import logging
logger = logging.getLogger(__name__)
+
class Command(BaseCommand):
- help = 'Generate due recurring support payments'
-
+ help = "Generate due recurring support payments"
+
def add_arguments(self, parser):
parser.add_argument(
- '--dry-run',
- action='store_true',
- help='Show what would be generated without actually creating payments',
+ "--dry-run",
+ action="store_true",
+ help="Show what would be generated without actually creating payments",
)
parser.add_argument(
- '--days-ahead',
+ "--days-ahead",
type=int,
default=0,
- help='Generate payments that are due within this many days (default: 0 = only today)',
+ help="Generate payments that are due within this many days (default: 0 = only today)",
)
-
+
def handle(self, *args, **options):
- dry_run = options['dry_run']
- days_ahead = options['days_ahead']
-
+ dry_run = options["dry_run"]
+ days_ahead = options["days_ahead"]
+
heute = timezone.now().date()
cutoff_date = heute + timedelta(days=days_ahead)
-
+
self.stdout.write(
self.style.SUCCESS(
f'Checking for recurring payments due up to {cutoff_date.strftime("%d.%m.%Y")}...'
)
)
-
+
if dry_run:
- self.stdout.write(self.style.WARNING('DRY RUN MODE - No payments will be created'))
-
+ self.stdout.write(
+ self.style.WARNING("DRY RUN MODE - No payments will be created")
+ )
+
# Get all active recurring payment templates that are due
templates = UnterstuetzungWiederkehrend.objects.filter(
- aktiv=True,
- naechste_generierung__lte=cutoff_date
- ).select_related('destinataer', 'konto')
-
+ aktiv=True, naechste_generierung__lte=cutoff_date
+ ).select_related("destinataer", "konto")
+
generated_count = 0
error_count = 0
-
+
for template in templates:
try:
if dry_run:
self.stdout.write(
- f'Would generate: {template.destinataer.get_full_name()} - '
+ f"Would generate: {template.destinataer.get_full_name()} - "
f'€{template.betrag} due {template.naechste_generierung.strftime("%d.%m.%Y")}'
)
generated_count += 1
@@ -66,68 +70,67 @@ class Command(BaseCommand):
if neue_zahlung:
self.stdout.write(
self.style.SUCCESS(
- f'Generated: {neue_zahlung.destinataer.get_full_name()} - '
+ f"Generated: {neue_zahlung.destinataer.get_full_name()} - "
f'€{neue_zahlung.betrag} due {neue_zahlung.faellig_am.strftime("%d.%m.%Y")}'
)
)
generated_count += 1
- logger.info(f'Generated recurring payment: {neue_zahlung.pk}')
+ logger.info(f"Generated recurring payment: {neue_zahlung.pk}")
else:
self.stdout.write(
self.style.WARNING(
- f'No payment generated for {template.destinataer.get_full_name()} '
- f'(may have reached end date or not yet due)'
+ f"No payment generated for {template.destinataer.get_full_name()} "
+ f"(may have reached end date or not yet due)"
)
)
except Exception as e:
error_count += 1
self.stdout.write(
self.style.ERROR(
- f'Error generating payment for {template.destinataer.get_full_name()}: {str(e)}'
+ f"Error generating payment for {template.destinataer.get_full_name()}: {str(e)}"
)
)
- logger.error(f'Error generating recurring payment for template {template.pk}: {str(e)}')
-
+ logger.error(
+ f"Error generating recurring payment for template {template.pk}: {str(e)}"
+ )
+
# Summary
- self.stdout.write('\n' + '='*50)
+ self.stdout.write("\n" + "=" * 50)
if dry_run:
self.stdout.write(
self.style.SUCCESS(
- f'DRY RUN COMPLETE: {generated_count} payments would be generated'
+ f"DRY RUN COMPLETE: {generated_count} payments would be generated"
)
)
else:
self.stdout.write(
self.style.SUCCESS(
- f'GENERATION COMPLETE: {generated_count} payments generated'
+ f"GENERATION COMPLETE: {generated_count} payments generated"
)
)
-
+
if error_count > 0:
- self.stdout.write(
- self.style.ERROR(f'{error_count} errors encountered')
- )
-
+ self.stdout.write(self.style.ERROR(f"{error_count} errors encountered"))
+
# Also check for overdue payments and report them
from stiftung.models import DestinataerUnterstuetzung
-
+
overdue_payments = DestinataerUnterstuetzung.objects.filter(
- faellig_am__lt=heute,
- status__in=['geplant', 'faellig']
- ).select_related('destinataer')
-
+ faellig_am__lt=heute, status__in=["geplant", "faellig"]
+ ).select_related("destinataer")
+
if overdue_payments.exists():
- self.stdout.write('\n' + '='*50)
+ self.stdout.write("\n" + "=" * 50)
self.stdout.write(
self.style.WARNING(
- f'WARNING: {overdue_payments.count()} overdue payments found:'
+ f"WARNING: {overdue_payments.count()} overdue payments found:"
)
)
for payment in overdue_payments[:10]: # Limit to first 10
days_overdue = (heute - payment.faellig_am).days
self.stdout.write(
- f' - {payment.destinataer.get_full_name()}: €{payment.betrag} '
- f'({days_overdue} days overdue)'
+ f" - {payment.destinataer.get_full_name()}: €{payment.betrag} "
+ f"({days_overdue} days overdue)"
)
if overdue_payments.count() > 10:
- self.stdout.write(f' ... and {overdue_payments.count() - 10} more')
+ self.stdout.write(f" ... and {overdue_payments.count() - 10} more")
diff --git a/app/stiftung/management/commands/init_config.py b/app/stiftung/management/commands/init_config.py
index c9b233b..5af320a 100644
--- a/app/stiftung/management/commands/init_config.py
+++ b/app/stiftung/management/commands/init_config.py
@@ -1,93 +1,94 @@
from django.core.management.base import BaseCommand
+
from stiftung.models import AppConfiguration
class Command(BaseCommand):
- help = 'Initialize default app configuration settings'
+ help = "Initialize default app configuration settings"
def handle(self, *args, **options):
# Paperless Integration Settings
paperless_settings = [
{
- 'key': 'paperless_api_url',
- 'display_name': 'Paperless API URL',
- 'description': 'The base URL for your Paperless-NGX API (e.g., http://paperless.example.com:8000)',
- 'value': 'http://192.168.178.167:30070',
- 'default_value': 'http://192.168.178.167:30070',
- 'setting_type': 'url',
- 'category': 'paperless',
- 'order': 1
+ "key": "paperless_api_url",
+ "display_name": "Paperless API URL",
+ "description": "The base URL for your Paperless-NGX API (e.g., http://paperless.example.com:8000)",
+ "value": "http://192.168.178.167:30070",
+ "default_value": "http://192.168.178.167:30070",
+ "setting_type": "url",
+ "category": "paperless",
+ "order": 1,
},
{
- 'key': 'paperless_api_token',
- 'display_name': 'Paperless API Token',
- 'description': 'The authentication token for Paperless API access',
- 'value': '',
- 'default_value': '',
- 'setting_type': 'text',
- 'category': 'paperless',
- 'order': 2
+ "key": "paperless_api_token",
+ "display_name": "Paperless API Token",
+ "description": "The authentication token for Paperless API access",
+ "value": "",
+ "default_value": "",
+ "setting_type": "text",
+ "category": "paperless",
+ "order": 2,
},
{
- 'key': 'paperless_destinataere_tag',
- 'display_name': 'Destinatäre Tag Name',
- 'description': 'The tag name used to identify Destinatäre documents in Paperless',
- 'value': 'Stiftung_Destinatäre',
- 'default_value': 'Stiftung_Destinatäre',
- 'setting_type': 'tag',
- 'category': 'paperless',
- 'order': 3
+ "key": "paperless_destinataere_tag",
+ "display_name": "Destinatäre Tag Name",
+ "description": "The tag name used to identify Destinatäre documents in Paperless",
+ "value": "Stiftung_Destinatäre",
+ "default_value": "Stiftung_Destinatäre",
+ "setting_type": "tag",
+ "category": "paperless",
+ "order": 3,
},
{
- 'key': 'paperless_destinataere_tag_id',
- 'display_name': 'Destinatäre Tag ID',
- 'description': 'The numeric ID of the Destinatäre tag in Paperless',
- 'value': '210',
- 'default_value': '210',
- 'setting_type': 'tag_id',
- 'category': 'paperless',
- 'order': 4
+ "key": "paperless_destinataere_tag_id",
+ "display_name": "Destinatäre Tag ID",
+ "description": "The numeric ID of the Destinatäre tag in Paperless",
+ "value": "210",
+ "default_value": "210",
+ "setting_type": "tag_id",
+ "category": "paperless",
+ "order": 4,
},
{
- 'key': 'paperless_land_tag',
- 'display_name': 'Land & Pächter Tag Name',
- 'description': 'The tag name used to identify Land and Pächter documents in Paperless',
- 'value': 'Stiftung_Land_und_Pächter',
- 'default_value': 'Stiftung_Land_und_Pächter',
- 'setting_type': 'tag',
- 'category': 'paperless',
- 'order': 5
+ "key": "paperless_land_tag",
+ "display_name": "Land & Pächter Tag Name",
+ "description": "The tag name used to identify Land and Pächter documents in Paperless",
+ "value": "Stiftung_Land_und_Pächter",
+ "default_value": "Stiftung_Land_und_Pächter",
+ "setting_type": "tag",
+ "category": "paperless",
+ "order": 5,
},
{
- 'key': 'paperless_land_tag_id',
- 'display_name': 'Land & Pächter Tag ID',
- 'description': 'The numeric ID of the Land & Pächter tag in Paperless',
- 'value': '204',
- 'default_value': '204',
- 'setting_type': 'tag_id',
- 'category': 'paperless',
- 'order': 6
+ "key": "paperless_land_tag_id",
+ "display_name": "Land & Pächter Tag ID",
+ "description": "The numeric ID of the Land & Pächter tag in Paperless",
+ "value": "204",
+ "default_value": "204",
+ "setting_type": "tag_id",
+ "category": "paperless",
+ "order": 6,
},
{
- 'key': 'paperless_admin_tag',
- 'display_name': 'Administration Tag Name',
- 'description': 'The tag name used to identify Administration documents in Paperless',
- 'value': 'Stiftung_Administration',
- 'default_value': 'Stiftung_Administration',
- 'setting_type': 'tag',
- 'category': 'paperless',
- 'order': 7
+ "key": "paperless_admin_tag",
+ "display_name": "Administration Tag Name",
+ "description": "The tag name used to identify Administration documents in Paperless",
+ "value": "Stiftung_Administration",
+ "default_value": "Stiftung_Administration",
+ "setting_type": "tag",
+ "category": "paperless",
+ "order": 7,
},
{
- 'key': 'paperless_admin_tag_id',
- 'display_name': 'Administration Tag ID',
- 'description': 'The numeric ID of the Administration tag in Paperless',
- 'value': '216',
- 'default_value': '216',
- 'setting_type': 'tag_id',
- 'category': 'paperless',
- 'order': 8
- }
+ "key": "paperless_admin_tag_id",
+ "display_name": "Administration Tag ID",
+ "description": "The numeric ID of the Administration tag in Paperless",
+ "value": "216",
+ "default_value": "216",
+ "setting_type": "tag_id",
+ "category": "paperless",
+ "order": 8,
+ },
]
created_count = 0
@@ -95,26 +96,25 @@ class Command(BaseCommand):
for setting_data in paperless_settings:
setting, created = AppConfiguration.objects.get_or_create(
- key=setting_data['key'],
- defaults=setting_data
+ key=setting_data["key"], defaults=setting_data
)
-
+
if created:
created_count += 1
self.stdout.write(
- self.style.SUCCESS(f'Created setting: {setting.display_name}')
+ self.style.SUCCESS(f"Created setting: {setting.display_name}")
)
else:
# Update existing setting with new defaults if needed
if not setting.description:
- setting.description = setting_data['description']
+ setting.description = setting_data["description"]
setting.save()
updated_count += 1
self.stdout.write(
self.style.SUCCESS(
- f'Configuration initialized successfully! '
- f'Created {created_count} new settings, updated {updated_count} existing settings.'
+ f"Configuration initialized successfully! "
+ f"Created {created_count} new settings, updated {updated_count} existing settings."
)
)
self.stdout.write(
diff --git a/app/stiftung/management/commands/init_corporate_settings.py b/app/stiftung/management/commands/init_corporate_settings.py
index e32d60c..493ab97 100644
--- a/app/stiftung/management/commands/init_corporate_settings.py
+++ b/app/stiftung/management/commands/init_corporate_settings.py
@@ -1,114 +1,116 @@
"""
Management command to initialize corporate identity settings
"""
+
from django.core.management.base import BaseCommand
+
from stiftung.models import AppConfiguration
class Command(BaseCommand):
- help = 'Initialize corporate identity settings for PDF generation'
+ help = "Initialize corporate identity settings for PDF generation"
def handle(self, *args, **options):
corporate_settings = [
{
- 'key': 'corporate_stiftung_name',
- 'display_name': 'Name der Stiftung',
- 'description': 'Der offizielle Name der Stiftung für PDF-Dokumente',
- 'value': 'Stiftung',
- 'default_value': 'Stiftung',
- 'setting_type': 'text',
- 'category': 'corporate',
- 'order': 1
+ "key": "corporate_stiftung_name",
+ "display_name": "Name der Stiftung",
+ "description": "Der offizielle Name der Stiftung für PDF-Dokumente",
+ "value": "Stiftung",
+ "default_value": "Stiftung",
+ "setting_type": "text",
+ "category": "corporate",
+ "order": 1,
},
{
- 'key': 'corporate_logo_path',
- 'display_name': 'Logo-Pfad',
- 'description': 'Pfad zur Logo-Datei (relativ zu MEDIA_ROOT oder STATIC_ROOT)',
- 'value': '',
- 'default_value': '',
- 'setting_type': 'text',
- 'category': 'corporate',
- 'order': 2
+ "key": "corporate_logo_path",
+ "display_name": "Logo-Pfad",
+ "description": "Pfad zur Logo-Datei (relativ zu MEDIA_ROOT oder STATIC_ROOT)",
+ "value": "",
+ "default_value": "",
+ "setting_type": "text",
+ "category": "corporate",
+ "order": 2,
},
{
- 'key': 'corporate_primary_color',
- 'display_name': 'Primärfarbe',
- 'description': 'Hauptfarbe für Überschriften und Akzente (Hex-Code)',
- 'value': '#2c3e50',
- 'default_value': '#2c3e50',
- 'setting_type': 'text',
- 'category': 'corporate',
- 'order': 3
+ "key": "corporate_primary_color",
+ "display_name": "Primärfarbe",
+ "description": "Hauptfarbe für Überschriften und Akzente (Hex-Code)",
+ "value": "#2c3e50",
+ "default_value": "#2c3e50",
+ "setting_type": "text",
+ "category": "corporate",
+ "order": 3,
},
{
- 'key': 'corporate_secondary_color',
- 'display_name': 'Sekundärfarbe',
- 'description': 'Zweitfarbe für Akzente und Details (Hex-Code)',
- 'value': '#3498db',
- 'default_value': '#3498db',
- 'setting_type': 'text',
- 'category': 'corporate',
- 'order': 4
+ "key": "corporate_secondary_color",
+ "display_name": "Sekundärfarbe",
+ "description": "Zweitfarbe für Akzente und Details (Hex-Code)",
+ "value": "#3498db",
+ "default_value": "#3498db",
+ "setting_type": "text",
+ "category": "corporate",
+ "order": 4,
},
{
- 'key': 'corporate_address_line1',
- 'display_name': 'Adresse Zeile 1',
- 'description': 'Erste Zeile der Stiftungsadresse',
- 'value': '',
- 'default_value': '',
- 'setting_type': 'text',
- 'category': 'corporate',
- 'order': 5
+ "key": "corporate_address_line1",
+ "display_name": "Adresse Zeile 1",
+ "description": "Erste Zeile der Stiftungsadresse",
+ "value": "",
+ "default_value": "",
+ "setting_type": "text",
+ "category": "corporate",
+ "order": 5,
},
{
- 'key': 'corporate_address_line2',
- 'display_name': 'Adresse Zeile 2',
- 'description': 'Zweite Zeile der Stiftungsadresse (PLZ, Ort)',
- 'value': '',
- 'default_value': '',
- 'setting_type': 'text',
- 'category': 'corporate',
- 'order': 6
+ "key": "corporate_address_line2",
+ "display_name": "Adresse Zeile 2",
+ "description": "Zweite Zeile der Stiftungsadresse (PLZ, Ort)",
+ "value": "",
+ "default_value": "",
+ "setting_type": "text",
+ "category": "corporate",
+ "order": 6,
},
{
- 'key': 'corporate_phone',
- 'display_name': 'Telefonnummer',
- 'description': 'Telefonnummer der Stiftung',
- 'value': '',
- 'default_value': '',
- 'setting_type': 'text',
- 'category': 'corporate',
- 'order': 7
+ "key": "corporate_phone",
+ "display_name": "Telefonnummer",
+ "description": "Telefonnummer der Stiftung",
+ "value": "",
+ "default_value": "",
+ "setting_type": "text",
+ "category": "corporate",
+ "order": 7,
},
{
- 'key': 'corporate_email',
- 'display_name': 'E-Mail-Adresse',
- 'description': 'Offizielle E-Mail-Adresse der Stiftung',
- 'value': '',
- 'default_value': '',
- 'setting_type': 'text',
- 'category': 'corporate',
- 'order': 8
+ "key": "corporate_email",
+ "display_name": "E-Mail-Adresse",
+ "description": "Offizielle E-Mail-Adresse der Stiftung",
+ "value": "",
+ "default_value": "",
+ "setting_type": "text",
+ "category": "corporate",
+ "order": 8,
},
{
- 'key': 'corporate_website',
- 'display_name': 'Website',
- 'description': 'Website der Stiftung',
- 'value': '',
- 'default_value': '',
- 'setting_type': 'url',
- 'category': 'corporate',
- 'order': 9
+ "key": "corporate_website",
+ "display_name": "Website",
+ "description": "Website der Stiftung",
+ "value": "",
+ "default_value": "",
+ "setting_type": "url",
+ "category": "corporate",
+ "order": 9,
},
{
- 'key': 'corporate_footer_text',
- 'display_name': 'Fußzeilen-Text',
- 'description': 'Text für die Fußzeile in PDF-Dokumenten',
- 'value': 'Dieser Bericht wurde automatisch generiert.',
- 'default_value': 'Dieser Bericht wurde automatisch generiert.',
- 'setting_type': 'text',
- 'category': 'corporate',
- 'order': 10
+ "key": "corporate_footer_text",
+ "display_name": "Fußzeilen-Text",
+ "description": "Text für die Fußzeile in PDF-Dokumenten",
+ "value": "Dieser Bericht wurde automatisch generiert.",
+ "default_value": "Dieser Bericht wurde automatisch generiert.",
+ "setting_type": "text",
+ "category": "corporate",
+ "order": 10,
},
]
@@ -117,33 +119,32 @@ class Command(BaseCommand):
for setting_data in corporate_settings:
setting, created = AppConfiguration.objects.get_or_create(
- key=setting_data['key'],
- defaults=setting_data
+ key=setting_data["key"], defaults=setting_data
)
-
+
if created:
created_count += 1
self.stdout.write(
- self.style.SUCCESS(f'Created setting: {setting.display_name}')
+ self.style.SUCCESS(f"Created setting: {setting.display_name}")
)
else:
# Update existing setting with new defaults if needed
if not setting.description:
- setting.description = setting_data['description']
+ setting.description = setting_data["description"]
setting.save()
updated_count += 1
self.stdout.write(
self.style.SUCCESS(
- f'Corporate identity settings initialized! '
- f'Created {created_count} new settings, updated {updated_count} existing settings.'
+ f"Corporate identity settings initialized! "
+ f"Created {created_count} new settings, updated {updated_count} existing settings."
)
)
-
+
if created_count > 0:
self.stdout.write(
self.style.WARNING(
- 'Please configure your corporate identity settings in '
- 'Administration -> Application Settings before generating PDFs.'
+ "Please configure your corporate identity settings in "
+ "Administration -> Application Settings before generating PDFs."
)
)
diff --git a/app/stiftung/management/commands/migrate_verpachtungen.py b/app/stiftung/management/commands/migrate_verpachtungen.py
index d42cfd5..b3c59cf 100644
--- a/app/stiftung/management/commands/migrate_verpachtungen.py
+++ b/app/stiftung/management/commands/migrate_verpachtungen.py
@@ -1,84 +1,93 @@
+import logging
+
from django.core.management.base import BaseCommand
from django.db import transaction
-from stiftung.models import Land, Verpachtung, Paechter
-import logging
+
+from stiftung.models import Land, Paechter, Verpachtung
logger = logging.getLogger(__name__)
+
class Command(BaseCommand):
- help = 'Migriert bestehende Verpachtungen in die neue Land-Struktur'
+ help = "Migriert bestehende Verpachtungen in die neue Land-Struktur"
def add_arguments(self, parser):
parser.add_argument(
- '--dry-run',
- action='store_true',
- help='Zeigt nur an, was gemacht würde, ohne Änderungen zu speichern',
+ "--dry-run",
+ action="store_true",
+ help="Zeigt nur an, was gemacht würde, ohne Änderungen zu speichern",
)
def handle(self, *args, **options):
- dry_run = options['dry_run']
-
+ dry_run = options["dry_run"]
+
if dry_run:
- self.stdout.write(self.style.WARNING('DRY RUN - Keine Änderungen werden gespeichert!'))
-
+ self.stdout.write(
+ self.style.WARNING("DRY RUN - Keine Änderungen werden gespeichert!")
+ )
+
# Alle aktiven Verpachtungen finden
- aktive_verpachtungen = Verpachtung.objects.filter(status='aktiv')
-
- self.stdout.write(f'Gefunden: {aktive_verpachtungen.count()} aktive Verpachtungen')
-
+ aktive_verpachtungen = Verpachtung.objects.filter(status="aktiv")
+
+ self.stdout.write(
+ f"Gefunden: {aktive_verpachtungen.count()} aktive Verpachtungen"
+ )
+
migrated_count = 0
skipped_count = 0
-
+
with transaction.atomic():
for verpachtung in aktive_verpachtungen:
land = verpachtung.land
-
+
# Prüfen ob bereits migriert
if land.aktueller_paechter is not None:
self.stdout.write(
self.style.WARNING(
- f'Übersprungen: {land} hat bereits einen aktuellen Pächter'
+ f"Übersprungen: {land} hat bereits einen aktuellen Pächter"
)
)
skipped_count += 1
continue
-
+
# Migration durchführen
- self.stdout.write(f'Migriere: {land} -> {verpachtung.paechter}')
-
+ self.stdout.write(f"Migriere: {land} -> {verpachtung.paechter}")
+
if not dry_run:
# Pächter-Daten ins Land übertragen
land.aktueller_paechter = verpachtung.paechter
land.paechter_name = verpachtung.paechter.get_full_name()
- land.paechter_anschrift = self._get_paechter_anschrift(verpachtung.paechter)
+ land.paechter_anschrift = self._get_paechter_anschrift(
+ verpachtung.paechter
+ )
land.pachtbeginn = verpachtung.pachtbeginn
land.pachtende = verpachtung.pachtende
land.verlaengerung_klausel = bool(verpachtung.verlaengerung)
-
+
# Pachtzins übertragen
land.pachtzins_pauschal = verpachtung.pachtzins_jaehrlich
-
+
# Verpachtete Fläche aktualisieren (falls nicht gesetzt)
if land.verp_flaeche_aktuell == 0:
land.verp_flaeche_aktuell = verpachtung.verpachtete_flaeche
-
+
land.save()
-
+
migrated_count += 1
-
+
if dry_run:
self.stdout.write(
self.style.SUCCESS(
- f'DRY RUN abgeschlossen: {migrated_count} Verpachtungen würden migriert, {skipped_count} übersprungen'
+ f"DRY RUN abgeschlossen: {migrated_count} Verpachtungen würden migriert, {skipped_count} übersprungen"
)
)
else:
self.stdout.write(
self.style.SUCCESS(
- f'Migration abgeschlossen: {migrated_count} Verpachtungen migriert, {skipped_count} übersprungen'
+ f"Migration abgeschlossen: {migrated_count} Verpachtungen migriert, {skipped_count} übersprungen"
)
)
-
+
def _get_paechter_anschrift(self, paechter):
"""Erstellt eine Anschrift aus den Pächter-Daten"""
parts = []
@@ -88,5 +97,5 @@ class Command(BaseCommand):
parts.append(f"{paechter.plz} {paechter.ort}")
elif paechter.ort:
parts.append(paechter.ort)
-
- return '\n'.join(parts) if parts else ''
+
+ return "\n".join(parts) if parts else ""
diff --git a/app/stiftung/management/commands/sync_abrechnungen.py b/app/stiftung/management/commands/sync_abrechnungen.py
index 8af3c9f..28025f0 100644
--- a/app/stiftung/management/commands/sync_abrechnungen.py
+++ b/app/stiftung/management/commands/sync_abrechnungen.py
@@ -12,110 +12,116 @@ Usage:
python manage.py sync_abrechnungen [--dry-run] [--year YEAR]
"""
+from datetime import date
+from decimal import Decimal
+
from django.core.management.base import BaseCommand, CommandError
from django.db import transaction
-from decimal import Decimal
-from datetime import date
-from stiftung.models import Verpachtung, LandVerpachtung, LandAbrechnung
+
+from stiftung.models import LandAbrechnung, LandVerpachtung, Verpachtung
class Command(BaseCommand):
- help = 'Synchronize existing Verpachtungen with LandAbrechnungen'
+ help = "Synchronize existing Verpachtungen with LandAbrechnungen"
def add_arguments(self, parser):
parser.add_argument(
- '--dry-run',
- action='store_true',
- help='Show what would be done without making changes',
+ "--dry-run",
+ action="store_true",
+ help="Show what would be done without making changes",
)
parser.add_argument(
- '--year',
+ "--year",
type=int,
- help='Only sync data for specific year',
+ help="Only sync data for specific year",
)
parser.add_argument(
- '--force',
- action='store_true',
- help='Force update even if Abrechnungen already exist',
+ "--force",
+ action="store_true",
+ help="Force update even if Abrechnungen already exist",
)
def handle(self, *args, **options):
- dry_run = options['dry_run']
- target_year = options['year']
- force = options['force']
-
+ dry_run = options["dry_run"]
+ target_year = options["year"]
+ force = options["force"]
+
self.stdout.write(
- self.style.SUCCESS('🔄 Starting Abrechnung synchronization...')
+ self.style.SUCCESS("🔄 Starting Abrechnung synchronization...")
)
-
+
if dry_run:
- self.stdout.write(self.style.WARNING('📋 DRY RUN MODE - No changes will be made'))
-
+ self.stdout.write(
+ self.style.WARNING("📋 DRY RUN MODE - No changes will be made")
+ )
+
# Statistics
stats = {
- 'legacy_contracts': 0,
- 'new_contracts': 0,
- 'abrechnungen_created': 0,
- 'abrechnungen_updated': 0,
- 'total_rent_amount': Decimal('0.00'),
- 'years_processed': set(),
+ "legacy_contracts": 0,
+ "new_contracts": 0,
+ "abrechnungen_created": 0,
+ "abrechnungen_updated": 0,
+ "total_rent_amount": Decimal("0.00"),
+ "years_processed": set(),
}
-
+
try:
with transaction.atomic():
# Process Legacy Verpachtungen
- self.stdout.write('\n📄 Processing Legacy Verpachtungen...')
+ self.stdout.write("\n📄 Processing Legacy Verpachtungen...")
legacy_verpachtungen = Verpachtung.objects.all()
-
+
for verpachtung in legacy_verpachtungen:
- stats['legacy_contracts'] += 1
+ stats["legacy_contracts"] += 1
years_affected = self._get_affected_years(
verpachtung.pachtbeginn,
verpachtung.verlaengerung or verpachtung.pachtende,
- target_year
+ target_year,
)
-
+
for year in years_affected:
- stats['years_processed'].add(year)
- rent_amount = self._calculate_legacy_rent_for_year(verpachtung, year)
-
+ stats["years_processed"].add(year)
+ rent_amount = self._calculate_legacy_rent_for_year(
+ verpachtung, year
+ )
+
if not dry_run:
created, updated = self._update_abrechnung(
verpachtung.land,
year,
rent_amount,
- Decimal('0.00'), # No umlage for legacy
+ Decimal("0.00"), # No umlage for legacy
f"Legacy-Verpachtung {verpachtung.vertragsnummer}",
- force
+ force,
)
if created:
- stats['abrechnungen_created'] += 1
+ stats["abrechnungen_created"] += 1
if updated:
- stats['abrechnungen_updated'] += 1
-
- stats['total_rent_amount'] += rent_amount
-
+ stats["abrechnungen_updated"] += 1
+
+ stats["total_rent_amount"] += rent_amount
+
self.stdout.write(
f" 📊 {verpachtung.vertragsnummer} ({year}): {rent_amount:.2f}€"
)
-
- # Process New LandVerpachtungen
- self.stdout.write('\n🆕 Processing New LandVerpachtungen...')
+
+ # Process New LandVerpachtungen
+ self.stdout.write("\n🆕 Processing New LandVerpachtungen...")
land_verpachtungen = LandVerpachtung.objects.all()
-
+
for verpachtung in land_verpachtungen:
- stats['new_contracts'] += 1
+ stats["new_contracts"] += 1
years_affected = self._get_affected_years(
- verpachtung.pachtbeginn,
- verpachtung.pachtende,
- target_year
+ verpachtung.pachtbeginn, verpachtung.pachtende, target_year
)
-
+
for year in years_affected:
- stats['years_processed'].add(year)
- rent_amount = self._calculate_new_rent_for_year(verpachtung, year)
- umlage_amount = Decimal('0.00') # To be calculated later
-
+ stats["years_processed"].add(year)
+ rent_amount = self._calculate_new_rent_for_year(
+ verpachtung, year
+ )
+ umlage_amount = Decimal("0.00") # To be calculated later
+
if not dry_run:
created, updated = self._update_abrechnung(
verpachtung.land,
@@ -123,131 +129,143 @@ class Command(BaseCommand):
rent_amount,
umlage_amount,
f"LandVerpachtung {verpachtung.vertragsnummer}",
- force
+ force,
)
if created:
- stats['abrechnungen_created'] += 1
+ stats["abrechnungen_created"] += 1
if updated:
- stats['abrechnungen_updated'] += 1
-
- stats['total_rent_amount'] += rent_amount
-
+ stats["abrechnungen_updated"] += 1
+
+ stats["total_rent_amount"] += rent_amount
+
self.stdout.write(
f" 📊 {verpachtung.vertragsnummer} ({year}): {rent_amount:.2f}€"
)
-
+
if dry_run:
# Rollback transaction in dry run
transaction.set_rollback(True)
-
+
except Exception as e:
self.stdout.write(
- self.style.ERROR(f'❌ Error during synchronization: {str(e)}')
+ self.style.ERROR(f"❌ Error during synchronization: {str(e)}")
)
- raise CommandError(f'Synchronization failed: {str(e)}')
-
+ raise CommandError(f"Synchronization failed: {str(e)}")
+
# Print summary
- self.stdout.write('\n' + '='*50)
- self.stdout.write(self.style.SUCCESS('📈 SYNCHRONIZATION SUMMARY'))
- self.stdout.write('='*50)
+ self.stdout.write("\n" + "=" * 50)
+ self.stdout.write(self.style.SUCCESS("📈 SYNCHRONIZATION SUMMARY"))
+ self.stdout.write("=" * 50)
self.stdout.write(f"Legacy contracts processed: {stats['legacy_contracts']}")
self.stdout.write(f"New contracts processed: {stats['new_contracts']}")
- self.stdout.write(f"Years affected: {', '.join(map(str, sorted(stats['years_processed'])))}")
+ self.stdout.write(
+ f"Years affected: {', '.join(map(str, sorted(stats['years_processed'])))}"
+ )
self.stdout.write(f"Abrechnungen created: {stats['abrechnungen_created']}")
self.stdout.write(f"Abrechnungen updated: {stats['abrechnungen_updated']}")
self.stdout.write(f"Total rent amount: {stats['total_rent_amount']:.2f}€")
-
+
if dry_run:
- self.stdout.write(self.style.WARNING('\n📋 This was a DRY RUN - no changes were saved'))
+ self.stdout.write(
+ self.style.WARNING("\n📋 This was a DRY RUN - no changes were saved")
+ )
else:
- self.stdout.write(self.style.SUCCESS('\n✅ Synchronization completed successfully!'))
+ self.stdout.write(
+ self.style.SUCCESS("\n✅ Synchronization completed successfully!")
+ )
def _get_affected_years(self, start_date, end_date, target_year=None):
"""Get all years affected by a contract"""
if not start_date:
return []
-
+
years = []
start_year = start_date.year
end_year = end_date.year if end_date else date.today().year
-
+
if target_year:
if start_year <= target_year <= end_year:
return [target_year]
else:
return []
-
+
for year in range(start_year, end_year + 1):
years.append(year)
-
+
return years
-
+
def _calculate_legacy_rent_for_year(self, verpachtung, year):
"""Calculate rent for legacy Verpachtung for specific year"""
if not verpachtung.pachtzins_jaehrlich or not verpachtung.pachtbeginn:
- return Decimal('0.00')
-
+ return Decimal("0.00")
+
year_start = date(year, 1, 1)
year_end = date(year, 12, 31)
-
- contract_end_date = verpachtung.verlaengerung if verpachtung.verlaengerung else verpachtung.pachtende
+
+ contract_end_date = (
+ verpachtung.verlaengerung
+ if verpachtung.verlaengerung
+ else verpachtung.pachtende
+ )
contract_start = max(verpachtung.pachtbeginn, year_start)
contract_end = min(contract_end_date or year_end, year_end)
-
+
if contract_start > contract_end:
- return Decimal('0.00')
-
+ return Decimal("0.00")
+
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(verpachtung.pachtzins_jaehrlich)) * proportion
-
+
def _calculate_new_rent_for_year(self, verpachtung, year):
"""Calculate rent for new LandVerpachtung for specific year"""
if not verpachtung.pachtzins_pauschal or not verpachtung.pachtbeginn:
- return Decimal('0.00')
-
+ return Decimal("0.00")
+
year_start = date(year, 1, 1)
year_end = date(year, 12, 31)
-
+
contract_start = max(verpachtung.pachtbeginn, year_start)
contract_end = min(verpachtung.pachtende or year_end, year_end)
-
+
if contract_start > contract_end:
- return Decimal('0.00')
-
+ return Decimal("0.00")
+
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(verpachtung.pachtzins_pauschal)) * proportion
-
- def _update_abrechnung(self, land, year, rent_amount, umlage_amount, source_note, force):
+
+ def _update_abrechnung(
+ self, land, year, rent_amount, umlage_amount, source_note, force
+ ):
"""Update or create Abrechnung for specific land and year"""
abrechnung, created = LandAbrechnung.objects.get_or_create(
land=land,
abrechnungsjahr=year,
defaults={
- 'pacht_vereinnahmt': rent_amount,
- 'umlagen_vereinnahmt': umlage_amount,
- 'bemerkungen': f'[{date.today().strftime("%d.%m.%Y")}] Automatisch synchronisiert von {source_note}'
- }
+ "pacht_vereinnahmt": rent_amount,
+ "umlagen_vereinnahmt": umlage_amount,
+ "bemerkungen": f'[{date.today().strftime("%d.%m.%Y")}] Automatisch synchronisiert von {source_note}',
+ },
)
-
+
updated = False
if not created and force:
# Update existing
abrechnung.pacht_vereinnahmt += rent_amount
abrechnung.umlagen_vereinnahmt += umlage_amount
-
+
sync_note = f'[{date.today().strftime("%d.%m.%Y")}] Resync: +{rent_amount:.2f}€ von {source_note}'
if abrechnung.bemerkungen:
- abrechnung.bemerkungen += f'\n{sync_note}'
+ abrechnung.bemerkungen += f"\n{sync_note}"
else:
abrechnung.bemerkungen = sync_note
-
+
abrechnung.save()
updated = True
-
+
return created, updated
diff --git a/app/stiftung/management/commands/unify_verpachtungen.py b/app/stiftung/management/commands/unify_verpachtungen.py
index eb36baa..95a2a01 100644
--- a/app/stiftung/management/commands/unify_verpachtungen.py
+++ b/app/stiftung/management/commands/unify_verpachtungen.py
@@ -1,111 +1,127 @@
+import logging
+from datetime import datetime
+
from django.core.management.base import BaseCommand
from django.db import transaction
-from stiftung.models import Land, Verpachtung, Paechter, LandAbrechnung
-from datetime import datetime
-import logging
+
+from stiftung.models import Land, LandAbrechnung, Paechter, Verpachtung
logger = logging.getLogger(__name__)
+
class Command(BaseCommand):
- help = 'Vereinheitlicht Verpachtungen, Land und Abrechnungen zu einem konsistenten System'
+ help = "Vereinheitlicht Verpachtungen, Land und Abrechnungen zu einem konsistenten System"
def add_arguments(self, parser):
parser.add_argument(
- '--dry-run',
- action='store_true',
- help='Zeigt nur an, was gemacht würde, ohne Änderungen zu speichern',
+ "--dry-run",
+ action="store_true",
+ help="Zeigt nur an, was gemacht würde, ohne Änderungen zu speichern",
)
parser.add_argument(
- '--create-abrechnungen',
- action='store_true',
- help='Erstellt automatisch Abrechnungen aus Verpachtungsdaten',
+ "--create-abrechnungen",
+ action="store_true",
+ help="Erstellt automatisch Abrechnungen aus Verpachtungsdaten",
)
def handle(self, *args, **options):
- dry_run = options['dry_run']
- create_abrechnungen = options['create_abrechnungen']
-
+ dry_run = options["dry_run"]
+ create_abrechnungen = options["create_abrechnungen"]
+
if dry_run:
- self.stdout.write(self.style.WARNING('DRY RUN - Keine Änderungen werden gespeichert!'))
-
+ self.stdout.write(
+ self.style.WARNING("DRY RUN - Keine Änderungen werden gespeichert!")
+ )
+
# Schritt 1: Alle Verpachtungen analysieren
- alle_verpachtungen = Verpachtung.objects.all().order_by('land', '-pachtbeginn')
- self.stdout.write(f'Gefunden: {alle_verpachtungen.count()} Verpachtungen insgesamt')
-
+ alle_verpachtungen = Verpachtung.objects.all().order_by("land", "-pachtbeginn")
+ self.stdout.write(
+ f"Gefunden: {alle_verpachtungen.count()} Verpachtungen insgesamt"
+ )
+
land_updates = 0
abrechnungen_created = 0
-
+
with transaction.atomic():
current_land = None
-
+
for verpachtung in alle_verpachtungen:
land = verpachtung.land
-
+
# Für jedes Land nur die neueste aktive Verpachtung als "aktuell" setzen
if current_land != land:
current_land = land
-
+
# Prüfen ob dies die neueste aktive Verpachtung ist
- if verpachtung.status == 'aktiv' and not land.aktueller_paechter:
- self.stdout.write(f'Setze aktuelle Verpachtung: {land} -> {verpachtung.paechter}')
-
+ if verpachtung.status == "aktiv" and not land.aktueller_paechter:
+ self.stdout.write(
+ f"Setze aktuelle Verpachtung: {land} -> {verpachtung.paechter}"
+ )
+
if not dry_run:
# Land-Felder aktualisieren
land.aktueller_paechter = verpachtung.paechter
land.paechter_name = verpachtung.paechter.get_full_name()
- land.paechter_anschrift = self._get_paechter_anschrift(verpachtung.paechter)
+ land.paechter_anschrift = self._get_paechter_anschrift(
+ verpachtung.paechter
+ )
land.pachtbeginn = verpachtung.pachtbeginn
land.pachtende = verpachtung.pachtende
land.verlaengerung_klausel = bool(verpachtung.verlaengerung)
land.pachtzins_pauschal = verpachtung.pachtzins_jaehrlich
-
+
# Verpachtete Fläche synchronisieren
land.verp_flaeche_aktuell = verpachtung.verpachtete_flaeche
-
+
land.save()
land_updates += 1
-
+
# Schritt 2: Abrechnungen aus Verpachtungen erstellen (optional)
- if create_abrechnungen and verpachtung.status == 'aktiv':
+ if create_abrechnungen and verpachtung.status == "aktiv":
# Erstelle Abrechnungen für die letzten 3 Jahre
current_year = datetime.now().year
for jahr in range(current_year - 2, current_year + 1):
-
+
# Prüfen ob Abrechnung bereits existiert
existing = LandAbrechnung.objects.filter(
- land=land,
- abrechnungsjahr=jahr
+ land=land, abrechnungsjahr=jahr
).first()
-
+
if not existing:
- self.stdout.write(f'Erstelle Abrechnung: {land} - {jahr}')
-
+ self.stdout.write(f"Erstelle Abrechnung: {land} - {jahr}")
+
if not dry_run:
abrechnung = LandAbrechnung.objects.create(
land=land,
abrechnungsjahr=jahr,
pacht_vereinnahmt=verpachtung.pachtzins_jaehrlich,
- bemerkungen=f'Automatisch erstellt aus Verpachtung {verpachtung.vertragsnummer}'
+ bemerkungen=f"Automatisch erstellt aus Verpachtung {verpachtung.vertragsnummer}",
)
abrechnungen_created += 1
-
+
# Zusammenfassung
- self.stdout.write(self.style.SUCCESS('\n=== MIGRATION ABGESCHLOSSEN ==='))
+ self.stdout.write(self.style.SUCCESS("\n=== MIGRATION ABGESCHLOSSEN ==="))
if dry_run:
- self.stdout.write(f'DRY RUN: {land_updates} Länder würden aktualisiert')
+ self.stdout.write(f"DRY RUN: {land_updates} Länder würden aktualisiert")
if create_abrechnungen:
- self.stdout.write(f'DRY RUN: {abrechnungen_created} Abrechnungen würden erstellt')
+ self.stdout.write(
+ f"DRY RUN: {abrechnungen_created} Abrechnungen würden erstellt"
+ )
else:
- self.stdout.write(f'✓ {land_updates} Länder aktualisiert')
+ self.stdout.write(f"✓ {land_updates} Länder aktualisiert")
if create_abrechnungen:
- self.stdout.write(f'✓ {abrechnungen_created} Abrechnungen erstellt')
-
+ self.stdout.write(f"✓ {abrechnungen_created} Abrechnungen erstellt")
+
# Empfehlungen
- self.stdout.write(self.style.WARNING('\n=== NÄCHSTE SCHRITTE ==='))
- self.stdout.write('1. Prüfen Sie die migrierten Daten in der Weboberfläche')
- self.stdout.write('2. Alte Verpachtungs-Views können als "Legacy" markiert werden')
- self.stdout.write('3. Neue Verpachtungen sollten direkt im Land-Model erstellt werden')
-
+ self.stdout.write(self.style.WARNING("\n=== NÄCHSTE SCHRITTE ==="))
+ self.stdout.write("1. Prüfen Sie die migrierten Daten in der Weboberfläche")
+ self.stdout.write(
+ '2. Alte Verpachtungs-Views können als "Legacy" markiert werden'
+ )
+ self.stdout.write(
+ "3. Neue Verpachtungen sollten direkt im Land-Model erstellt werden"
+ )
+
def _get_paechter_anschrift(self, paechter):
"""Erstellt eine Anschrift aus den Pächter-Daten"""
parts = []
@@ -115,5 +131,5 @@ class Command(BaseCommand):
parts.append(f"{paechter.plz} {paechter.ort}")
elif paechter.ort:
parts.append(paechter.ort)
-
- return '\n'.join(parts) if parts else ''
+
+ return "\n".join(parts) if parts else ""
diff --git a/app/stiftung/middleware.py b/app/stiftung/middleware.py
index 3be3021..c99f033 100644
--- a/app/stiftung/middleware.py
+++ b/app/stiftung/middleware.py
@@ -4,11 +4,13 @@ Automatically tracks all model changes throughout the application
"""
import threading
-from django.utils.deprecation import MiddlewareMixin
+
from django.contrib.auth.signals import user_logged_in, user_logged_out
-from django.db.models.signals import post_save, post_delete, pre_save
+from django.db.models.signals import post_delete, post_save, pre_save
from django.dispatch import receiver
-from stiftung.audit import log_action, track_model_changes, get_client_ip
+from django.utils.deprecation import MiddlewareMixin
+
+from stiftung.audit import get_client_ip, log_action, track_model_changes
# Thread-local storage for request context
_local = threading.local()
@@ -18,54 +20,54 @@ class AuditMiddleware(MiddlewareMixin):
"""
Middleware that sets up request context for audit logging
"""
-
+
def process_request(self, request):
"""Store request in thread-local storage for access in signal handlers"""
_local.request = request
_local.user_changes = {} # Store pre-save state for change tracking
return None
-
+
def process_response(self, request, response):
"""Clean up thread-local storage"""
- if hasattr(_local, 'request'):
- delattr(_local, 'request')
- if hasattr(_local, 'user_changes'):
- delattr(_local, 'user_changes')
+ if hasattr(_local, "request"):
+ delattr(_local, "request")
+ if hasattr(_local, "user_changes"):
+ delattr(_local, "user_changes")
return response
def get_current_request():
"""Get the current request from thread-local storage"""
- return getattr(_local, 'request', None)
+ return getattr(_local, "request", None)
def get_entity_type_from_model(model):
"""Map Django model to audit entity type"""
model_name = model.__name__.lower()
-
+
mapping = {
- 'destinataer': 'destinataer',
- 'land': 'land',
- 'paechter': 'paechter',
- 'verpachtung': 'verpachtung',
- 'foerderung': 'foerderung',
- 'rentmeister': 'rentmeister',
- 'stiftungskonto': 'stiftungskonto',
- 'verwaltungskosten': 'verwaltungskosten',
- 'banktransaction': 'banktransaction',
- 'dokumentlink': 'dokumentlink',
- 'user': 'user',
- 'person': 'destinataer', # Legacy model maps to destinataer
+ "destinataer": "destinataer",
+ "land": "land",
+ "paechter": "paechter",
+ "verpachtung": "verpachtung",
+ "foerderung": "foerderung",
+ "rentmeister": "rentmeister",
+ "stiftungskonto": "stiftungskonto",
+ "verwaltungskosten": "verwaltungskosten",
+ "banktransaction": "banktransaction",
+ "dokumentlink": "dokumentlink",
+ "user": "user",
+ "person": "destinataer", # Legacy model maps to destinataer
}
-
- return mapping.get(model_name, 'unknown')
+
+ return mapping.get(model_name, "unknown")
def get_entity_name(instance):
"""Get a human-readable name for an entity"""
- if hasattr(instance, 'get_full_name') and callable(instance.get_full_name):
+ if hasattr(instance, "get_full_name") and callable(instance.get_full_name):
return instance.get_full_name()
- elif hasattr(instance, '__str__'):
+ elif hasattr(instance, "__str__"):
return str(instance)
else:
return f"{instance.__class__.__name__} #{instance.pk}"
@@ -76,22 +78,22 @@ def get_entity_name(instance):
def store_pre_save_state(sender, instance, **kwargs):
"""Store the pre-save state for change tracking"""
request = get_current_request()
- if not request or not hasattr(request, 'user'):
+ if not request or not hasattr(request, "user"):
return
-
+
# Skip if user is not authenticated
if not request.user.is_authenticated:
return
-
+
# Skip audit log entries themselves to avoid infinite loops
- if sender.__name__ == 'AuditLog':
+ if sender.__name__ == "AuditLog":
return
-
+
# Store the current state if this is an update
if instance.pk:
try:
old_instance = sender.objects.get(pk=instance.pk)
- if not hasattr(_local, 'user_changes'):
+ if not hasattr(_local, "user_changes"):
_local.user_changes = {}
_local.user_changes[instance.pk] = old_instance
except sender.DoesNotExist:
@@ -102,53 +104,53 @@ def store_pre_save_state(sender, instance, **kwargs):
def log_model_save(sender, instance, created, **kwargs):
"""Log model creation and updates"""
request = get_current_request()
- if not request or not hasattr(request, 'user'):
+ if not request or not hasattr(request, "user"):
return
-
+
# Skip if user is not authenticated
if not request.user.is_authenticated:
return
-
+
# Skip audit log entries themselves to avoid infinite loops
- if sender.__name__ == 'AuditLog':
+ if sender.__name__ == "AuditLog":
return
-
+
# Skip certain system models
- if sender.__name__ in ['Session', 'LogEntry', 'ContentType', 'Permission']:
+ if sender.__name__ in ["Session", "LogEntry", "ContentType", "Permission"]:
return
-
+
entity_type = get_entity_type_from_model(sender)
entity_name = get_entity_name(instance)
entity_id = str(instance.pk)
-
+
if created:
# Log creation
description = f"Neue {entity_type.replace('_', ' ').title()} '{entity_name}' wurde erstellt"
log_action(
request=request,
- action='create',
+ action="create",
entity_type=entity_type,
entity_id=entity_id,
entity_name=entity_name,
- description=description
+ description=description,
)
else:
# Log update with changes
changes = {}
- if hasattr(_local, 'user_changes') and instance.pk in _local.user_changes:
+ if hasattr(_local, "user_changes") and instance.pk in _local.user_changes:
old_instance = _local.user_changes[instance.pk]
changes = track_model_changes(old_instance, instance)
-
+
if changes: # Only log if there are actual changes
description = f"{entity_type.replace('_', ' ').title()} '{entity_name}' wurde aktualisiert"
log_action(
request=request,
- action='update',
+ action="update",
entity_type=entity_type,
entity_id=entity_id,
entity_name=entity_name,
description=description,
- changes=changes
+ changes=changes,
)
@@ -156,33 +158,35 @@ def log_model_save(sender, instance, created, **kwargs):
def log_model_delete(sender, instance, **kwargs):
"""Log model deletion"""
request = get_current_request()
- if not request or not hasattr(request, 'user'):
+ if not request or not hasattr(request, "user"):
return
-
+
# Skip if user is not authenticated
if not request.user.is_authenticated:
return
-
+
# Skip audit log entries themselves
- if sender.__name__ == 'AuditLog':
+ if sender.__name__ == "AuditLog":
return
-
+
# Skip certain system models
- if sender.__name__ in ['Session', 'LogEntry', 'ContentType', 'Permission']:
+ if sender.__name__ in ["Session", "LogEntry", "ContentType", "Permission"]:
return
-
+
entity_type = get_entity_type_from_model(sender)
entity_name = get_entity_name(instance)
entity_id = str(instance.pk)
-
- description = f"{entity_type.replace('_', ' ').title()} '{entity_name}' wurde gelöscht"
+
+ description = (
+ f"{entity_type.replace('_', ' ').title()} '{entity_name}' wurde gelöscht"
+ )
log_action(
request=request,
- action='delete',
+ action="delete",
entity_type=entity_type,
entity_id=entity_id,
entity_name=entity_name,
- description=description
+ description=description,
)
@@ -192,11 +196,11 @@ def log_user_login(sender, request, user, **kwargs):
"""Log user login"""
log_action(
request=request,
- action='login',
- entity_type='user',
+ action="login",
+ entity_type="user",
entity_id=str(user.pk),
entity_name=user.username,
- description=f"Benutzer {user.username} hat sich angemeldet"
+ description=f"Benutzer {user.username} hat sich angemeldet",
)
@@ -206,9 +210,9 @@ def log_user_logout(sender, request, user, **kwargs):
if user: # user might be None if session expired
log_action(
request=request,
- action='logout',
- entity_type='user',
+ action="logout",
+ entity_type="user",
entity_id=str(user.pk),
entity_name=user.username,
- description=f"Benutzer {user.username} hat sich abgemeldet"
+ description=f"Benutzer {user.username} hat sich abgemeldet",
)
diff --git a/app/stiftung/migrations/0001_initial.py b/app/stiftung/migrations/0001_initial.py
index cccf69c..b5c06ea 100644
--- a/app/stiftung/migrations/0001_initial.py
+++ b/app/stiftung/migrations/0001_initial.py
@@ -1,7 +1,8 @@
# Generated by Django 5.0.6 on 2025-08-13 20:59
-import django.db.models.deletion
import uuid
+
+import django.db.models.deletion
from django.db import migrations, models
@@ -9,39 +10,76 @@ class Migration(migrations.Migration):
initial = True
- dependencies = [
- ]
+ dependencies = []
operations = [
migrations.CreateModel(
- name='DokumentLink',
+ name="DokumentLink",
fields=[
- ('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)),
- ('paperless_document_id', models.IntegerField()),
- ('kontext', models.CharField(max_length=30)),
- ('titel', models.CharField(max_length=255)),
+ (
+ "id",
+ models.UUIDField(
+ default=uuid.uuid4,
+ editable=False,
+ primary_key=True,
+ serialize=False,
+ ),
+ ),
+ ("paperless_document_id", models.IntegerField()),
+ ("kontext", models.CharField(max_length=30)),
+ ("titel", models.CharField(max_length=255)),
],
),
migrations.CreateModel(
- name='Person',
+ name="Person",
fields=[
- ('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)),
- ('familienzweig', models.CharField(max_length=100)),
- ('vorname', models.CharField(max_length=100)),
- ('nachname', models.CharField(max_length=100)),
- ('geburtsdatum', models.DateField(blank=True, null=True)),
- ('email', models.EmailField(blank=True, max_length=254, null=True)),
- ('iban', models.CharField(blank=True, max_length=34, null=True)),
+ (
+ "id",
+ models.UUIDField(
+ default=uuid.uuid4,
+ editable=False,
+ primary_key=True,
+ serialize=False,
+ ),
+ ),
+ ("familienzweig", models.CharField(max_length=100)),
+ ("vorname", models.CharField(max_length=100)),
+ ("nachname", models.CharField(max_length=100)),
+ ("geburtsdatum", models.DateField(blank=True, null=True)),
+ ("email", models.EmailField(blank=True, max_length=254, null=True)),
+ ("iban", models.CharField(blank=True, max_length=34, null=True)),
],
),
migrations.CreateModel(
- name='Foerderung',
+ name="Foerderung",
fields=[
- ('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)),
- ('jahr', models.IntegerField()),
- ('betrag', models.DecimalField(decimal_places=2, max_digits=12)),
- ('verwendungsnachweis', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='stiftung.dokumentlink')),
- ('person', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='stiftung.person')),
+ (
+ "id",
+ models.UUIDField(
+ default=uuid.uuid4,
+ editable=False,
+ primary_key=True,
+ serialize=False,
+ ),
+ ),
+ ("jahr", models.IntegerField()),
+ ("betrag", models.DecimalField(decimal_places=2, max_digits=12)),
+ (
+ "verwendungsnachweis",
+ models.ForeignKey(
+ blank=True,
+ null=True,
+ on_delete=django.db.models.deletion.SET_NULL,
+ to="stiftung.dokumentlink",
+ ),
+ ),
+ (
+ "person",
+ models.ForeignKey(
+ on_delete=django.db.models.deletion.CASCADE,
+ to="stiftung.person",
+ ),
+ ),
],
),
]
diff --git a/app/stiftung/migrations/0002_alter_dokumentlink_options_alter_foerderung_options_and_more.py b/app/stiftung/migrations/0002_alter_dokumentlink_options_alter_foerderung_options_and_more.py
index ab07d72..ed4ee0e 100644
--- a/app/stiftung/migrations/0002_alter_dokumentlink_options_alter_foerderung_options_and_more.py
+++ b/app/stiftung/migrations/0002_alter_dokumentlink_options_alter_foerderung_options_and_more.py
@@ -9,104 +9,172 @@ from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
- ('stiftung', '0001_initial'),
+ ("stiftung", "0001_initial"),
]
operations = [
migrations.AlterModelOptions(
- name='dokumentlink',
- options={'ordering': ['titel'], 'verbose_name': 'Dokument', 'verbose_name_plural': 'Dokumente'},
+ name="dokumentlink",
+ options={
+ "ordering": ["titel"],
+ "verbose_name": "Dokument",
+ "verbose_name_plural": "Dokumente",
+ },
),
migrations.AlterModelOptions(
- name='foerderung',
- options={'ordering': ['-jahr', '-betrag'], 'verbose_name': 'Förderung', 'verbose_name_plural': 'Förderungen'},
+ name="foerderung",
+ options={
+ "ordering": ["-jahr", "-betrag"],
+ "verbose_name": "Förderung",
+ "verbose_name_plural": "Förderungen",
+ },
),
migrations.AlterModelOptions(
- name='person',
- options={'ordering': ['nachname', 'vorname'], 'verbose_name': 'Person', 'verbose_name_plural': 'Personen'},
+ name="person",
+ options={
+ "ordering": ["nachname", "vorname"],
+ "verbose_name": "Person",
+ "verbose_name_plural": "Personen",
+ },
),
migrations.AddField(
- model_name='dokumentlink',
- name='beschreibung',
+ model_name="dokumentlink",
+ name="beschreibung",
field=models.TextField(blank=True, null=True),
),
migrations.AddField(
- model_name='foerderung',
- name='antragsdatum',
+ model_name="foerderung",
+ name="antragsdatum",
field=models.DateField(default=django.utils.timezone.now),
),
migrations.AddField(
- model_name='foerderung',
- name='bemerkungen',
+ model_name="foerderung",
+ name="bemerkungen",
field=models.TextField(blank=True, null=True),
),
migrations.AddField(
- model_name='foerderung',
- name='entscheidungsdatum',
+ model_name="foerderung",
+ name="entscheidungsdatum",
field=models.DateField(blank=True, null=True),
),
migrations.AddField(
- model_name='foerderung',
- name='kategorie',
- field=models.CharField(choices=[('bildung', 'Bildung'), ('forschung', 'Forschung'), ('kultur', 'Kultur'), ('soziales', 'Soziales'), ('umwelt', 'Umwelt'), ('anderes', 'Anderes')], default='anderes', max_length=20),
+ model_name="foerderung",
+ name="kategorie",
+ field=models.CharField(
+ choices=[
+ ("bildung", "Bildung"),
+ ("forschung", "Forschung"),
+ ("kultur", "Kultur"),
+ ("soziales", "Soziales"),
+ ("umwelt", "Umwelt"),
+ ("anderes", "Anderes"),
+ ],
+ default="anderes",
+ max_length=20,
+ ),
),
migrations.AddField(
- model_name='foerderung',
- name='status',
- field=models.CharField(choices=[('beantragt', 'Beantragt'), ('genehmigt', 'Genehmigt'), ('ausgezahlt', 'Ausgezahlt'), ('abgelehnt', 'Abgelehnt'), ('storniert', 'Storniert')], default='beantragt', max_length=20),
+ model_name="foerderung",
+ name="status",
+ field=models.CharField(
+ choices=[
+ ("beantragt", "Beantragt"),
+ ("genehmigt", "Genehmigt"),
+ ("ausgezahlt", "Ausgezahlt"),
+ ("abgelehnt", "Abgelehnt"),
+ ("storniert", "Storniert"),
+ ],
+ default="beantragt",
+ max_length=20,
+ ),
),
migrations.AddField(
- model_name='person',
- name='adresse',
+ model_name="person",
+ name="adresse",
field=models.TextField(blank=True, null=True),
),
migrations.AddField(
- model_name='person',
- name='aktiv',
+ model_name="person",
+ name="aktiv",
field=models.BooleanField(default=True),
),
migrations.AddField(
- model_name='person',
- name='notizen',
+ model_name="person",
+ name="notizen",
field=models.TextField(blank=True, null=True),
),
migrations.AddField(
- model_name='person',
- name='telefon',
+ model_name="person",
+ name="telefon",
field=models.CharField(blank=True, max_length=20, null=True),
),
migrations.AlterField(
- model_name='dokumentlink',
- name='kontext',
- field=models.CharField(choices=[('antrag', 'Antrag'), ('verwendungsnachweis', 'Verwendungsnachweis'), ('rechnung', 'Rechnung'), ('vertrag', 'Vertrag'), ('bericht', 'Bericht'), ('anderes', 'Anderes')], default='anderes', max_length=30),
+ model_name="dokumentlink",
+ name="kontext",
+ field=models.CharField(
+ choices=[
+ ("antrag", "Antrag"),
+ ("verwendungsnachweis", "Verwendungsnachweis"),
+ ("rechnung", "Rechnung"),
+ ("vertrag", "Vertrag"),
+ ("bericht", "Bericht"),
+ ("anderes", "Anderes"),
+ ],
+ default="anderes",
+ max_length=30,
+ ),
),
migrations.AlterField(
- model_name='dokumentlink',
- name='paperless_document_id',
+ model_name="dokumentlink",
+ name="paperless_document_id",
field=models.IntegerField(unique=True),
),
migrations.AlterField(
- model_name='foerderung',
- name='jahr',
- field=models.IntegerField(validators=[django.core.validators.MinValueValidator(1900), django.core.validators.MaxValueValidator(2100)]),
+ model_name="foerderung",
+ name="jahr",
+ field=models.IntegerField(
+ validators=[
+ django.core.validators.MinValueValidator(1900),
+ django.core.validators.MaxValueValidator(2100),
+ ]
+ ),
),
migrations.AlterField(
- model_name='foerderung',
- name='person',
- field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='stiftung.person', verbose_name='Person'),
+ model_name="foerderung",
+ name="person",
+ field=models.ForeignKey(
+ on_delete=django.db.models.deletion.CASCADE,
+ to="stiftung.person",
+ verbose_name="Person",
+ ),
),
migrations.AlterField(
- model_name='foerderung',
- name='verwendungsnachweis',
- field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='stiftung.dokumentlink', verbose_name='Verwendungsnachweis'),
+ model_name="foerderung",
+ name="verwendungsnachweis",
+ field=models.ForeignKey(
+ blank=True,
+ null=True,
+ on_delete=django.db.models.deletion.SET_NULL,
+ to="stiftung.dokumentlink",
+ verbose_name="Verwendungsnachweis",
+ ),
),
migrations.AlterField(
- model_name='person',
- name='familienzweig',
- field=models.CharField(choices=[('hauptzweig', 'Hauptzweig'), ('nebenzweig', 'Nebenzweig'), ('verwandt', 'Verwandt'), ('anderer', 'Anderer')], default='hauptzweig', max_length=100),
+ model_name="person",
+ name="familienzweig",
+ field=models.CharField(
+ choices=[
+ ("hauptzweig", "Hauptzweig"),
+ ("nebenzweig", "Nebenzweig"),
+ ("verwandt", "Verwandt"),
+ ("anderer", "Anderer"),
+ ],
+ default="hauptzweig",
+ max_length=100,
+ ),
),
migrations.AlterUniqueTogether(
- name='foerderung',
- unique_together={('person', 'jahr', 'kategorie')},
+ name="foerderung",
+ unique_together={("person", "jahr", "kategorie")},
),
]
diff --git a/app/stiftung/migrations/0003_land_alter_dokumentlink_kontext_verpachtung.py b/app/stiftung/migrations/0003_land_alter_dokumentlink_kontext_verpachtung.py
index 3dc7a23..f1cf9e3 100644
--- a/app/stiftung/migrations/0003_land_alter_dokumentlink_kontext_verpachtung.py
+++ b/app/stiftung/migrations/0003_land_alter_dokumentlink_kontext_verpachtung.py
@@ -1,78 +1,293 @@
# Generated by Django 5.0.6 on 2025-08-13 21:43
+import uuid
+
import django.core.validators
import django.db.models.deletion
-import uuid
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
- ('stiftung', '0002_alter_dokumentlink_options_alter_foerderung_options_and_more'),
+ (
+ "stiftung",
+ "0002_alter_dokumentlink_options_alter_foerderung_options_and_more",
+ ),
]
operations = [
migrations.CreateModel(
- name='Land',
+ name="Land",
fields=[
- ('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)),
- ('lfd_nr', models.CharField(max_length=20, unique=True, verbose_name='Lfd. Nr.')),
- ('ew_nummer', models.CharField(blank=True, max_length=50, null=True, verbose_name='EW-Nummer')),
- ('amtsgericht', models.CharField(max_length=100, verbose_name='Amtsgericht')),
- ('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')),
- ('groesse_qm', models.DecimalField(decimal_places=2, max_digits=12, validators=[django.core.validators.MinValueValidator(0.01)], verbose_name='Größe in qm')),
- ('gruenland_qm', models.DecimalField(decimal_places=2, default=0, max_digits=12, validators=[django.core.validators.MinValueValidator(0)], verbose_name='Grünland (qm)')),
- ('acker_qm', models.DecimalField(decimal_places=2, default=0, max_digits=12, validators=[django.core.validators.MinValueValidator(0)], verbose_name='Acker (qm)')),
- ('wald_qm', models.DecimalField(decimal_places=2, default=0, max_digits=12, validators=[django.core.validators.MinValueValidator(0)], verbose_name='Wald (qm)')),
- ('sonstiges_qm', models.DecimalField(decimal_places=2, default=0, max_digits=12, validators=[django.core.validators.MinValueValidator(0)], verbose_name='Sonstiges (qm)')),
- ('verpachtete_gesamtflaeche', models.DecimalField(decimal_places=2, max_digits=12, validators=[django.core.validators.MinValueValidator(0)], verbose_name='Verpachtete Gesamtfläche (qm)')),
- ('flaeche_alte_liste', models.DecimalField(blank=True, decimal_places=2, max_digits=12, null=True, verbose_name='Fläche alte Liste (qm)')),
- ('verp_flaeche_aktuell', models.DecimalField(decimal_places=2, max_digits=12, validators=[django.core.validators.MinValueValidator(0)], verbose_name='Verp. Fläche aktuell (qm)')),
- ('anteil_grundsteuer', models.DecimalField(blank=True, decimal_places=2, max_digits=8, null=True, verbose_name='Anteil Grundsteuer (%)')),
- ('anteil_lwk', models.DecimalField(blank=True, decimal_places=2, max_digits=8, null=True, verbose_name='Anteil LWK (%)')),
- ('aktiv', models.BooleanField(default=True, verbose_name='Aktiv')),
- ('notizen', models.TextField(blank=True, null=True, verbose_name='Ergänzende Kommentare')),
- ('erstellt_am', models.DateTimeField(auto_now_add=True)),
- ('aktualisiert_am', models.DateTimeField(auto_now=True)),
+ (
+ "id",
+ models.UUIDField(
+ default=uuid.uuid4,
+ editable=False,
+ primary_key=True,
+ serialize=False,
+ ),
+ ),
+ (
+ "lfd_nr",
+ models.CharField(
+ max_length=20, unique=True, verbose_name="Lfd. Nr."
+ ),
+ ),
+ (
+ "ew_nummer",
+ models.CharField(
+ blank=True, max_length=50, null=True, verbose_name="EW-Nummer"
+ ),
+ ),
+ (
+ "amtsgericht",
+ models.CharField(max_length=100, verbose_name="Amtsgericht"),
+ ),
+ ("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"),
+ ),
+ (
+ "groesse_qm",
+ models.DecimalField(
+ decimal_places=2,
+ max_digits=12,
+ validators=[django.core.validators.MinValueValidator(0.01)],
+ verbose_name="Größe in qm",
+ ),
+ ),
+ (
+ "gruenland_qm",
+ models.DecimalField(
+ decimal_places=2,
+ default=0,
+ max_digits=12,
+ validators=[django.core.validators.MinValueValidator(0)],
+ verbose_name="Grünland (qm)",
+ ),
+ ),
+ (
+ "acker_qm",
+ models.DecimalField(
+ decimal_places=2,
+ default=0,
+ max_digits=12,
+ validators=[django.core.validators.MinValueValidator(0)],
+ verbose_name="Acker (qm)",
+ ),
+ ),
+ (
+ "wald_qm",
+ models.DecimalField(
+ decimal_places=2,
+ default=0,
+ max_digits=12,
+ validators=[django.core.validators.MinValueValidator(0)],
+ verbose_name="Wald (qm)",
+ ),
+ ),
+ (
+ "sonstiges_qm",
+ models.DecimalField(
+ decimal_places=2,
+ default=0,
+ max_digits=12,
+ validators=[django.core.validators.MinValueValidator(0)],
+ verbose_name="Sonstiges (qm)",
+ ),
+ ),
+ (
+ "verpachtete_gesamtflaeche",
+ models.DecimalField(
+ decimal_places=2,
+ max_digits=12,
+ validators=[django.core.validators.MinValueValidator(0)],
+ verbose_name="Verpachtete Gesamtfläche (qm)",
+ ),
+ ),
+ (
+ "flaeche_alte_liste",
+ models.DecimalField(
+ blank=True,
+ decimal_places=2,
+ max_digits=12,
+ null=True,
+ verbose_name="Fläche alte Liste (qm)",
+ ),
+ ),
+ (
+ "verp_flaeche_aktuell",
+ models.DecimalField(
+ decimal_places=2,
+ max_digits=12,
+ validators=[django.core.validators.MinValueValidator(0)],
+ verbose_name="Verp. Fläche aktuell (qm)",
+ ),
+ ),
+ (
+ "anteil_grundsteuer",
+ models.DecimalField(
+ blank=True,
+ decimal_places=2,
+ max_digits=8,
+ null=True,
+ verbose_name="Anteil Grundsteuer (%)",
+ ),
+ ),
+ (
+ "anteil_lwk",
+ models.DecimalField(
+ blank=True,
+ decimal_places=2,
+ max_digits=8,
+ null=True,
+ verbose_name="Anteil LWK (%)",
+ ),
+ ),
+ ("aktiv", models.BooleanField(default=True, verbose_name="Aktiv")),
+ (
+ "notizen",
+ models.TextField(
+ blank=True, null=True, verbose_name="Ergänzende Kommentare"
+ ),
+ ),
+ ("erstellt_am", models.DateTimeField(auto_now_add=True)),
+ ("aktualisiert_am", models.DateTimeField(auto_now=True)),
],
options={
- 'verbose_name': 'Land',
- 'verbose_name_plural': 'Ländereien',
- 'ordering': ['gemeinde', 'gemarkung', 'flur', 'flurstueck'],
+ "verbose_name": "Land",
+ "verbose_name_plural": "Ländereien",
+ "ordering": ["gemeinde", "gemarkung", "flur", "flurstueck"],
},
),
migrations.AlterField(
- model_name='dokumentlink',
- name='kontext',
- field=models.CharField(choices=[('antrag', 'Antrag'), ('verwendungsnachweis', 'Verwendungsnachweis'), ('rechnung', 'Rechnung'), ('vertrag', 'Vertrag'), ('bericht', 'Bericht'), ('landkarte', 'Landkarte'), ('kataster', 'Kataster'), ('anderes', 'Anderes')], default='anderes', max_length=30),
+ model_name="dokumentlink",
+ name="kontext",
+ field=models.CharField(
+ choices=[
+ ("antrag", "Antrag"),
+ ("verwendungsnachweis", "Verwendungsnachweis"),
+ ("rechnung", "Rechnung"),
+ ("vertrag", "Vertrag"),
+ ("bericht", "Bericht"),
+ ("landkarte", "Landkarte"),
+ ("kataster", "Kataster"),
+ ("anderes", "Anderes"),
+ ],
+ default="anderes",
+ max_length=30,
+ ),
),
migrations.CreateModel(
- name='Verpachtung',
+ name="Verpachtung",
fields=[
- ('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)),
- ('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(blank=True, null=True, verbose_name='Verlängerung bis')),
- ('pachtzins_pro_qm', models.DecimalField(decimal_places=4, max_digits=8, verbose_name='Pachtzins pro qm (€)')),
- ('pachtzins_jaehrlich', models.DecimalField(decimal_places=2, max_digits=12, verbose_name='Jährlicher Pachtzins (€)')),
- ('verpachtete_flaeche', models.DecimalField(decimal_places=2, max_digits=12, verbose_name='Verpachtete Fläche (qm)')),
- ('status', models.CharField(choices=[('aktiv', 'Aktiv'), ('beendet', 'Beendet'), ('gekuendigt', 'Gekündigt'), ('verlängert', 'Verlängert')], default='aktiv', max_length=20)),
- ('bemerkungen', models.TextField(blank=True, null=True, verbose_name='Ergänzende Kommentare')),
- ('erstellt_am', models.DateTimeField(auto_now_add=True)),
- ('aktualisiert_am', models.DateTimeField(auto_now=True)),
- ('land', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='stiftung.land', verbose_name='Land')),
- ('paechter', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='stiftung.person', verbose_name='Pächter')),
- ('verwendungsnachweis', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='stiftung.dokumentlink', verbose_name='Verwendungsnachweis')),
+ (
+ "id",
+ models.UUIDField(
+ default=uuid.uuid4,
+ editable=False,
+ primary_key=True,
+ serialize=False,
+ ),
+ ),
+ (
+ "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(
+ blank=True, null=True, verbose_name="Verlängerung bis"
+ ),
+ ),
+ (
+ "pachtzins_pro_qm",
+ models.DecimalField(
+ decimal_places=4,
+ max_digits=8,
+ verbose_name="Pachtzins pro qm (€)",
+ ),
+ ),
+ (
+ "pachtzins_jaehrlich",
+ models.DecimalField(
+ decimal_places=2,
+ max_digits=12,
+ verbose_name="Jährlicher Pachtzins (€)",
+ ),
+ ),
+ (
+ "verpachtete_flaeche",
+ models.DecimalField(
+ decimal_places=2,
+ max_digits=12,
+ verbose_name="Verpachtete Fläche (qm)",
+ ),
+ ),
+ (
+ "status",
+ models.CharField(
+ choices=[
+ ("aktiv", "Aktiv"),
+ ("beendet", "Beendet"),
+ ("gekuendigt", "Gekündigt"),
+ ("verlängert", "Verlängert"),
+ ],
+ default="aktiv",
+ max_length=20,
+ ),
+ ),
+ (
+ "bemerkungen",
+ models.TextField(
+ blank=True, null=True, verbose_name="Ergänzende Kommentare"
+ ),
+ ),
+ ("erstellt_am", models.DateTimeField(auto_now_add=True)),
+ ("aktualisiert_am", models.DateTimeField(auto_now=True)),
+ (
+ "land",
+ models.ForeignKey(
+ on_delete=django.db.models.deletion.CASCADE,
+ to="stiftung.land",
+ verbose_name="Land",
+ ),
+ ),
+ (
+ "paechter",
+ models.ForeignKey(
+ on_delete=django.db.models.deletion.CASCADE,
+ to="stiftung.person",
+ verbose_name="Pächter",
+ ),
+ ),
+ (
+ "verwendungsnachweis",
+ models.ForeignKey(
+ blank=True,
+ null=True,
+ on_delete=django.db.models.deletion.SET_NULL,
+ to="stiftung.dokumentlink",
+ verbose_name="Verwendungsnachweis",
+ ),
+ ),
],
options={
- 'verbose_name': 'Verpachtung',
- 'verbose_name_plural': 'Verpachtungen',
- 'ordering': ['-pachtbeginn'],
+ "verbose_name": "Verpachtung",
+ "verbose_name_plural": "Verpachtungen",
+ "ordering": ["-pachtbeginn"],
},
),
]
diff --git a/app/stiftung/migrations/0004_csvimport.py b/app/stiftung/migrations/0004_csvimport.py
index 470c44a..a070cc9 100644
--- a/app/stiftung/migrations/0004_csvimport.py
+++ b/app/stiftung/migrations/0004_csvimport.py
@@ -1,36 +1,106 @@
# Generated by Django 5.0.6 on 2025-08-13 22:18
import uuid
+
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
- ('stiftung', '0003_land_alter_dokumentlink_kontext_verpachtung'),
+ ("stiftung", "0003_land_alter_dokumentlink_kontext_verpachtung"),
]
operations = [
migrations.CreateModel(
- name='CSVImport',
+ name="CSVImport",
fields=[
- ('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)),
- ('import_type', models.CharField(choices=[('personen', 'Personen'), ('laendereien', 'Ländereien'), ('verpachtungen', 'Verpachtungen')], max_length=20, 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(choices=[('pending', 'Ausstehend'), ('processing', 'Wird verarbeitet'), ('completed', 'Abgeschlossen'), ('failed', 'Fehlgeschlagen'), ('partial', 'Teilweise erfolgreich')], default='pending', max_length=20)),
- ('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(blank=True, null=True, verbose_name='Fehlerprotokoll')),
- ('created_by', models.CharField(blank=True, max_length=100, null=True, verbose_name='Erstellt von')),
- ('started_at', models.DateTimeField(auto_now_add=True, verbose_name='Gestartet um')),
- ('completed_at', models.DateTimeField(blank=True, null=True, verbose_name='Abgeschlossen um')),
+ (
+ "id",
+ models.UUIDField(
+ default=uuid.uuid4,
+ editable=False,
+ primary_key=True,
+ serialize=False,
+ ),
+ ),
+ (
+ "import_type",
+ models.CharField(
+ choices=[
+ ("personen", "Personen"),
+ ("laendereien", "Ländereien"),
+ ("verpachtungen", "Verpachtungen"),
+ ],
+ max_length=20,
+ 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(
+ choices=[
+ ("pending", "Ausstehend"),
+ ("processing", "Wird verarbeitet"),
+ ("completed", "Abgeschlossen"),
+ ("failed", "Fehlgeschlagen"),
+ ("partial", "Teilweise erfolgreich"),
+ ],
+ default="pending",
+ max_length=20,
+ ),
+ ),
+ (
+ "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(
+ blank=True, null=True, verbose_name="Fehlerprotokoll"
+ ),
+ ),
+ (
+ "created_by",
+ models.CharField(
+ blank=True,
+ max_length=100,
+ null=True,
+ verbose_name="Erstellt von",
+ ),
+ ),
+ (
+ "started_at",
+ models.DateTimeField(
+ auto_now_add=True, verbose_name="Gestartet um"
+ ),
+ ),
+ (
+ "completed_at",
+ models.DateTimeField(
+ blank=True, null=True, verbose_name="Abgeschlossen um"
+ ),
+ ),
],
options={
- 'verbose_name': 'CSV Import',
- 'verbose_name_plural': 'CSV Imports',
- 'ordering': ['-started_at'],
+ "verbose_name": "CSV Import",
+ "verbose_name_plural": "CSV Imports",
+ "ordering": ["-started_at"],
},
),
]
diff --git a/app/stiftung/migrations/0005_destinataer_paechter_alter_person_options_and_more.py b/app/stiftung/migrations/0005_destinataer_paechter_alter_person_options_and_more.py
index a2b2d1b..375d539 100644
--- a/app/stiftung/migrations/0005_destinataer_paechter_alter_person_options_and_more.py
+++ b/app/stiftung/migrations/0005_destinataer_paechter_alter_person_options_and_more.py
@@ -1,93 +1,298 @@
# Generated by Django 5.0.6 on 2025-08-14 10:38
-import django.db.models.deletion
import uuid
+
+import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
- ('stiftung', '0004_csvimport'),
+ ("stiftung", "0004_csvimport"),
]
operations = [
migrations.CreateModel(
- name='Destinataer',
+ name="Destinataer",
fields=[
- ('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)),
- ('familienzweig', models.CharField(choices=[('hauptzweig', 'Hauptzweig'), ('nebenzweig', 'Nebenzweig'), ('verwandt', 'Verwandt'), ('anderer', 'Anderer')], default='hauptzweig', max_length=100)),
- ('vorname', models.CharField(max_length=100, verbose_name='Vorname')),
- ('nachname', models.CharField(max_length=100, verbose_name='Nachname')),
- ('geburtsdatum', models.DateField(blank=True, null=True, verbose_name='Geburtsdatum')),
- ('email', models.EmailField(blank=True, max_length=254, null=True, verbose_name='E-Mail')),
- ('telefon', models.CharField(blank=True, max_length=20, null=True, verbose_name='Telefon')),
- ('iban', models.CharField(blank=True, max_length=34, null=True, verbose_name='IBAN')),
- ('adresse', models.TextField(blank=True, null=True, verbose_name='Adresse')),
- ('berufsgruppe', models.CharField(choices=[('student', 'Student/Studentin'), ('wissenschaftler', 'Wissenschaftler/in'), ('künstler', 'Künstler/in'), ('sozialarbeiter', 'Sozialarbeiter/in'), ('umweltschützer', 'Umweltschützer/in'), ('andere', 'Andere')], default='andere', max_length=20, verbose_name='Berufsgruppe')),
- ('ausbildungsstand', models.CharField(blank=True, max_length=100, null=True, verbose_name='Ausbildungsstand')),
- ('institution', models.CharField(blank=True, max_length=200, null=True, verbose_name='Institution/Organisation')),
- ('projekt_beschreibung', models.TextField(blank=True, null=True, verbose_name='Projektbeschreibung')),
- ('jaehrliches_einkommen', models.DecimalField(blank=True, decimal_places=2, max_digits=12, null=True, verbose_name='Jährliches Einkommen (€)')),
- ('finanzielle_notlage', models.BooleanField(default=False, verbose_name='Finanzielle Notlage')),
- ('notizen', models.TextField(blank=True, null=True, verbose_name='Notizen')),
- ('aktiv', models.BooleanField(default=True, verbose_name='Aktiv')),
+ (
+ "id",
+ models.UUIDField(
+ default=uuid.uuid4,
+ editable=False,
+ primary_key=True,
+ serialize=False,
+ ),
+ ),
+ (
+ "familienzweig",
+ models.CharField(
+ choices=[
+ ("hauptzweig", "Hauptzweig"),
+ ("nebenzweig", "Nebenzweig"),
+ ("verwandt", "Verwandt"),
+ ("anderer", "Anderer"),
+ ],
+ default="hauptzweig",
+ max_length=100,
+ ),
+ ),
+ ("vorname", models.CharField(max_length=100, verbose_name="Vorname")),
+ ("nachname", models.CharField(max_length=100, verbose_name="Nachname")),
+ (
+ "geburtsdatum",
+ models.DateField(
+ blank=True, null=True, verbose_name="Geburtsdatum"
+ ),
+ ),
+ (
+ "email",
+ models.EmailField(
+ blank=True, max_length=254, null=True, verbose_name="E-Mail"
+ ),
+ ),
+ (
+ "telefon",
+ models.CharField(
+ blank=True, max_length=20, null=True, verbose_name="Telefon"
+ ),
+ ),
+ (
+ "iban",
+ models.CharField(
+ blank=True, max_length=34, null=True, verbose_name="IBAN"
+ ),
+ ),
+ (
+ "adresse",
+ models.TextField(blank=True, null=True, verbose_name="Adresse"),
+ ),
+ (
+ "berufsgruppe",
+ models.CharField(
+ choices=[
+ ("student", "Student/Studentin"),
+ ("wissenschaftler", "Wissenschaftler/in"),
+ ("künstler", "Künstler/in"),
+ ("sozialarbeiter", "Sozialarbeiter/in"),
+ ("umweltschützer", "Umweltschützer/in"),
+ ("andere", "Andere"),
+ ],
+ default="andere",
+ max_length=20,
+ verbose_name="Berufsgruppe",
+ ),
+ ),
+ (
+ "ausbildungsstand",
+ models.CharField(
+ blank=True,
+ max_length=100,
+ null=True,
+ verbose_name="Ausbildungsstand",
+ ),
+ ),
+ (
+ "institution",
+ models.CharField(
+ blank=True,
+ max_length=200,
+ null=True,
+ verbose_name="Institution/Organisation",
+ ),
+ ),
+ (
+ "projekt_beschreibung",
+ models.TextField(
+ blank=True, null=True, verbose_name="Projektbeschreibung"
+ ),
+ ),
+ (
+ "jaehrliches_einkommen",
+ models.DecimalField(
+ blank=True,
+ decimal_places=2,
+ max_digits=12,
+ null=True,
+ verbose_name="Jährliches Einkommen (€)",
+ ),
+ ),
+ (
+ "finanzielle_notlage",
+ models.BooleanField(
+ default=False, verbose_name="Finanzielle Notlage"
+ ),
+ ),
+ (
+ "notizen",
+ models.TextField(blank=True, null=True, verbose_name="Notizen"),
+ ),
+ ("aktiv", models.BooleanField(default=True, verbose_name="Aktiv")),
],
options={
- 'verbose_name': 'Destinatär',
- 'verbose_name_plural': 'Destinatäre',
- 'ordering': ['nachname', 'vorname'],
+ "verbose_name": "Destinatär",
+ "verbose_name_plural": "Destinatäre",
+ "ordering": ["nachname", "vorname"],
},
),
migrations.CreateModel(
- name='Paechter',
+ name="Paechter",
fields=[
- ('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)),
- ('familienzweig', models.CharField(choices=[('hauptzweig', 'Hauptzweig'), ('nebenzweig', 'Nebenzweig'), ('verwandt', 'Verwandt'), ('anderer', 'Anderer')], default='hauptzweig', max_length=100)),
- ('vorname', models.CharField(max_length=100, verbose_name='Vorname')),
- ('nachname', models.CharField(max_length=100, verbose_name='Nachname')),
- ('geburtsdatum', models.DateField(blank=True, null=True, verbose_name='Geburtsdatum')),
- ('email', models.EmailField(blank=True, max_length=254, null=True, verbose_name='E-Mail')),
- ('telefon', models.CharField(blank=True, max_length=20, null=True, verbose_name='Telefon')),
- ('iban', models.CharField(blank=True, max_length=34, null=True, verbose_name='IBAN')),
- ('adresse', models.TextField(blank=True, null=True, verbose_name='Adresse')),
- ('pachtnummer', models.CharField(blank=True, max_length=50, null=True, verbose_name='Pachtnummer')),
- ('pachtbeginn_erste', models.DateField(blank=True, null=True, verbose_name='Erster Pachtbeginn')),
- ('pachtende_letzte', models.DateField(blank=True, null=True, verbose_name='Letztes Pachtende')),
- ('pachtzins_aktuell', models.DecimalField(blank=True, decimal_places=2, max_digits=12, null=True, verbose_name='Aktueller Pachtzins (€/Jahr)')),
- ('landwirtschaftliche_ausbildung', models.BooleanField(default=False, verbose_name='Landwirtschaftliche Ausbildung')),
- ('berufserfahrung_jahre', models.IntegerField(blank=True, null=True, verbose_name='Berufserfahrung (Jahre)')),
- ('spezialisierung', models.CharField(blank=True, max_length=100, null=True, verbose_name='Spezialisierung')),
- ('notizen', models.TextField(blank=True, null=True, verbose_name='Notizen')),
- ('aktiv', models.BooleanField(default=True, verbose_name='Aktiv')),
+ (
+ "id",
+ models.UUIDField(
+ default=uuid.uuid4,
+ editable=False,
+ primary_key=True,
+ serialize=False,
+ ),
+ ),
+ (
+ "familienzweig",
+ models.CharField(
+ choices=[
+ ("hauptzweig", "Hauptzweig"),
+ ("nebenzweig", "Nebenzweig"),
+ ("verwandt", "Verwandt"),
+ ("anderer", "Anderer"),
+ ],
+ default="hauptzweig",
+ max_length=100,
+ ),
+ ),
+ ("vorname", models.CharField(max_length=100, verbose_name="Vorname")),
+ ("nachname", models.CharField(max_length=100, verbose_name="Nachname")),
+ (
+ "geburtsdatum",
+ models.DateField(
+ blank=True, null=True, verbose_name="Geburtsdatum"
+ ),
+ ),
+ (
+ "email",
+ models.EmailField(
+ blank=True, max_length=254, null=True, verbose_name="E-Mail"
+ ),
+ ),
+ (
+ "telefon",
+ models.CharField(
+ blank=True, max_length=20, null=True, verbose_name="Telefon"
+ ),
+ ),
+ (
+ "iban",
+ models.CharField(
+ blank=True, max_length=34, null=True, verbose_name="IBAN"
+ ),
+ ),
+ (
+ "adresse",
+ models.TextField(blank=True, null=True, verbose_name="Adresse"),
+ ),
+ (
+ "pachtnummer",
+ models.CharField(
+ blank=True, max_length=50, null=True, verbose_name="Pachtnummer"
+ ),
+ ),
+ (
+ "pachtbeginn_erste",
+ models.DateField(
+ blank=True, null=True, verbose_name="Erster Pachtbeginn"
+ ),
+ ),
+ (
+ "pachtende_letzte",
+ models.DateField(
+ blank=True, null=True, verbose_name="Letztes Pachtende"
+ ),
+ ),
+ (
+ "pachtzins_aktuell",
+ models.DecimalField(
+ blank=True,
+ decimal_places=2,
+ max_digits=12,
+ null=True,
+ verbose_name="Aktueller Pachtzins (€/Jahr)",
+ ),
+ ),
+ (
+ "landwirtschaftliche_ausbildung",
+ models.BooleanField(
+ default=False, verbose_name="Landwirtschaftliche Ausbildung"
+ ),
+ ),
+ (
+ "berufserfahrung_jahre",
+ models.IntegerField(
+ blank=True, null=True, verbose_name="Berufserfahrung (Jahre)"
+ ),
+ ),
+ (
+ "spezialisierung",
+ models.CharField(
+ blank=True,
+ max_length=100,
+ null=True,
+ verbose_name="Spezialisierung",
+ ),
+ ),
+ (
+ "notizen",
+ models.TextField(blank=True, null=True, verbose_name="Notizen"),
+ ),
+ ("aktiv", models.BooleanField(default=True, verbose_name="Aktiv")),
],
options={
- 'verbose_name': 'Pächter',
- 'verbose_name_plural': 'Pächter',
- 'ordering': ['nachname', 'vorname'],
+ "verbose_name": "Pächter",
+ "verbose_name_plural": "Pächter",
+ "ordering": ["nachname", "vorname"],
},
),
migrations.AlterModelOptions(
- name='person',
- options={'ordering': ['nachname', 'vorname'], 'verbose_name': 'Person (Legacy)', 'verbose_name_plural': 'Personen (Legacy)'},
+ name="person",
+ options={
+ "ordering": ["nachname", "vorname"],
+ "verbose_name": "Person (Legacy)",
+ "verbose_name_plural": "Personen (Legacy)",
+ },
),
migrations.AlterUniqueTogether(
- name='foerderung',
+ name="foerderung",
unique_together=set(),
),
migrations.AlterField(
- model_name='foerderung',
- name='person',
- field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='stiftung.person', verbose_name='Person (Legacy)'),
+ model_name="foerderung",
+ name="person",
+ field=models.ForeignKey(
+ blank=True,
+ null=True,
+ on_delete=django.db.models.deletion.CASCADE,
+ to="stiftung.person",
+ verbose_name="Person (Legacy)",
+ ),
),
migrations.AddField(
- model_name='foerderung',
- name='destinataer',
- field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='stiftung.destinataer', verbose_name='Destinatär'),
+ model_name="foerderung",
+ name="destinataer",
+ field=models.ForeignKey(
+ blank=True,
+ null=True,
+ on_delete=django.db.models.deletion.CASCADE,
+ to="stiftung.destinataer",
+ verbose_name="Destinatär",
+ ),
),
migrations.AlterField(
- model_name='verpachtung',
- name='paechter',
- field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='stiftung.paechter', verbose_name='Pächter'),
+ model_name="verpachtung",
+ name="paechter",
+ field=models.ForeignKey(
+ on_delete=django.db.models.deletion.CASCADE,
+ to="stiftung.paechter",
+ verbose_name="Pächter",
+ ),
),
]
diff --git a/app/stiftung/migrations/0006_remove_paechter_familienzweig_and_more.py b/app/stiftung/migrations/0006_remove_paechter_familienzweig_and_more.py
index 89d7dd1..6175307 100644
--- a/app/stiftung/migrations/0006_remove_paechter_familienzweig_and_more.py
+++ b/app/stiftung/migrations/0006_remove_paechter_familienzweig_and_more.py
@@ -6,17 +6,27 @@ from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
- ('stiftung', '0005_destinataer_paechter_alter_person_options_and_more'),
+ ("stiftung", "0005_destinataer_paechter_alter_person_options_and_more"),
]
operations = [
migrations.RemoveField(
- model_name='paechter',
- name='familienzweig',
+ model_name="paechter",
+ name="familienzweig",
),
migrations.AlterField(
- model_name='csvimport',
- name='import_type',
- field=models.CharField(choices=[('destinataere', 'Destinatäre'), ('paechter', 'Pächter'), ('laendereien', 'Ländereien'), ('verpachtungen', 'Verpachtungen'), ('personen', 'Personen (Legacy)')], max_length=20, verbose_name='Import-Typ'),
+ model_name="csvimport",
+ name="import_type",
+ field=models.CharField(
+ choices=[
+ ("destinataere", "Destinatäre"),
+ ("paechter", "Pächter"),
+ ("laendereien", "Ländereien"),
+ ("verpachtungen", "Verpachtungen"),
+ ("personen", "Personen (Legacy)"),
+ ],
+ max_length=20,
+ verbose_name="Import-Typ",
+ ),
),
]
diff --git a/app/stiftung/migrations/0007_remove_destinataer_adresse_remove_paechter_adresse_and_more.py b/app/stiftung/migrations/0007_remove_destinataer_adresse_remove_paechter_adresse_and_more.py
index 2542d52..e70c132 100644
--- a/app/stiftung/migrations/0007_remove_destinataer_adresse_remove_paechter_adresse_and_more.py
+++ b/app/stiftung/migrations/0007_remove_destinataer_adresse_remove_paechter_adresse_and_more.py
@@ -6,51 +6,71 @@ from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
- ('stiftung', '0006_remove_paechter_familienzweig_and_more'),
+ ("stiftung", "0006_remove_paechter_familienzweig_and_more"),
]
operations = [
migrations.RemoveField(
- model_name='destinataer',
- name='adresse',
+ model_name="destinataer",
+ name="adresse",
),
migrations.RemoveField(
- model_name='paechter',
- name='adresse',
+ model_name="paechter",
+ name="adresse",
),
migrations.AddField(
- model_name='destinataer',
- name='ort',
- field=models.CharField(blank=True, max_length=100, null=True, verbose_name='Ort'),
+ model_name="destinataer",
+ name="ort",
+ field=models.CharField(
+ blank=True, max_length=100, null=True, verbose_name="Ort"
+ ),
),
migrations.AddField(
- model_name='destinataer',
- name='plz',
- field=models.CharField(blank=True, max_length=10, null=True, verbose_name='PLZ'),
+ model_name="destinataer",
+ name="plz",
+ field=models.CharField(
+ blank=True, max_length=10, null=True, verbose_name="PLZ"
+ ),
),
migrations.AddField(
- model_name='destinataer',
- name='strasse',
- field=models.CharField(blank=True, max_length=200, null=True, verbose_name='Straße'),
+ model_name="destinataer",
+ name="strasse",
+ field=models.CharField(
+ blank=True, max_length=200, null=True, verbose_name="Straße"
+ ),
),
migrations.AddField(
- model_name='paechter',
- name='ort',
- field=models.CharField(blank=True, max_length=100, null=True, verbose_name='Ort'),
+ model_name="paechter",
+ name="ort",
+ field=models.CharField(
+ blank=True, max_length=100, null=True, verbose_name="Ort"
+ ),
),
migrations.AddField(
- model_name='paechter',
- name='personentyp',
- field=models.CharField(choices=[('natuerlich', 'Natürliche Person'), ('gesellschaft', 'Gesellschaft (GmbH, KG, etc.)')], default='natuerlich', max_length=20, verbose_name='Typ des Pächters'),
+ model_name="paechter",
+ name="personentyp",
+ field=models.CharField(
+ choices=[
+ ("natuerlich", "Natürliche Person"),
+ ("gesellschaft", "Gesellschaft (GmbH, KG, etc.)"),
+ ],
+ default="natuerlich",
+ max_length=20,
+ verbose_name="Typ des Pächters",
+ ),
),
migrations.AddField(
- model_name='paechter',
- name='plz',
- field=models.CharField(blank=True, max_length=10, null=True, verbose_name='PLZ'),
+ model_name="paechter",
+ name="plz",
+ field=models.CharField(
+ blank=True, max_length=10, null=True, verbose_name="PLZ"
+ ),
),
migrations.AddField(
- model_name='paechter',
- name='strasse',
- field=models.CharField(blank=True, max_length=200, null=True, verbose_name='Straße'),
+ model_name="paechter",
+ name="strasse",
+ field=models.CharField(
+ blank=True, max_length=200, null=True, verbose_name="Straße"
+ ),
),
]
diff --git a/app/stiftung/migrations/0008_dokumentlink_destinataer_id_and_more.py b/app/stiftung/migrations/0008_dokumentlink_destinataer_id_and_more.py
index 671cfa3..3cda78c 100644
--- a/app/stiftung/migrations/0008_dokumentlink_destinataer_id_and_more.py
+++ b/app/stiftung/migrations/0008_dokumentlink_destinataer_id_and_more.py
@@ -6,38 +6,57 @@ from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
- ('stiftung', '0007_remove_destinataer_adresse_remove_paechter_adresse_and_more'),
+ (
+ "stiftung",
+ "0007_remove_destinataer_adresse_remove_paechter_adresse_and_more",
+ ),
]
operations = [
migrations.AddField(
- model_name='dokumentlink',
- name='destinataer_id',
- field=models.UUIDField(blank=True, null=True, verbose_name='Destinatär ID'),
+ model_name="dokumentlink",
+ name="destinataer_id",
+ field=models.UUIDField(blank=True, null=True, verbose_name="Destinatär ID"),
),
migrations.AddField(
- model_name='dokumentlink',
- name='foerderung_id',
- field=models.UUIDField(blank=True, null=True, verbose_name='Förderung ID'),
+ model_name="dokumentlink",
+ name="foerderung_id",
+ field=models.UUIDField(blank=True, null=True, verbose_name="Förderung ID"),
),
migrations.AddField(
- model_name='dokumentlink',
- name='land_id',
- field=models.UUIDField(blank=True, null=True, verbose_name='Länderei ID'),
+ model_name="dokumentlink",
+ name="land_id",
+ field=models.UUIDField(blank=True, null=True, verbose_name="Länderei ID"),
),
migrations.AddField(
- model_name='dokumentlink',
- name='paechter_id',
- field=models.UUIDField(blank=True, null=True, verbose_name='Pächter ID'),
+ model_name="dokumentlink",
+ name="paechter_id",
+ field=models.UUIDField(blank=True, null=True, verbose_name="Pächter ID"),
),
migrations.AddField(
- model_name='dokumentlink',
- name='verpachtung_id',
- field=models.UUIDField(blank=True, null=True, verbose_name='Verpachtung ID'),
+ model_name="dokumentlink",
+ name="verpachtung_id",
+ field=models.UUIDField(
+ blank=True, null=True, verbose_name="Verpachtung ID"
+ ),
),
migrations.AlterField(
- model_name='dokumentlink',
- name='kontext',
- field=models.CharField(choices=[('pachtvertrag', 'Pachtvertrag'), ('antrag', 'Antrag'), ('verwendungsnachweis', 'Verwendungsnachweis'), ('rechnung', 'Rechnung'), ('vertrag', 'Vertrag'), ('bericht', 'Bericht'), ('landkarte', 'Landkarte'), ('kataster', 'Kataster'), ('anderes', 'Anderes')], default='anderes', max_length=30),
+ model_name="dokumentlink",
+ name="kontext",
+ field=models.CharField(
+ choices=[
+ ("pachtvertrag", "Pachtvertrag"),
+ ("antrag", "Antrag"),
+ ("verwendungsnachweis", "Verwendungsnachweis"),
+ ("rechnung", "Rechnung"),
+ ("vertrag", "Vertrag"),
+ ("bericht", "Bericht"),
+ ("landkarte", "Landkarte"),
+ ("kataster", "Kataster"),
+ ("anderes", "Anderes"),
+ ],
+ default="anderes",
+ max_length=30,
+ ),
),
]
diff --git a/app/stiftung/migrations/0009_alter_dokumentlink_paperless_document_id.py b/app/stiftung/migrations/0009_alter_dokumentlink_paperless_document_id.py
index ab988a8..217e30e 100644
--- a/app/stiftung/migrations/0009_alter_dokumentlink_paperless_document_id.py
+++ b/app/stiftung/migrations/0009_alter_dokumentlink_paperless_document_id.py
@@ -6,13 +6,13 @@ from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
- ('stiftung', '0008_dokumentlink_destinataer_id_and_more'),
+ ("stiftung", "0008_dokumentlink_destinataer_id_and_more"),
]
operations = [
migrations.AlterField(
- model_name='dokumentlink',
- name='paperless_document_id',
+ model_name="dokumentlink",
+ name="paperless_document_id",
field=models.IntegerField(),
),
]
diff --git a/app/stiftung/migrations/0010_rentmeister_stiftungskonto_verwaltungskosten.py b/app/stiftung/migrations/0010_rentmeister_stiftungskonto_verwaltungskosten.py
index dc65fe9..575e587 100644
--- a/app/stiftung/migrations/0010_rentmeister_stiftungskonto_verwaltungskosten.py
+++ b/app/stiftung/migrations/0010_rentmeister_stiftungskonto_verwaltungskosten.py
@@ -1,100 +1,344 @@
# Generated by Django 5.0.6 on 2025-08-24 17:48
-import django.db.models.deletion
import uuid
+
+import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
- ('stiftung', '0009_alter_dokumentlink_paperless_document_id'),
+ ("stiftung", "0009_alter_dokumentlink_paperless_document_id"),
]
operations = [
migrations.CreateModel(
- name='Rentmeister',
+ name="Rentmeister",
fields=[
- ('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)),
- ('anrede', models.CharField(blank=True, choices=[('herr', 'Herr'), ('frau', 'Frau'), ('dr', 'Dr.'), ('prof', 'Prof.'), ('prof_dr', 'Prof. Dr.')], max_length=10, verbose_name='Anrede')),
- ('vorname', models.CharField(max_length=100, verbose_name='Vorname')),
- ('nachname', models.CharField(max_length=100, verbose_name='Nachname')),
- ('titel', models.CharField(blank=True, max_length=50, verbose_name='Titel')),
- ('email', models.EmailField(blank=True, max_length=254, verbose_name='E-Mail')),
- ('telefon', models.CharField(blank=True, max_length=20, verbose_name='Telefon')),
- ('mobil', models.CharField(blank=True, max_length=20, verbose_name='Mobil')),
- ('strasse', models.CharField(blank=True, max_length=200, verbose_name='Straße')),
- ('plz', models.CharField(blank=True, max_length=10, verbose_name='PLZ')),
- ('ort', models.CharField(blank=True, max_length=100, verbose_name='Ort')),
- ('iban', models.CharField(blank=True, max_length=34, verbose_name='IBAN')),
- ('bic', models.CharField(blank=True, max_length=11, verbose_name='BIC')),
- ('bank_name', models.CharField(blank=True, max_length=100, verbose_name='Bank')),
- ('seit_datum', models.DateField(verbose_name='Rentmeister seit')),
- ('bis_datum', models.DateField(blank=True, null=True, verbose_name='Rentmeister bis')),
- ('aktiv', models.BooleanField(default=True, verbose_name='Aktiv')),
- ('monatliche_verguetung', models.DecimalField(blank=True, decimal_places=2, max_digits=8, null=True, verbose_name='Monatliche Vergütung (€)')),
- ('km_pauschale', models.DecimalField(decimal_places=2, default=0.3, max_digits=4, 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)),
+ (
+ "id",
+ models.UUIDField(
+ default=uuid.uuid4,
+ editable=False,
+ primary_key=True,
+ serialize=False,
+ ),
+ ),
+ (
+ "anrede",
+ models.CharField(
+ blank=True,
+ choices=[
+ ("herr", "Herr"),
+ ("frau", "Frau"),
+ ("dr", "Dr."),
+ ("prof", "Prof."),
+ ("prof_dr", "Prof. Dr."),
+ ],
+ max_length=10,
+ verbose_name="Anrede",
+ ),
+ ),
+ ("vorname", models.CharField(max_length=100, verbose_name="Vorname")),
+ ("nachname", models.CharField(max_length=100, verbose_name="Nachname")),
+ (
+ "titel",
+ models.CharField(blank=True, max_length=50, verbose_name="Titel"),
+ ),
+ (
+ "email",
+ models.EmailField(
+ blank=True, max_length=254, verbose_name="E-Mail"
+ ),
+ ),
+ (
+ "telefon",
+ models.CharField(blank=True, max_length=20, verbose_name="Telefon"),
+ ),
+ (
+ "mobil",
+ models.CharField(blank=True, max_length=20, verbose_name="Mobil"),
+ ),
+ (
+ "strasse",
+ models.CharField(blank=True, max_length=200, verbose_name="Straße"),
+ ),
+ (
+ "plz",
+ models.CharField(blank=True, max_length=10, verbose_name="PLZ"),
+ ),
+ (
+ "ort",
+ models.CharField(blank=True, max_length=100, verbose_name="Ort"),
+ ),
+ (
+ "iban",
+ models.CharField(blank=True, max_length=34, verbose_name="IBAN"),
+ ),
+ (
+ "bic",
+ models.CharField(blank=True, max_length=11, verbose_name="BIC"),
+ ),
+ (
+ "bank_name",
+ models.CharField(blank=True, max_length=100, verbose_name="Bank"),
+ ),
+ ("seit_datum", models.DateField(verbose_name="Rentmeister seit")),
+ (
+ "bis_datum",
+ models.DateField(
+ blank=True, null=True, verbose_name="Rentmeister bis"
+ ),
+ ),
+ ("aktiv", models.BooleanField(default=True, verbose_name="Aktiv")),
+ (
+ "monatliche_verguetung",
+ models.DecimalField(
+ blank=True,
+ decimal_places=2,
+ max_digits=8,
+ null=True,
+ verbose_name="Monatliche Vergütung (€)",
+ ),
+ ),
+ (
+ "km_pauschale",
+ models.DecimalField(
+ decimal_places=2,
+ default=0.3,
+ max_digits=4,
+ 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)),
],
options={
- 'verbose_name': 'Rentmeister',
- 'verbose_name_plural': 'Rentmeister',
- 'ordering': ['nachname', 'vorname'],
+ "verbose_name": "Rentmeister",
+ "verbose_name_plural": "Rentmeister",
+ "ordering": ["nachname", "vorname"],
},
),
migrations.CreateModel(
- name='StiftungsKonto',
+ name="StiftungsKonto",
fields=[
- ('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=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(blank=True, max_length=11, verbose_name='BIC')),
- ('konto_typ', models.CharField(choices=[('girokonto', 'Girokonto'), ('sparkonto', 'Sparkonto'), ('festgeld', 'Festgeld'), ('tagesgeld', 'Tagesgeld'), ('depot', 'Depot'), ('sonstiges', 'Sonstiges')], default='girokonto', max_length=20, verbose_name='Kontotyp')),
- ('saldo', models.DecimalField(decimal_places=2, default=0.0, max_digits=10, verbose_name='Aktueller Saldo')),
- ('saldo_datum', models.DateField(blank=True, null=True, verbose_name='Saldo-Datum')),
- ('zinssatz', models.DecimalField(blank=True, decimal_places=2, max_digits=5, null=True, verbose_name='Zinssatz (%)')),
- ('laufzeit_bis', models.DateField(blank=True, null=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)),
+ (
+ "id",
+ models.UUIDField(
+ default=uuid.uuid4,
+ editable=False,
+ primary_key=True,
+ serialize=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(blank=True, max_length=11, verbose_name="BIC"),
+ ),
+ (
+ "konto_typ",
+ models.CharField(
+ choices=[
+ ("girokonto", "Girokonto"),
+ ("sparkonto", "Sparkonto"),
+ ("festgeld", "Festgeld"),
+ ("tagesgeld", "Tagesgeld"),
+ ("depot", "Depot"),
+ ("sonstiges", "Sonstiges"),
+ ],
+ default="girokonto",
+ max_length=20,
+ verbose_name="Kontotyp",
+ ),
+ ),
+ (
+ "saldo",
+ models.DecimalField(
+ decimal_places=2,
+ default=0.0,
+ max_digits=10,
+ verbose_name="Aktueller Saldo",
+ ),
+ ),
+ (
+ "saldo_datum",
+ models.DateField(blank=True, null=True, verbose_name="Saldo-Datum"),
+ ),
+ (
+ "zinssatz",
+ models.DecimalField(
+ blank=True,
+ decimal_places=2,
+ max_digits=5,
+ null=True,
+ verbose_name="Zinssatz (%)",
+ ),
+ ),
+ (
+ "laufzeit_bis",
+ models.DateField(
+ blank=True, null=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)),
],
options={
- 'verbose_name': 'Stiftungskonto',
- 'verbose_name_plural': 'Stiftungskonten',
- 'ordering': ['bank_name', 'kontoname'],
+ "verbose_name": "Stiftungskonto",
+ "verbose_name_plural": "Stiftungskonten",
+ "ordering": ["bank_name", "kontoname"],
},
),
migrations.CreateModel(
- name='Verwaltungskosten',
+ name="Verwaltungskosten",
fields=[
- ('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)),
- ('bezeichnung', models.CharField(max_length=200, verbose_name='Bezeichnung')),
- ('kategorie', models.CharField(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')], max_length=30, verbose_name='Kategorie')),
- ('betrag', models.DecimalField(decimal_places=2, max_digits=10, verbose_name='Betrag (€)')),
- ('datum', models.DateField(verbose_name='Datum')),
- ('lieferant_firma', models.CharField(blank=True, max_length=200, verbose_name='Lieferant/Firma')),
- ('rechnungsnummer', models.CharField(blank=True, max_length=100, verbose_name='Rechnungsnummer')),
- ('status', models.CharField(choices=[('geplant', 'Geplant'), ('bestellt', 'Bestellt'), ('erhalten', 'Erhalten'), ('bezahlt', 'Bezahlt'), ('storniert', 'Storniert')], default='geplant', max_length=20, verbose_name='Status')),
- ('km_anzahl', models.DecimalField(blank=True, decimal_places=1, max_digits=8, null=True, verbose_name='Kilometer')),
- ('km_satz', models.DecimalField(blank=True, decimal_places=2, max_digits=4, null=True, verbose_name='€/km')),
- ('von_ort', models.CharField(blank=True, max_length=100, verbose_name='Von (Ort)')),
- ('nach_ort', models.CharField(blank=True, max_length=100, verbose_name='Nach (Ort)')),
- ('zweck', models.CharField(blank=True, max_length=200, 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)),
- ('konto', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='stiftung.stiftungskonto', verbose_name='Konto')),
- ('rentmeister', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='stiftung.rentmeister', verbose_name='Rentmeister')),
+ (
+ "id",
+ models.UUIDField(
+ default=uuid.uuid4,
+ editable=False,
+ primary_key=True,
+ serialize=False,
+ ),
+ ),
+ (
+ "bezeichnung",
+ models.CharField(max_length=200, verbose_name="Bezeichnung"),
+ ),
+ (
+ "kategorie",
+ models.CharField(
+ 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"),
+ ],
+ max_length=30,
+ verbose_name="Kategorie",
+ ),
+ ),
+ (
+ "betrag",
+ models.DecimalField(
+ decimal_places=2, max_digits=10, verbose_name="Betrag (€)"
+ ),
+ ),
+ ("datum", models.DateField(verbose_name="Datum")),
+ (
+ "lieferant_firma",
+ models.CharField(
+ blank=True, max_length=200, verbose_name="Lieferant/Firma"
+ ),
+ ),
+ (
+ "rechnungsnummer",
+ models.CharField(
+ blank=True, max_length=100, verbose_name="Rechnungsnummer"
+ ),
+ ),
+ (
+ "status",
+ models.CharField(
+ choices=[
+ ("geplant", "Geplant"),
+ ("bestellt", "Bestellt"),
+ ("erhalten", "Erhalten"),
+ ("bezahlt", "Bezahlt"),
+ ("storniert", "Storniert"),
+ ],
+ default="geplant",
+ max_length=20,
+ verbose_name="Status",
+ ),
+ ),
+ (
+ "km_anzahl",
+ models.DecimalField(
+ blank=True,
+ decimal_places=1,
+ max_digits=8,
+ null=True,
+ verbose_name="Kilometer",
+ ),
+ ),
+ (
+ "km_satz",
+ models.DecimalField(
+ blank=True,
+ decimal_places=2,
+ max_digits=4,
+ null=True,
+ verbose_name="€/km",
+ ),
+ ),
+ (
+ "von_ort",
+ models.CharField(
+ blank=True, max_length=100, verbose_name="Von (Ort)"
+ ),
+ ),
+ (
+ "nach_ort",
+ models.CharField(
+ blank=True, max_length=100, verbose_name="Nach (Ort)"
+ ),
+ ),
+ (
+ "zweck",
+ models.CharField(
+ blank=True, max_length=200, 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)),
+ (
+ "konto",
+ models.ForeignKey(
+ blank=True,
+ null=True,
+ on_delete=django.db.models.deletion.SET_NULL,
+ to="stiftung.stiftungskonto",
+ verbose_name="Konto",
+ ),
+ ),
+ (
+ "rentmeister",
+ models.ForeignKey(
+ blank=True,
+ null=True,
+ on_delete=django.db.models.deletion.SET_NULL,
+ to="stiftung.rentmeister",
+ verbose_name="Rentmeister",
+ ),
+ ),
],
options={
- 'verbose_name': 'Verwaltungskosten',
- 'verbose_name_plural': 'Verwaltungskosten',
- 'ordering': ['-datum', '-erstellt_am'],
+ "verbose_name": "Verwaltungskosten",
+ "verbose_name_plural": "Verwaltungskosten",
+ "ordering": ["-datum", "-erstellt_am"],
},
),
]
diff --git a/app/stiftung/migrations/0011_banktransaction.py b/app/stiftung/migrations/0011_banktransaction.py
index 9a21b3c..bb478a2 100644
--- a/app/stiftung/migrations/0011_banktransaction.py
+++ b/app/stiftung/migrations/0011_banktransaction.py
@@ -1,44 +1,156 @@
# Generated by Django 5.0.6 on 2025-08-24 19:27
-import django.db.models.deletion
import uuid
+
+import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
- ('stiftung', '0010_rentmeister_stiftungskonto_verwaltungskosten'),
+ ("stiftung", "0010_rentmeister_stiftungskonto_verwaltungskosten"),
]
operations = [
migrations.CreateModel(
- name='BankTransaction',
+ name="BankTransaction",
fields=[
- ('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)),
- ('datum', models.DateField(verbose_name='Buchungsdatum')),
- ('valuta', models.DateField(blank=True, null=True, verbose_name='Valutadatum')),
- ('betrag', models.DecimalField(decimal_places=2, max_digits=12, verbose_name='Betrag (€)')),
- ('waehrung', models.CharField(default='EUR', max_length=3, verbose_name='Währung')),
- ('verwendungszweck', models.TextField(verbose_name='Verwendungszweck')),
- ('empfaenger_zahlungspflichtiger', models.CharField(blank=True, max_length=200, verbose_name='Empfänger/Zahlungspflichtiger')),
- ('iban_gegenpartei', models.CharField(blank=True, max_length=34, verbose_name='IBAN Gegenpartei')),
- ('bic_gegenpartei', models.CharField(blank=True, max_length=11, verbose_name='BIC Gegenpartei')),
- ('referenz', models.CharField(blank=True, max_length=100, verbose_name='Referenz/Transaktions-ID')),
- ('transaction_type', models.CharField(choices=[('eingang', 'Eingang'), ('ausgang', 'Ausgang'), ('lastschrift', 'Lastschrift'), ('ueberweisung', 'Überweisung'), ('dauerauftrag', 'Dauerauftrag'), ('kartenzahlung', 'Kartenzahlung'), ('zinsen', 'Zinsen'), ('gebuehren', 'Gebühren'), ('sonstiges', 'Sonstiges')], default='sonstiges', max_length=20, verbose_name='Transaktionsart')),
- ('status', models.CharField(choices=[('imported', 'Importiert'), ('verified', 'Geprüft'), ('assigned', 'Zugeordnet'), ('ignored', 'Ignoriert')], default='imported', max_length=20, verbose_name='Status')),
- ('kommentare', models.TextField(blank=True, verbose_name='Kommentare')),
- ('import_datei', models.CharField(blank=True, max_length=255, verbose_name='Import-Datei')),
- ('importiert_am', models.DateTimeField(auto_now_add=True, verbose_name='Importiert am')),
- ('saldo_nach_buchung', models.DecimalField(blank=True, decimal_places=2, max_digits=12, null=True, verbose_name='Saldo nach Buchung')),
- ('konto', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='stiftung.stiftungskonto', verbose_name='Konto')),
- ('verwaltungskosten', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='stiftung.verwaltungskosten', verbose_name='Zugeordnete Verwaltungskosten')),
+ (
+ "id",
+ models.UUIDField(
+ default=uuid.uuid4,
+ editable=False,
+ primary_key=True,
+ serialize=False,
+ ),
+ ),
+ ("datum", models.DateField(verbose_name="Buchungsdatum")),
+ (
+ "valuta",
+ models.DateField(blank=True, null=True, verbose_name="Valutadatum"),
+ ),
+ (
+ "betrag",
+ models.DecimalField(
+ decimal_places=2, max_digits=12, verbose_name="Betrag (€)"
+ ),
+ ),
+ (
+ "waehrung",
+ models.CharField(
+ default="EUR", max_length=3, verbose_name="Währung"
+ ),
+ ),
+ ("verwendungszweck", models.TextField(verbose_name="Verwendungszweck")),
+ (
+ "empfaenger_zahlungspflichtiger",
+ models.CharField(
+ blank=True,
+ max_length=200,
+ verbose_name="Empfänger/Zahlungspflichtiger",
+ ),
+ ),
+ (
+ "iban_gegenpartei",
+ models.CharField(
+ blank=True, max_length=34, verbose_name="IBAN Gegenpartei"
+ ),
+ ),
+ (
+ "bic_gegenpartei",
+ models.CharField(
+ blank=True, max_length=11, verbose_name="BIC Gegenpartei"
+ ),
+ ),
+ (
+ "referenz",
+ models.CharField(
+ blank=True,
+ max_length=100,
+ verbose_name="Referenz/Transaktions-ID",
+ ),
+ ),
+ (
+ "transaction_type",
+ models.CharField(
+ choices=[
+ ("eingang", "Eingang"),
+ ("ausgang", "Ausgang"),
+ ("lastschrift", "Lastschrift"),
+ ("ueberweisung", "Überweisung"),
+ ("dauerauftrag", "Dauerauftrag"),
+ ("kartenzahlung", "Kartenzahlung"),
+ ("zinsen", "Zinsen"),
+ ("gebuehren", "Gebühren"),
+ ("sonstiges", "Sonstiges"),
+ ],
+ default="sonstiges",
+ max_length=20,
+ verbose_name="Transaktionsart",
+ ),
+ ),
+ (
+ "status",
+ models.CharField(
+ choices=[
+ ("imported", "Importiert"),
+ ("verified", "Geprüft"),
+ ("assigned", "Zugeordnet"),
+ ("ignored", "Ignoriert"),
+ ],
+ default="imported",
+ max_length=20,
+ verbose_name="Status",
+ ),
+ ),
+ ("kommentare", models.TextField(blank=True, verbose_name="Kommentare")),
+ (
+ "import_datei",
+ models.CharField(
+ blank=True, max_length=255, verbose_name="Import-Datei"
+ ),
+ ),
+ (
+ "importiert_am",
+ models.DateTimeField(
+ auto_now_add=True, verbose_name="Importiert am"
+ ),
+ ),
+ (
+ "saldo_nach_buchung",
+ models.DecimalField(
+ blank=True,
+ decimal_places=2,
+ max_digits=12,
+ null=True,
+ verbose_name="Saldo nach Buchung",
+ ),
+ ),
+ (
+ "konto",
+ models.ForeignKey(
+ on_delete=django.db.models.deletion.CASCADE,
+ to="stiftung.stiftungskonto",
+ verbose_name="Konto",
+ ),
+ ),
+ (
+ "verwaltungskosten",
+ models.ForeignKey(
+ blank=True,
+ null=True,
+ on_delete=django.db.models.deletion.SET_NULL,
+ to="stiftung.verwaltungskosten",
+ verbose_name="Zugeordnete Verwaltungskosten",
+ ),
+ ),
],
options={
- 'verbose_name': 'Banktransaktion',
- 'verbose_name_plural': 'Banktransaktionen',
- 'ordering': ['-datum', '-importiert_am'],
- 'unique_together': {('konto', 'datum', 'betrag', 'referenz')},
+ "verbose_name": "Banktransaktion",
+ "verbose_name_plural": "Banktransaktionen",
+ "ordering": ["-datum", "-importiert_am"],
+ "unique_together": {("konto", "datum", "betrag", "referenz")},
},
),
]
diff --git a/app/stiftung/migrations/0012_verwaltungskosten_quellkonto_and_more.py b/app/stiftung/migrations/0012_verwaltungskosten_quellkonto_and_more.py
index 6e8017d..a8987f6 100644
--- a/app/stiftung/migrations/0012_verwaltungskosten_quellkonto_and_more.py
+++ b/app/stiftung/migrations/0012_verwaltungskosten_quellkonto_and_more.py
@@ -7,28 +7,55 @@ from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
- ('stiftung', '0011_banktransaction'),
+ ("stiftung", "0011_banktransaction"),
]
operations = [
migrations.AddField(
- model_name='verwaltungskosten',
- name='quellkonto',
- field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='ausgaben', to='stiftung.stiftungskonto', verbose_name='Quellkonto'),
+ model_name="verwaltungskosten",
+ name="quellkonto",
+ field=models.ForeignKey(
+ blank=True,
+ null=True,
+ on_delete=django.db.models.deletion.SET_NULL,
+ related_name="ausgaben",
+ to="stiftung.stiftungskonto",
+ verbose_name="Quellkonto",
+ ),
),
migrations.AddField(
- model_name='verwaltungskosten',
- name='zahlungskonto',
- field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='zahlungen', to='stiftung.stiftungskonto', verbose_name='Zahlungskonto'),
+ model_name="verwaltungskosten",
+ name="zahlungskonto",
+ field=models.ForeignKey(
+ blank=True,
+ null=True,
+ on_delete=django.db.models.deletion.SET_NULL,
+ related_name="zahlungen",
+ to="stiftung.stiftungskonto",
+ verbose_name="Zahlungskonto",
+ ),
),
migrations.AlterField(
- model_name='verwaltungskosten',
- name='konto',
- field=models.ForeignKey(blank=True, help_text='Veraltet - verwende Zahlungskonto und Quellkonto', null=True, on_delete=django.db.models.deletion.SET_NULL, to='stiftung.stiftungskonto', verbose_name='Konto (Legacy)'),
+ model_name="verwaltungskosten",
+ name="konto",
+ field=models.ForeignKey(
+ blank=True,
+ help_text="Veraltet - verwende Zahlungskonto und Quellkonto",
+ null=True,
+ on_delete=django.db.models.deletion.SET_NULL,
+ to="stiftung.stiftungskonto",
+ verbose_name="Konto (Legacy)",
+ ),
),
migrations.AlterField(
- model_name='verwaltungskosten',
- name='rentmeister',
- field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='stiftung.rentmeister', verbose_name='Zuständiger Rentmeister'),
+ model_name="verwaltungskosten",
+ name="rentmeister",
+ field=models.ForeignKey(
+ blank=True,
+ null=True,
+ on_delete=django.db.models.deletion.SET_NULL,
+ to="stiftung.rentmeister",
+ verbose_name="Zuständiger Rentmeister",
+ ),
),
]
diff --git a/app/stiftung/migrations/0013_alter_verwaltungskosten_status.py b/app/stiftung/migrations/0013_alter_verwaltungskosten_status.py
index 371b067..e3b3d7d 100644
--- a/app/stiftung/migrations/0013_alter_verwaltungskosten_status.py
+++ b/app/stiftung/migrations/0013_alter_verwaltungskosten_status.py
@@ -6,13 +6,25 @@ from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
- ('stiftung', '0012_verwaltungskosten_quellkonto_and_more'),
+ ("stiftung", "0012_verwaltungskosten_quellkonto_and_more"),
]
operations = [
migrations.AlterField(
- model_name='verwaltungskosten',
- name='status',
- field=models.CharField(choices=[('geplant', 'Geplant'), ('bestellt', 'Bestellt'), ('erhalten', 'Erhalten'), ('in_bearbeitung', 'In Bearbeitung'), ('bezahlt', 'Bezahlt'), ('storniert', 'Storniert')], default='geplant', max_length=20, verbose_name='Status'),
+ model_name="verwaltungskosten",
+ name="status",
+ field=models.CharField(
+ choices=[
+ ("geplant", "Geplant"),
+ ("bestellt", "Bestellt"),
+ ("erhalten", "Erhalten"),
+ ("in_bearbeitung", "In Bearbeitung"),
+ ("bezahlt", "Bezahlt"),
+ ("storniert", "Storniert"),
+ ],
+ default="geplant",
+ max_length=20,
+ verbose_name="Status",
+ ),
),
]
diff --git a/app/stiftung/migrations/0014_dokumentlink_rentmeister_id.py b/app/stiftung/migrations/0014_dokumentlink_rentmeister_id.py
index 7e12549..087a174 100644
--- a/app/stiftung/migrations/0014_dokumentlink_rentmeister_id.py
+++ b/app/stiftung/migrations/0014_dokumentlink_rentmeister_id.py
@@ -6,13 +6,15 @@ from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
- ('stiftung', '0013_alter_verwaltungskosten_status'),
+ ("stiftung", "0013_alter_verwaltungskosten_status"),
]
operations = [
migrations.AddField(
- model_name='dokumentlink',
- name='rentmeister_id',
- field=models.UUIDField(blank=True, null=True, verbose_name='Rentmeister ID'),
+ model_name="dokumentlink",
+ name="rentmeister_id",
+ field=models.UUIDField(
+ blank=True, null=True, verbose_name="Rentmeister ID"
+ ),
),
]
diff --git a/app/stiftung/migrations/0015_backupjob_auditlog.py b/app/stiftung/migrations/0015_backupjob_auditlog.py
index 28bc7af..b1dfc9b 100644
--- a/app/stiftung/migrations/0015_backupjob_auditlog.py
+++ b/app/stiftung/migrations/0015_backupjob_auditlog.py
@@ -1,7 +1,8 @@
# Generated by Django 5.0.6 on 2025-08-26 08:33
-import django.db.models.deletion
import uuid
+
+import django.db.models.deletion
from django.conf import settings
from django.db import migrations, models
@@ -9,55 +10,229 @@ from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
- ('stiftung', '0014_dokumentlink_rentmeister_id'),
+ ("stiftung", "0014_dokumentlink_rentmeister_id"),
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]
operations = [
migrations.CreateModel(
- name='BackupJob',
+ name="BackupJob",
fields=[
- ('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)),
- ('backup_type', models.CharField(choices=[('full', 'Vollständiges Backup'), ('database', 'Nur Datenbank'), ('files', 'Nur Dateien')], max_length=20, verbose_name='Backup-Typ')),
- ('status', models.CharField(choices=[('pending', 'Wartend'), ('running', 'Läuft'), ('completed', 'Abgeschlossen'), ('failed', 'Fehlgeschlagen')], default='pending', max_length=20, verbose_name='Status')),
- ('created_at', models.DateTimeField(auto_now_add=True, verbose_name='Erstellt am')),
- ('started_at', models.DateTimeField(blank=True, null=True, verbose_name='Gestartet am')),
- ('completed_at', models.DateTimeField(blank=True, null=True, verbose_name='Abgeschlossen am')),
- ('backup_filename', models.CharField(blank=True, max_length=255, verbose_name='Backup-Dateiname')),
- ('backup_size', models.BigIntegerField(blank=True, null=True, verbose_name='Backup-Größe (Bytes)')),
- ('error_message', models.TextField(blank=True, verbose_name='Fehlermeldung')),
- ('database_size', models.BigIntegerField(blank=True, null=True, verbose_name='Datenbankgröße (Bytes)')),
- ('files_count', models.IntegerField(blank=True, null=True, verbose_name='Anzahl Dateien')),
- ('created_by', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, to=settings.AUTH_USER_MODEL, verbose_name='Erstellt von')),
+ (
+ "id",
+ models.UUIDField(
+ default=uuid.uuid4,
+ editable=False,
+ primary_key=True,
+ serialize=False,
+ ),
+ ),
+ (
+ "backup_type",
+ models.CharField(
+ choices=[
+ ("full", "Vollständiges Backup"),
+ ("database", "Nur Datenbank"),
+ ("files", "Nur Dateien"),
+ ],
+ max_length=20,
+ verbose_name="Backup-Typ",
+ ),
+ ),
+ (
+ "status",
+ models.CharField(
+ choices=[
+ ("pending", "Wartend"),
+ ("running", "Läuft"),
+ ("completed", "Abgeschlossen"),
+ ("failed", "Fehlgeschlagen"),
+ ],
+ default="pending",
+ max_length=20,
+ verbose_name="Status",
+ ),
+ ),
+ (
+ "created_at",
+ models.DateTimeField(auto_now_add=True, verbose_name="Erstellt am"),
+ ),
+ (
+ "started_at",
+ models.DateTimeField(
+ blank=True, null=True, verbose_name="Gestartet am"
+ ),
+ ),
+ (
+ "completed_at",
+ models.DateTimeField(
+ blank=True, null=True, verbose_name="Abgeschlossen am"
+ ),
+ ),
+ (
+ "backup_filename",
+ models.CharField(
+ blank=True, max_length=255, verbose_name="Backup-Dateiname"
+ ),
+ ),
+ (
+ "backup_size",
+ models.BigIntegerField(
+ blank=True, null=True, verbose_name="Backup-Größe (Bytes)"
+ ),
+ ),
+ (
+ "error_message",
+ models.TextField(blank=True, verbose_name="Fehlermeldung"),
+ ),
+ (
+ "database_size",
+ models.BigIntegerField(
+ blank=True, null=True, verbose_name="Datenbankgröße (Bytes)"
+ ),
+ ),
+ (
+ "files_count",
+ models.IntegerField(
+ blank=True, null=True, verbose_name="Anzahl Dateien"
+ ),
+ ),
+ (
+ "created_by",
+ models.ForeignKey(
+ null=True,
+ on_delete=django.db.models.deletion.SET_NULL,
+ to=settings.AUTH_USER_MODEL,
+ verbose_name="Erstellt von",
+ ),
+ ),
],
options={
- 'verbose_name': 'Backup-Job',
- 'verbose_name_plural': 'Backup-Jobs',
- 'ordering': ['-created_at'],
+ "verbose_name": "Backup-Job",
+ "verbose_name_plural": "Backup-Jobs",
+ "ordering": ["-created_at"],
},
),
migrations.CreateModel(
- name='AuditLog',
+ name="AuditLog",
fields=[
- ('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)),
- ('username', models.CharField(max_length=150, verbose_name='Benutzername')),
- ('timestamp', models.DateTimeField(auto_now_add=True, verbose_name='Zeitpunkt')),
- ('action', models.CharField(choices=[('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')], max_length=20, verbose_name='Aktion')),
- ('entity_type', models.CharField(choices=[('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')], max_length=20, verbose_name='Entitätstyp')),
- ('entity_id', models.CharField(blank=True, max_length=100, verbose_name='Entitäts-ID')),
- ('entity_name', models.CharField(max_length=255, verbose_name='Entitätsname')),
- ('description', models.TextField(verbose_name='Beschreibung')),
- ('changes', models.JSONField(blank=True, null=True, verbose_name='Änderungen')),
- ('ip_address', models.GenericIPAddressField(blank=True, null=True, verbose_name='IP-Adresse')),
- ('user_agent', models.TextField(blank=True, verbose_name='User Agent')),
- ('session_key', models.CharField(blank=True, max_length=40, verbose_name='Session-Key')),
- ('user', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, to=settings.AUTH_USER_MODEL, verbose_name='Benutzer')),
+ (
+ "id",
+ models.UUIDField(
+ default=uuid.uuid4,
+ editable=False,
+ primary_key=True,
+ serialize=False,
+ ),
+ ),
+ (
+ "username",
+ models.CharField(max_length=150, verbose_name="Benutzername"),
+ ),
+ (
+ "timestamp",
+ models.DateTimeField(auto_now_add=True, verbose_name="Zeitpunkt"),
+ ),
+ (
+ "action",
+ models.CharField(
+ choices=[
+ ("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"),
+ ],
+ max_length=20,
+ verbose_name="Aktion",
+ ),
+ ),
+ (
+ "entity_type",
+ models.CharField(
+ choices=[
+ ("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"),
+ ],
+ max_length=20,
+ verbose_name="Entitätstyp",
+ ),
+ ),
+ (
+ "entity_id",
+ models.CharField(
+ blank=True, max_length=100, verbose_name="Entitäts-ID"
+ ),
+ ),
+ (
+ "entity_name",
+ models.CharField(max_length=255, verbose_name="Entitätsname"),
+ ),
+ ("description", models.TextField(verbose_name="Beschreibung")),
+ (
+ "changes",
+ models.JSONField(blank=True, null=True, verbose_name="Änderungen"),
+ ),
+ (
+ "ip_address",
+ models.GenericIPAddressField(
+ blank=True, null=True, verbose_name="IP-Adresse"
+ ),
+ ),
+ ("user_agent", models.TextField(blank=True, verbose_name="User Agent")),
+ (
+ "session_key",
+ models.CharField(
+ blank=True, max_length=40, verbose_name="Session-Key"
+ ),
+ ),
+ (
+ "user",
+ models.ForeignKey(
+ null=True,
+ on_delete=django.db.models.deletion.SET_NULL,
+ to=settings.AUTH_USER_MODEL,
+ verbose_name="Benutzer",
+ ),
+ ),
],
options={
- 'verbose_name': 'Audit Log Eintrag',
- 'verbose_name_plural': 'Audit Log Einträge',
- 'ordering': ['-timestamp'],
- 'indexes': [models.Index(fields=['timestamp'], name='stiftung_au_timesta_c4591e_idx'), models.Index(fields=['user', 'timestamp'], name='stiftung_au_user_id_e3fc12_idx'), models.Index(fields=['entity_type', 'timestamp'], name='stiftung_au_entity__68f25d_idx'), models.Index(fields=['action', 'timestamp'], name='stiftung_au_action_288765_idx')],
+ "verbose_name": "Audit Log Eintrag",
+ "verbose_name_plural": "Audit Log Einträge",
+ "ordering": ["-timestamp"],
+ "indexes": [
+ models.Index(
+ fields=["timestamp"], name="stiftung_au_timesta_c4591e_idx"
+ ),
+ models.Index(
+ fields=["user", "timestamp"],
+ name="stiftung_au_user_id_e3fc12_idx",
+ ),
+ models.Index(
+ fields=["entity_type", "timestamp"],
+ name="stiftung_au_entity__68f25d_idx",
+ ),
+ models.Index(
+ fields=["action", "timestamp"],
+ name="stiftung_au_action_288765_idx",
+ ),
+ ],
},
),
]
diff --git a/app/stiftung/migrations/0016_applicationpermission.py b/app/stiftung/migrations/0016_applicationpermission.py
index e0f4195..3095660 100644
--- a/app/stiftung/migrations/0016_applicationpermission.py
+++ b/app/stiftung/migrations/0016_applicationpermission.py
@@ -6,19 +6,57 @@ from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
- ('stiftung', '0015_backupjob_auditlog'),
+ ("stiftung", "0015_backupjob_auditlog"),
]
operations = [
migrations.CreateModel(
- name='ApplicationPermission',
+ name="ApplicationPermission",
fields=[
- ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
+ (
+ "id",
+ models.BigAutoField(
+ auto_created=True,
+ primary_key=True,
+ serialize=False,
+ verbose_name="ID",
+ ),
+ ),
],
options={
- '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'), ('manage_documents', 'Kann Dokumente verwalten'), ('view_documents', 'Kann Dokumente anzeigen'), ('link_documents', 'Kann Dokumente verknüpfen'), ('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'), ('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_data', 'Kann Daten importieren'), ('export_data', 'Kann Daten exportieren'), ('access_django_admin', 'Kann Django Admin aufrufen'), ('view_system_stats', 'Kann Systemstatistiken anzeigen')],
- 'managed': False,
- 'default_permissions': (),
+ "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"),
+ ("manage_documents", "Kann Dokumente verwalten"),
+ ("view_documents", "Kann Dokumente anzeigen"),
+ ("link_documents", "Kann Dokumente verknüpfen"),
+ ("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"),
+ ("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_data", "Kann Daten importieren"),
+ ("export_data", "Kann Daten exportieren"),
+ ("access_django_admin", "Kann Django Admin aufrufen"),
+ ("view_system_stats", "Kann Systemstatistiken anzeigen"),
+ ],
+ "managed": False,
+ "default_permissions": (),
},
),
]
diff --git a/app/stiftung/migrations/0017_destinataer_haushaltsgroesse_and_more.py b/app/stiftung/migrations/0017_destinataer_haushaltsgroesse_and_more.py
index 32f0a58..19dc900 100644
--- a/app/stiftung/migrations/0017_destinataer_haushaltsgroesse_and_more.py
+++ b/app/stiftung/migrations/0017_destinataer_haushaltsgroesse_and_more.py
@@ -7,48 +7,74 @@ from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
- ('stiftung', '0016_applicationpermission'),
+ ("stiftung", "0016_applicationpermission"),
]
operations = [
migrations.AddField(
- model_name='destinataer',
- name='haushaltsgroesse',
- field=models.PositiveIntegerField(default=1, verbose_name='Haushaltsgröße'),
+ model_name="destinataer",
+ name="haushaltsgroesse",
+ field=models.PositiveIntegerField(default=1, verbose_name="Haushaltsgröße"),
),
migrations.AddField(
- model_name='destinataer',
- name='ist_abkoemmling',
- field=models.BooleanField(default=False, verbose_name='Abkömmling gem. Satzung'),
+ model_name="destinataer",
+ name="ist_abkoemmling",
+ field=models.BooleanField(
+ default=False, verbose_name="Abkömmling gem. Satzung"
+ ),
),
migrations.AddField(
- model_name='destinataer',
- name='letzter_studiennachweis',
- field=models.DateField(blank=True, null=True, verbose_name='Letzter Studiennachweis'),
+ model_name="destinataer",
+ name="letzter_studiennachweis",
+ field=models.DateField(
+ blank=True, null=True, verbose_name="Letzter Studiennachweis"
+ ),
),
migrations.AddField(
- model_name='destinataer',
- name='monatliche_bezuege',
- field=models.DecimalField(blank=True, decimal_places=2, max_digits=10, null=True, verbose_name='Monatliche Bezüge (€)'),
+ model_name="destinataer",
+ name="monatliche_bezuege",
+ field=models.DecimalField(
+ blank=True,
+ decimal_places=2,
+ max_digits=10,
+ null=True,
+ verbose_name="Monatliche Bezüge (€)",
+ ),
),
migrations.AddField(
- model_name='destinataer',
- name='standard_konto',
- field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='stiftung.stiftungskonto', verbose_name='Standard Auszahlungskonto'),
+ model_name="destinataer",
+ name="standard_konto",
+ field=models.ForeignKey(
+ blank=True,
+ null=True,
+ on_delete=django.db.models.deletion.SET_NULL,
+ to="stiftung.stiftungskonto",
+ verbose_name="Standard Auszahlungskonto",
+ ),
),
migrations.AddField(
- model_name='destinataer',
- name='studiennachweis_erforderlich',
- field=models.BooleanField(default=False, verbose_name='Studiennachweis erforderlich'),
+ model_name="destinataer",
+ name="studiennachweis_erforderlich",
+ field=models.BooleanField(
+ default=False, verbose_name="Studiennachweis erforderlich"
+ ),
),
migrations.AddField(
- model_name='destinataer',
- name='unterstuetzung_bestaetigt',
- field=models.BooleanField(default=False, verbose_name='Unterstützung bestätigt'),
+ model_name="destinataer",
+ name="unterstuetzung_bestaetigt",
+ field=models.BooleanField(
+ default=False, verbose_name="Unterstützung bestätigt"
+ ),
),
migrations.AddField(
- model_name='destinataer',
- name='vermoegen',
- field=models.DecimalField(blank=True, decimal_places=2, max_digits=12, null=True, verbose_name='Vermögen (€)'),
+ model_name="destinataer",
+ name="vermoegen",
+ field=models.DecimalField(
+ blank=True,
+ decimal_places=2,
+ max_digits=12,
+ null=True,
+ verbose_name="Vermögen (€)",
+ ),
),
]
diff --git a/app/stiftung/migrations/0018_destinataer_vierteljaehrlicher_betrag.py b/app/stiftung/migrations/0018_destinataer_vierteljaehrlicher_betrag.py
index 12dc94b..f24ca63 100644
--- a/app/stiftung/migrations/0018_destinataer_vierteljaehrlicher_betrag.py
+++ b/app/stiftung/migrations/0018_destinataer_vierteljaehrlicher_betrag.py
@@ -6,13 +6,19 @@ from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
- ('stiftung', '0017_destinataer_haushaltsgroesse_and_more'),
+ ("stiftung", "0017_destinataer_haushaltsgroesse_and_more"),
]
operations = [
migrations.AddField(
- model_name='destinataer',
- name='vierteljaehrlicher_betrag',
- field=models.DecimalField(blank=True, decimal_places=2, max_digits=12, null=True, verbose_name='Vierteljährlicher Betrag (€)'),
+ model_name="destinataer",
+ name="vierteljaehrlicher_betrag",
+ field=models.DecimalField(
+ blank=True,
+ decimal_places=2,
+ max_digits=12,
+ null=True,
+ verbose_name="Vierteljährlicher Betrag (€)",
+ ),
),
]
diff --git a/app/stiftung/migrations/0019_destinataerunterstuetzung.py b/app/stiftung/migrations/0019_destinataerunterstuetzung.py
index 516cce0..2e95f5e 100644
--- a/app/stiftung/migrations/0019_destinataerunterstuetzung.py
+++ b/app/stiftung/migrations/0019_destinataerunterstuetzung.py
@@ -1,35 +1,91 @@
# Generated by Django 5.0.6 on 2025-08-29 13:40
-import django.db.models.deletion
import uuid
+
+import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
- ('stiftung', '0018_destinataer_vierteljaehrlicher_betrag'),
+ ("stiftung", "0018_destinataer_vierteljaehrlicher_betrag"),
]
operations = [
migrations.CreateModel(
- name='DestinataerUnterstuetzung',
+ name="DestinataerUnterstuetzung",
fields=[
- ('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)),
- ('betrag', models.DecimalField(decimal_places=2, max_digits=12, verbose_name='Betrag (€)')),
- ('faellig_am', models.DateField(verbose_name='Fällig am')),
- ('status', models.CharField(choices=[('geplant', 'Geplant'), ('in_bearbeitung', 'In Bearbeitung'), ('ausgezahlt', 'Ausgezahlt'), ('storniert', 'Storniert')], default='geplant', max_length=20, verbose_name='Status')),
- ('beschreibung', models.CharField(blank=True, max_length=255, verbose_name='Beschreibung')),
- ('erstellt_am', models.DateTimeField(auto_now_add=True)),
- ('aktualisiert_am', models.DateTimeField(auto_now=True)),
- ('destinataer', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='unterstuetzungen', to='stiftung.destinataer', verbose_name='Destinatär')),
- ('konto', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to='stiftung.stiftungskonto', verbose_name='Zahlungskonto')),
+ (
+ "id",
+ models.UUIDField(
+ default=uuid.uuid4,
+ editable=False,
+ primary_key=True,
+ serialize=False,
+ ),
+ ),
+ (
+ "betrag",
+ models.DecimalField(
+ decimal_places=2, max_digits=12, verbose_name="Betrag (€)"
+ ),
+ ),
+ ("faellig_am", models.DateField(verbose_name="Fällig am")),
+ (
+ "status",
+ models.CharField(
+ choices=[
+ ("geplant", "Geplant"),
+ ("in_bearbeitung", "In Bearbeitung"),
+ ("ausgezahlt", "Ausgezahlt"),
+ ("storniert", "Storniert"),
+ ],
+ default="geplant",
+ max_length=20,
+ verbose_name="Status",
+ ),
+ ),
+ (
+ "beschreibung",
+ models.CharField(
+ blank=True, max_length=255, verbose_name="Beschreibung"
+ ),
+ ),
+ ("erstellt_am", models.DateTimeField(auto_now_add=True)),
+ ("aktualisiert_am", models.DateTimeField(auto_now=True)),
+ (
+ "destinataer",
+ models.ForeignKey(
+ on_delete=django.db.models.deletion.CASCADE,
+ related_name="unterstuetzungen",
+ to="stiftung.destinataer",
+ verbose_name="Destinatär",
+ ),
+ ),
+ (
+ "konto",
+ models.ForeignKey(
+ on_delete=django.db.models.deletion.PROTECT,
+ to="stiftung.stiftungskonto",
+ verbose_name="Zahlungskonto",
+ ),
+ ),
],
options={
- 'verbose_name': 'Destinatärunterstützung',
- 'verbose_name_plural': 'Destinatärunterstützungen',
- 'ordering': ['-faellig_am', '-erstellt_am'],
- 'indexes': [models.Index(fields=['status', 'faellig_am'], name='stiftung_de_status_1e9799_idx'), models.Index(fields=['destinataer', 'status'], name='stiftung_de_destina_ba7286_idx')],
+ "verbose_name": "Destinatärunterstützung",
+ "verbose_name_plural": "Destinatärunterstützungen",
+ "ordering": ["-faellig_am", "-erstellt_am"],
+ "indexes": [
+ models.Index(
+ fields=["status", "faellig_am"],
+ name="stiftung_de_status_1e9799_idx",
+ ),
+ models.Index(
+ fields=["destinataer", "status"],
+ name="stiftung_de_destina_ba7286_idx",
+ ),
+ ],
},
),
]
diff --git a/app/stiftung/migrations/0020_destinataernotiz.py b/app/stiftung/migrations/0020_destinataernotiz.py
index 78bd15d..32a2668 100644
--- a/app/stiftung/migrations/0020_destinataernotiz.py
+++ b/app/stiftung/migrations/0020_destinataernotiz.py
@@ -1,7 +1,8 @@
# Generated by Django 5.0.6 on 2025-08-29 16:05
-import django.db.models.deletion
import uuid
+
+import django.db.models.deletion
from django.conf import settings
from django.db import migrations, models
@@ -9,26 +10,65 @@ from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
- ('stiftung', '0019_destinataerunterstuetzung'),
+ ("stiftung", "0019_destinataerunterstuetzung"),
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]
operations = [
migrations.CreateModel(
- name='DestinataerNotiz',
+ name="DestinataerNotiz",
fields=[
- ('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)),
- ('titel', models.CharField(blank=True, max_length=200, verbose_name='Titel')),
- ('text', models.TextField(blank=True, verbose_name='Notiz')),
- ('datei', models.FileField(blank=True, null=True, upload_to='destinataer_notizen/', verbose_name='Anhang')),
- ('erstellt_am', models.DateTimeField(auto_now_add=True, verbose_name='Erstellt am')),
- ('destinataer', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='notizen_eintraege', to='stiftung.destinataer', verbose_name='Destinatär')),
- ('erstellt_von', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to=settings.AUTH_USER_MODEL, verbose_name='Erstellt von')),
+ (
+ "id",
+ models.UUIDField(
+ default=uuid.uuid4,
+ editable=False,
+ primary_key=True,
+ serialize=False,
+ ),
+ ),
+ (
+ "titel",
+ models.CharField(blank=True, max_length=200, verbose_name="Titel"),
+ ),
+ ("text", models.TextField(blank=True, verbose_name="Notiz")),
+ (
+ "datei",
+ models.FileField(
+ blank=True,
+ null=True,
+ upload_to="destinataer_notizen/",
+ verbose_name="Anhang",
+ ),
+ ),
+ (
+ "erstellt_am",
+ models.DateTimeField(auto_now_add=True, verbose_name="Erstellt am"),
+ ),
+ (
+ "destinataer",
+ models.ForeignKey(
+ on_delete=django.db.models.deletion.CASCADE,
+ related_name="notizen_eintraege",
+ to="stiftung.destinataer",
+ verbose_name="Destinatär",
+ ),
+ ),
+ (
+ "erstellt_von",
+ models.ForeignKey(
+ blank=True,
+ null=True,
+ on_delete=django.db.models.deletion.SET_NULL,
+ to=settings.AUTH_USER_MODEL,
+ verbose_name="Erstellt von",
+ ),
+ ),
],
options={
- 'verbose_name': 'Destinatär-Notiz',
- 'verbose_name_plural': 'Destinatär-Notizen',
- 'ordering': ['-erstellt_am'],
+ "verbose_name": "Destinatär-Notiz",
+ "verbose_name_plural": "Destinatär-Notizen",
+ "ordering": ["-erstellt_am"],
},
),
]
diff --git a/app/stiftung/migrations/0021_land_adresse_land_aktueller_paechter_and_more.py b/app/stiftung/migrations/0021_land_adresse_land_aktueller_paechter_and_more.py
index 657e16b..5a8febf 100644
--- a/app/stiftung/migrations/0021_land_adresse_land_aktueller_paechter_and_more.py
+++ b/app/stiftung/migrations/0021_land_adresse_land_aktueller_paechter_and_more.py
@@ -1,134 +1,354 @@
# Generated by Django 5.0.6 on 2025-08-30 14:20
+import uuid
+
import django.core.validators
import django.db.models.deletion
-import uuid
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
- ('stiftung', '0020_destinataernotiz'),
+ ("stiftung", "0020_destinataernotiz"),
]
operations = [
migrations.AddField(
- model_name='land',
- name='adresse',
- field=models.CharField(blank=True, max_length=200, null=True, verbose_name='Adresse/Ortsangabe'),
+ model_name="land",
+ name="adresse",
+ field=models.CharField(
+ blank=True, max_length=200, null=True, verbose_name="Adresse/Ortsangabe"
+ ),
),
migrations.AddField(
- model_name='land',
- name='aktueller_paechter',
- field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='gepachtete_laendereien', to='stiftung.paechter', verbose_name='Aktueller Pächter'),
+ model_name="land",
+ name="aktueller_paechter",
+ field=models.ForeignKey(
+ blank=True,
+ null=True,
+ on_delete=django.db.models.deletion.SET_NULL,
+ related_name="gepachtete_laendereien",
+ to="stiftung.paechter",
+ verbose_name="Aktueller Pächter",
+ ),
),
migrations.AddField(
- model_name='land',
- name='grundbuchblatt',
- field=models.CharField(blank=True, max_length=50, null=True, verbose_name='Grundbuchblatt'),
+ model_name="land",
+ name="grundbuchblatt",
+ field=models.CharField(
+ blank=True, max_length=50, null=True, verbose_name="Grundbuchblatt"
+ ),
),
migrations.AddField(
- model_name='land',
- name='grundsteuer_umlage',
- field=models.BooleanField(default=True, verbose_name='Grundsteuer umlagefähig'),
+ model_name="land",
+ name="grundsteuer_umlage",
+ field=models.BooleanField(
+ default=True, verbose_name="Grundsteuer umlagefähig"
+ ),
),
migrations.AddField(
- model_name='land',
- name='jagdpacht_anteil_umlage',
- field=models.BooleanField(default=False, verbose_name='Jagdpachtanteile umlagefähig'),
+ model_name="land",
+ name="jagdpacht_anteil_umlage",
+ field=models.BooleanField(
+ default=False, verbose_name="Jagdpachtanteile umlagefähig"
+ ),
),
migrations.AddField(
- model_name='land',
- name='pachtbeginn',
- field=models.DateField(blank=True, null=True, verbose_name='Pachtbeginn'),
+ model_name="land",
+ name="pachtbeginn",
+ field=models.DateField(blank=True, null=True, verbose_name="Pachtbeginn"),
),
migrations.AddField(
- model_name='land',
- name='pachtende',
- field=models.DateField(blank=True, null=True, verbose_name='Pachtende'),
+ model_name="land",
+ name="pachtende",
+ field=models.DateField(blank=True, null=True, verbose_name="Pachtende"),
),
migrations.AddField(
- model_name='land',
- name='pachtzins_pauschal',
- field=models.DecimalField(blank=True, decimal_places=2, max_digits=12, null=True, validators=[django.core.validators.MinValueValidator(0)], verbose_name='Pachtzins pauschal/Jahr (€)'),
+ model_name="land",
+ name="pachtzins_pauschal",
+ field=models.DecimalField(
+ blank=True,
+ decimal_places=2,
+ max_digits=12,
+ null=True,
+ validators=[django.core.validators.MinValueValidator(0)],
+ verbose_name="Pachtzins pauschal/Jahr (€)",
+ ),
),
migrations.AddField(
- model_name='land',
- name='pachtzins_pro_ha',
- field=models.DecimalField(blank=True, decimal_places=2, max_digits=12, null=True, validators=[django.core.validators.MinValueValidator(0)], verbose_name='Pachtzins pro ha (€)'),
+ model_name="land",
+ name="pachtzins_pro_ha",
+ field=models.DecimalField(
+ blank=True,
+ decimal_places=2,
+ max_digits=12,
+ null=True,
+ validators=[django.core.validators.MinValueValidator(0)],
+ verbose_name="Pachtzins pro ha (€)",
+ ),
),
migrations.AddField(
- model_name='land',
- name='paechter_anschrift',
- field=models.TextField(blank=True, null=True, verbose_name='Pächter Anschrift'),
+ model_name="land",
+ name="paechter_anschrift",
+ field=models.TextField(
+ blank=True, null=True, verbose_name="Pächter Anschrift"
+ ),
),
migrations.AddField(
- model_name='land',
- name='paechter_name',
- field=models.CharField(blank=True, max_length=150, null=True, verbose_name='Pächter Name'),
+ model_name="land",
+ name="paechter_name",
+ field=models.CharField(
+ blank=True, max_length=150, null=True, verbose_name="Pächter Name"
+ ),
),
migrations.AddField(
- model_name='land',
- name='ust_option',
- field=models.BooleanField(default=False, verbose_name='USt-Option'),
+ model_name="land",
+ name="ust_option",
+ field=models.BooleanField(default=False, verbose_name="USt-Option"),
),
migrations.AddField(
- model_name='land',
- name='ust_satz',
- field=models.DecimalField(decimal_places=2, default=19.0, max_digits=4, verbose_name='USt-Satz (%)'),
+ model_name="land",
+ name="ust_satz",
+ field=models.DecimalField(
+ decimal_places=2,
+ default=19.0,
+ max_digits=4,
+ verbose_name="USt-Satz (%)",
+ ),
),
migrations.AddField(
- model_name='land',
- name='verbandsbeitraege_umlage',
- field=models.BooleanField(default=True, verbose_name='Verbandsbeiträge umlagefähig'),
+ model_name="land",
+ name="verbandsbeitraege_umlage",
+ field=models.BooleanField(
+ default=True, verbose_name="Verbandsbeiträge umlagefähig"
+ ),
),
migrations.AddField(
- model_name='land',
- name='verlaengerung_klausel',
- field=models.BooleanField(default=False, verbose_name='Automatische Verlängerung'),
+ model_name="land",
+ name="verlaengerung_klausel",
+ field=models.BooleanField(
+ default=False, verbose_name="Automatische Verlängerung"
+ ),
),
migrations.AddField(
- model_name='land',
- name='versicherungen_umlage',
- field=models.BooleanField(default=True, verbose_name='Versicherungen umlagefähig'),
+ model_name="land",
+ name="versicherungen_umlage",
+ field=models.BooleanField(
+ default=True, verbose_name="Versicherungen umlagefähig"
+ ),
),
migrations.AddField(
- model_name='land',
- name='zahlungsweise',
- field=models.CharField(choices=[('jaehrlich', 'Jährlich'), ('halbjaehrlich', 'Halbjährlich'), ('vierteljaehrlich', 'Vierteljährlich'), ('monatlich', 'Monatlich')], default='jaehrlich', max_length=20, verbose_name='Zahlungsweise'),
+ model_name="land",
+ name="zahlungsweise",
+ field=models.CharField(
+ choices=[
+ ("jaehrlich", "Jährlich"),
+ ("halbjaehrlich", "Halbjährlich"),
+ ("vierteljaehrlich", "Vierteljährlich"),
+ ("monatlich", "Monatlich"),
+ ],
+ default="jaehrlich",
+ max_length=20,
+ verbose_name="Zahlungsweise",
+ ),
),
migrations.CreateModel(
- name='LandAbrechnung',
+ name="LandAbrechnung",
fields=[
- ('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)),
- ('abrechnungsjahr', models.IntegerField(validators=[django.core.validators.MinValueValidator(2000)], verbose_name='Abrechnungsjahr')),
- ('pacht_vereinnahmt', models.DecimalField(decimal_places=2, default=0, max_digits=12, validators=[django.core.validators.MinValueValidator(0)], verbose_name='Pacht vereinnahmt (€)')),
- ('umlagen_vereinnahmt', models.DecimalField(decimal_places=2, default=0, max_digits=12, validators=[django.core.validators.MinValueValidator(0)], verbose_name='Umlagen vereinnahmt (€)')),
- ('sonstige_einnahmen', models.DecimalField(decimal_places=2, default=0, max_digits=12, validators=[django.core.validators.MinValueValidator(0)], verbose_name='Sonstige Einnahmen (€)')),
- ('zahlungen', models.JSONField(blank=True, help_text='Liste von Objekten {datum, betrag, art}', null=True, verbose_name='Zahlungstermine')),
- ('grundsteuer_bescheid_nr', models.CharField(blank=True, max_length=80, null=True, verbose_name='Grundsteuer-Bescheid Nr.')),
- ('grundsteuer_betrag', models.DecimalField(decimal_places=2, default=0, max_digits=12, validators=[django.core.validators.MinValueValidator(0)], verbose_name='Grundsteuer Betrag (€)')),
- ('versicherungen_betrag', models.DecimalField(decimal_places=2, default=0, max_digits=12, validators=[django.core.validators.MinValueValidator(0)], verbose_name='Versicherungen Betrag (€)')),
- ('verbandsbeitraege_betrag', models.DecimalField(decimal_places=2, default=0, max_digits=12, validators=[django.core.validators.MinValueValidator(0)], verbose_name='Verbandsbeiträge Betrag (€)')),
- ('sonstige_abgaben_betrag', models.DecimalField(decimal_places=2, default=0, max_digits=12, validators=[django.core.validators.MinValueValidator(0)], verbose_name='Sonstige öffentliche Abgaben (€)')),
- ('instandhaltung_betrag', models.DecimalField(decimal_places=2, default=0, max_digits=12, validators=[django.core.validators.MinValueValidator(0)], verbose_name='Instandhaltung/Reparaturen (€)')),
- ('verwaltung_recht_betrag', models.DecimalField(decimal_places=2, default=0, max_digits=12, validators=[django.core.validators.MinValueValidator(0)], verbose_name='Verwaltung/Recht (€)')),
- ('vorsteuer_aus_umlagen', models.DecimalField(decimal_places=2, default=0, max_digits=12, validators=[django.core.validators.MinValueValidator(0)], verbose_name='Vorsteuer aus umgelegten Kosten (€)')),
- ('offene_posten', models.DecimalField(decimal_places=2, default=0, max_digits=12, verbose_name='Offene Posten (€)')),
- ('bemerkungen', models.TextField(blank=True, null=True, verbose_name='Bemerkungen Abrechnung')),
- ('pachtvertrag_datei', models.FileField(blank=True, null=True, upload_to='land_abrechnungen/vertraege/', verbose_name='Pachtvertrag (Datei)')),
- ('grundsteuer_bescheid_datei', models.FileField(blank=True, null=True, upload_to='land_abrechnungen/bescheide/', verbose_name='Grundsteuerbescheid (Datei)')),
- ('versicherungsnachweis_datei', models.FileField(blank=True, null=True, upload_to='land_abrechnungen/versicherungen/', verbose_name='Versicherungsnachweis (Datei)')),
- ('erstellt_am', models.DateTimeField(auto_now_add=True)),
- ('aktualisiert_am', models.DateTimeField(auto_now=True)),
- ('land', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='abrechnungen', to='stiftung.land', verbose_name='Länderei')),
+ (
+ "id",
+ models.UUIDField(
+ default=uuid.uuid4,
+ editable=False,
+ primary_key=True,
+ serialize=False,
+ ),
+ ),
+ (
+ "abrechnungsjahr",
+ models.IntegerField(
+ validators=[django.core.validators.MinValueValidator(2000)],
+ verbose_name="Abrechnungsjahr",
+ ),
+ ),
+ (
+ "pacht_vereinnahmt",
+ models.DecimalField(
+ decimal_places=2,
+ default=0,
+ max_digits=12,
+ validators=[django.core.validators.MinValueValidator(0)],
+ verbose_name="Pacht vereinnahmt (€)",
+ ),
+ ),
+ (
+ "umlagen_vereinnahmt",
+ models.DecimalField(
+ decimal_places=2,
+ default=0,
+ max_digits=12,
+ validators=[django.core.validators.MinValueValidator(0)],
+ verbose_name="Umlagen vereinnahmt (€)",
+ ),
+ ),
+ (
+ "sonstige_einnahmen",
+ models.DecimalField(
+ decimal_places=2,
+ default=0,
+ max_digits=12,
+ validators=[django.core.validators.MinValueValidator(0)],
+ verbose_name="Sonstige Einnahmen (€)",
+ ),
+ ),
+ (
+ "zahlungen",
+ models.JSONField(
+ blank=True,
+ help_text="Liste von Objekten {datum, betrag, art}",
+ null=True,
+ verbose_name="Zahlungstermine",
+ ),
+ ),
+ (
+ "grundsteuer_bescheid_nr",
+ models.CharField(
+ blank=True,
+ max_length=80,
+ null=True,
+ verbose_name="Grundsteuer-Bescheid Nr.",
+ ),
+ ),
+ (
+ "grundsteuer_betrag",
+ models.DecimalField(
+ decimal_places=2,
+ default=0,
+ max_digits=12,
+ validators=[django.core.validators.MinValueValidator(0)],
+ verbose_name="Grundsteuer Betrag (€)",
+ ),
+ ),
+ (
+ "versicherungen_betrag",
+ models.DecimalField(
+ decimal_places=2,
+ default=0,
+ max_digits=12,
+ validators=[django.core.validators.MinValueValidator(0)],
+ verbose_name="Versicherungen Betrag (€)",
+ ),
+ ),
+ (
+ "verbandsbeitraege_betrag",
+ models.DecimalField(
+ decimal_places=2,
+ default=0,
+ max_digits=12,
+ validators=[django.core.validators.MinValueValidator(0)],
+ verbose_name="Verbandsbeiträge Betrag (€)",
+ ),
+ ),
+ (
+ "sonstige_abgaben_betrag",
+ models.DecimalField(
+ decimal_places=2,
+ default=0,
+ max_digits=12,
+ validators=[django.core.validators.MinValueValidator(0)],
+ verbose_name="Sonstige öffentliche Abgaben (€)",
+ ),
+ ),
+ (
+ "instandhaltung_betrag",
+ models.DecimalField(
+ decimal_places=2,
+ default=0,
+ max_digits=12,
+ validators=[django.core.validators.MinValueValidator(0)],
+ verbose_name="Instandhaltung/Reparaturen (€)",
+ ),
+ ),
+ (
+ "verwaltung_recht_betrag",
+ models.DecimalField(
+ decimal_places=2,
+ default=0,
+ max_digits=12,
+ validators=[django.core.validators.MinValueValidator(0)],
+ verbose_name="Verwaltung/Recht (€)",
+ ),
+ ),
+ (
+ "vorsteuer_aus_umlagen",
+ models.DecimalField(
+ decimal_places=2,
+ default=0,
+ max_digits=12,
+ validators=[django.core.validators.MinValueValidator(0)],
+ verbose_name="Vorsteuer aus umgelegten Kosten (€)",
+ ),
+ ),
+ (
+ "offene_posten",
+ models.DecimalField(
+ decimal_places=2,
+ default=0,
+ max_digits=12,
+ verbose_name="Offene Posten (€)",
+ ),
+ ),
+ (
+ "bemerkungen",
+ models.TextField(
+ blank=True, null=True, verbose_name="Bemerkungen Abrechnung"
+ ),
+ ),
+ (
+ "pachtvertrag_datei",
+ models.FileField(
+ blank=True,
+ null=True,
+ upload_to="land_abrechnungen/vertraege/",
+ verbose_name="Pachtvertrag (Datei)",
+ ),
+ ),
+ (
+ "grundsteuer_bescheid_datei",
+ models.FileField(
+ blank=True,
+ null=True,
+ upload_to="land_abrechnungen/bescheide/",
+ verbose_name="Grundsteuerbescheid (Datei)",
+ ),
+ ),
+ (
+ "versicherungsnachweis_datei",
+ models.FileField(
+ blank=True,
+ null=True,
+ upload_to="land_abrechnungen/versicherungen/",
+ verbose_name="Versicherungsnachweis (Datei)",
+ ),
+ ),
+ ("erstellt_am", models.DateTimeField(auto_now_add=True)),
+ ("aktualisiert_am", models.DateTimeField(auto_now=True)),
+ (
+ "land",
+ models.ForeignKey(
+ on_delete=django.db.models.deletion.CASCADE,
+ related_name="abrechnungen",
+ to="stiftung.land",
+ verbose_name="Länderei",
+ ),
+ ),
],
options={
- 'verbose_name': 'Landabrechnung',
- 'verbose_name_plural': 'Landabrechnungen',
- 'ordering': ['-abrechnungsjahr', 'land__gemeinde', 'land__gemarkung'],
- 'unique_together': {('land', 'abrechnungsjahr')},
+ "verbose_name": "Landabrechnung",
+ "verbose_name_plural": "Landabrechnungen",
+ "ordering": ["-abrechnungsjahr", "land__gemeinde", "land__gemarkung"],
+ "unique_together": {("land", "abrechnungsjahr")},
},
),
]
diff --git a/app/stiftung/migrations/0022_dokumentlink_land_verpachtung_id_and_more.py b/app/stiftung/migrations/0022_dokumentlink_land_verpachtung_id_and_more.py
index 7bab9df..c5da371 100644
--- a/app/stiftung/migrations/0022_dokumentlink_land_verpachtung_id_and_more.py
+++ b/app/stiftung/migrations/0022_dokumentlink_land_verpachtung_id_and_more.py
@@ -1,57 +1,185 @@
# Generated by Django 5.0.6 on 2025-08-30 16:59
+import uuid
+
import django.core.validators
import django.db.models.deletion
-import uuid
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
- ('stiftung', '0021_land_adresse_land_aktueller_paechter_and_more'),
+ ("stiftung", "0021_land_adresse_land_aktueller_paechter_and_more"),
]
operations = [
migrations.AddField(
- model_name='dokumentlink',
- name='land_verpachtung_id',
- field=models.UUIDField(blank=True, null=True, verbose_name='Landverpachtung ID (Neu)'),
+ model_name="dokumentlink",
+ name="land_verpachtung_id",
+ field=models.UUIDField(
+ blank=True, null=True, verbose_name="Landverpachtung ID (Neu)"
+ ),
),
migrations.AlterField(
- model_name='dokumentlink',
- name='verpachtung_id',
- field=models.UUIDField(blank=True, null=True, verbose_name='Verpachtung ID (Legacy)'),
+ model_name="dokumentlink",
+ name="verpachtung_id",
+ field=models.UUIDField(
+ blank=True, null=True, verbose_name="Verpachtung ID (Legacy)"
+ ),
),
migrations.CreateModel(
- name='LandVerpachtung',
+ name="LandVerpachtung",
fields=[
- ('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)),
- ('vertragsnummer', models.CharField(max_length=50, unique=True, verbose_name='Vertragsnummer')),
- ('pachtbeginn', models.DateField(verbose_name='Pachtbeginn')),
- ('pachtende', models.DateField(blank=True, null=True, verbose_name='Pachtende')),
- ('verlaengerung_klausel', models.BooleanField(default=False, verbose_name='Automatische Verlängerung')),
- ('verpachtete_flaeche', models.DecimalField(decimal_places=2, max_digits=12, validators=[django.core.validators.MinValueValidator(0.01)], verbose_name='Verpachtete Fläche (qm)')),
- ('pachtzins_pauschal', models.DecimalField(decimal_places=2, max_digits=12, validators=[django.core.validators.MinValueValidator(0)], verbose_name='Pachtzins pauschal/Jahr (€)')),
- ('pachtzins_pro_ha', models.DecimalField(blank=True, decimal_places=2, max_digits=12, null=True, validators=[django.core.validators.MinValueValidator(0)], verbose_name='Pachtzins pro ha (€)')),
- ('zahlungsweise', models.CharField(choices=[('jaehrlich', 'Jährlich'), ('halbjaehrlich', 'Halbjährlich'), ('vierteljaehrlich', 'Vierteljährlich'), ('monatlich', 'Monatlich')], default='jaehrlich', max_length=20, verbose_name='Zahlungsweise')),
- ('ust_option', models.BooleanField(default=False, verbose_name='USt-Option')),
- ('ust_satz', models.DecimalField(decimal_places=2, default=19.0, max_digits=4, verbose_name='USt-Satz (%)')),
- ('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', models.CharField(choices=[('aktiv', 'Aktiv'), ('beendet', 'Beendet'), ('gekuendigt', 'Gekündigt'), ('verlängert', 'Verlängert')], default='aktiv', max_length=20, verbose_name='Status')),
- ('bemerkungen', models.TextField(blank=True, null=True, verbose_name='Bemerkungen')),
- ('erstellt_am', models.DateTimeField(auto_now_add=True)),
- ('aktualisiert_am', models.DateTimeField(auto_now=True)),
- ('land', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='neue_verpachtungen', to='stiftung.land', verbose_name='Länderei')),
- ('paechter', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='neue_verpachtungen', to='stiftung.paechter', verbose_name='Pächter')),
+ (
+ "id",
+ models.UUIDField(
+ default=uuid.uuid4,
+ editable=False,
+ primary_key=True,
+ serialize=False,
+ ),
+ ),
+ (
+ "vertragsnummer",
+ models.CharField(
+ max_length=50, unique=True, verbose_name="Vertragsnummer"
+ ),
+ ),
+ ("pachtbeginn", models.DateField(verbose_name="Pachtbeginn")),
+ (
+ "pachtende",
+ models.DateField(blank=True, null=True, verbose_name="Pachtende"),
+ ),
+ (
+ "verlaengerung_klausel",
+ models.BooleanField(
+ default=False, verbose_name="Automatische Verlängerung"
+ ),
+ ),
+ (
+ "verpachtete_flaeche",
+ models.DecimalField(
+ decimal_places=2,
+ max_digits=12,
+ validators=[django.core.validators.MinValueValidator(0.01)],
+ verbose_name="Verpachtete Fläche (qm)",
+ ),
+ ),
+ (
+ "pachtzins_pauschal",
+ models.DecimalField(
+ decimal_places=2,
+ max_digits=12,
+ validators=[django.core.validators.MinValueValidator(0)],
+ verbose_name="Pachtzins pauschal/Jahr (€)",
+ ),
+ ),
+ (
+ "pachtzins_pro_ha",
+ models.DecimalField(
+ blank=True,
+ decimal_places=2,
+ max_digits=12,
+ null=True,
+ validators=[django.core.validators.MinValueValidator(0)],
+ verbose_name="Pachtzins pro ha (€)",
+ ),
+ ),
+ (
+ "zahlungsweise",
+ models.CharField(
+ choices=[
+ ("jaehrlich", "Jährlich"),
+ ("halbjaehrlich", "Halbjährlich"),
+ ("vierteljaehrlich", "Vierteljährlich"),
+ ("monatlich", "Monatlich"),
+ ],
+ default="jaehrlich",
+ max_length=20,
+ verbose_name="Zahlungsweise",
+ ),
+ ),
+ (
+ "ust_option",
+ models.BooleanField(default=False, verbose_name="USt-Option"),
+ ),
+ (
+ "ust_satz",
+ models.DecimalField(
+ decimal_places=2,
+ default=19.0,
+ max_digits=4,
+ verbose_name="USt-Satz (%)",
+ ),
+ ),
+ (
+ "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",
+ models.CharField(
+ choices=[
+ ("aktiv", "Aktiv"),
+ ("beendet", "Beendet"),
+ ("gekuendigt", "Gekündigt"),
+ ("verlängert", "Verlängert"),
+ ],
+ default="aktiv",
+ max_length=20,
+ verbose_name="Status",
+ ),
+ ),
+ (
+ "bemerkungen",
+ models.TextField(blank=True, null=True, verbose_name="Bemerkungen"),
+ ),
+ ("erstellt_am", models.DateTimeField(auto_now_add=True)),
+ ("aktualisiert_am", models.DateTimeField(auto_now=True)),
+ (
+ "land",
+ models.ForeignKey(
+ on_delete=django.db.models.deletion.CASCADE,
+ related_name="neue_verpachtungen",
+ to="stiftung.land",
+ verbose_name="Länderei",
+ ),
+ ),
+ (
+ "paechter",
+ models.ForeignKey(
+ on_delete=django.db.models.deletion.CASCADE,
+ related_name="neue_verpachtungen",
+ to="stiftung.paechter",
+ verbose_name="Pächter",
+ ),
+ ),
],
options={
- 'verbose_name': 'Landverpachtung',
- 'verbose_name_plural': 'Landverpachtungen',
- 'ordering': ['-pachtbeginn', 'land'],
+ "verbose_name": "Landverpachtung",
+ "verbose_name_plural": "Landverpachtungen",
+ "ordering": ["-pachtbeginn", "land"],
},
),
]
diff --git a/app/stiftung/migrations/0023_remove_legacy_verpachtung.py b/app/stiftung/migrations/0023_remove_legacy_verpachtung.py
index e99e416..1f2ed89 100644
--- a/app/stiftung/migrations/0023_remove_legacy_verpachtung.py
+++ b/app/stiftung/migrations/0023_remove_legacy_verpachtung.py
@@ -6,11 +6,11 @@ from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
- ('stiftung', '0022_dokumentlink_land_verpachtung_id_and_more'),
+ ("stiftung", "0022_dokumentlink_land_verpachtung_id_and_more"),
]
operations = [
migrations.DeleteModel(
- name='Verpachtung',
+ name="Verpachtung",
),
]
diff --git a/app/stiftung/migrations/0024_dokumentlink_abrechnung_id.py b/app/stiftung/migrations/0024_dokumentlink_abrechnung_id.py
index 7dfb952..1b34321 100644
--- a/app/stiftung/migrations/0024_dokumentlink_abrechnung_id.py
+++ b/app/stiftung/migrations/0024_dokumentlink_abrechnung_id.py
@@ -6,13 +6,13 @@ from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
- ('stiftung', '0023_remove_legacy_verpachtung'),
+ ("stiftung", "0023_remove_legacy_verpachtung"),
]
operations = [
migrations.AddField(
- model_name='dokumentlink',
- name='abrechnung_id',
- field=models.UUIDField(blank=True, null=True, verbose_name='Abrechnung ID'),
+ model_name="dokumentlink",
+ name="abrechnung_id",
+ field=models.UUIDField(blank=True, null=True, verbose_name="Abrechnung ID"),
),
]
diff --git a/app/stiftung/migrations/0025_appconfiguration.py b/app/stiftung/migrations/0025_appconfiguration.py
index fc33980..3aa5d18 100644
--- a/app/stiftung/migrations/0025_appconfiguration.py
+++ b/app/stiftung/migrations/0025_appconfiguration.py
@@ -1,37 +1,90 @@
# Generated by Django 5.0.6 on 2025-08-31 22:08
import uuid
+
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
- ('stiftung', '0024_dokumentlink_abrechnung_id'),
+ ("stiftung", "0024_dokumentlink_abrechnung_id"),
]
operations = [
migrations.CreateModel(
- name='AppConfiguration',
+ name="AppConfiguration",
fields=[
- ('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)),
- ('key', models.CharField(max_length=100, unique=True, verbose_name='Setting Key')),
- ('display_name', models.CharField(max_length=200, verbose_name='Display Name')),
- ('description', models.TextField(blank=True, null=True, verbose_name='Description')),
- ('value', models.TextField(verbose_name='Value')),
- ('default_value', models.TextField(verbose_name='Default Value')),
- ('setting_type', models.CharField(choices=[('text', 'Text'), ('number', 'Number'), ('boolean', 'Boolean'), ('url', 'URL'), ('tag', 'Tag Name'), ('tag_id', 'Tag ID')], default='text', max_length=20, verbose_name='Type')),
- ('category', models.CharField(choices=[('paperless', 'Paperless Integration'), ('general', 'General Settings'), ('notifications', 'Notifications'), ('system', 'System Settings')], default='general', max_length=50, verbose_name='Category')),
- ('is_active', models.BooleanField(default=True, verbose_name='Active')),
- ('is_system', models.BooleanField(default=False, verbose_name='System Setting (read-only)')),
- ('order', models.IntegerField(default=0, verbose_name='Display Order')),
- ('created_at', models.DateTimeField(auto_now_add=True)),
- ('updated_at', models.DateTimeField(auto_now=True)),
+ (
+ "id",
+ models.UUIDField(
+ default=uuid.uuid4,
+ editable=False,
+ primary_key=True,
+ serialize=False,
+ ),
+ ),
+ (
+ "key",
+ models.CharField(
+ max_length=100, unique=True, verbose_name="Setting Key"
+ ),
+ ),
+ (
+ "display_name",
+ models.CharField(max_length=200, verbose_name="Display Name"),
+ ),
+ (
+ "description",
+ models.TextField(blank=True, null=True, verbose_name="Description"),
+ ),
+ ("value", models.TextField(verbose_name="Value")),
+ ("default_value", models.TextField(verbose_name="Default Value")),
+ (
+ "setting_type",
+ models.CharField(
+ choices=[
+ ("text", "Text"),
+ ("number", "Number"),
+ ("boolean", "Boolean"),
+ ("url", "URL"),
+ ("tag", "Tag Name"),
+ ("tag_id", "Tag ID"),
+ ],
+ default="text",
+ max_length=20,
+ verbose_name="Type",
+ ),
+ ),
+ (
+ "category",
+ models.CharField(
+ choices=[
+ ("paperless", "Paperless Integration"),
+ ("general", "General Settings"),
+ ("notifications", "Notifications"),
+ ("system", "System Settings"),
+ ],
+ default="general",
+ max_length=50,
+ verbose_name="Category",
+ ),
+ ),
+ ("is_active", models.BooleanField(default=True, verbose_name="Active")),
+ (
+ "is_system",
+ models.BooleanField(
+ default=False, verbose_name="System Setting (read-only)"
+ ),
+ ),
+ ("order", models.IntegerField(default=0, verbose_name="Display Order")),
+ ("created_at", models.DateTimeField(auto_now_add=True)),
+ ("updated_at", models.DateTimeField(auto_now=True)),
],
options={
- 'verbose_name': 'App Configuration',
- 'verbose_name_plural': 'App Configurations',
- 'ordering': ['category', 'order', 'display_name'],
+ "verbose_name": "App Configuration",
+ "verbose_name_plural": "App Configurations",
+ "ordering": ["category", "order", "display_name"],
},
),
]
diff --git a/app/stiftung/migrations/0026_enhance_unterstuetzung_model.py b/app/stiftung/migrations/0026_enhance_unterstuetzung_model.py
index c043555..d7504dd 100644
--- a/app/stiftung/migrations/0026_enhance_unterstuetzung_model.py
+++ b/app/stiftung/migrations/0026_enhance_unterstuetzung_model.py
@@ -1,7 +1,8 @@
# Generated by Django 5.0.6 on 2025-09-01 20:03
-import django.db.models.deletion
import uuid
+
+import django.db.models.deletion
from django.conf import settings
from django.db import migrations, models
@@ -9,81 +10,192 @@ from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
- ('stiftung', '0025_appconfiguration'),
+ ("stiftung", "0025_appconfiguration"),
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]
operations = [
migrations.AddField(
- model_name='destinataerunterstuetzung',
- name='ausgezahlt_am',
- field=models.DateField(blank=True, null=True, verbose_name='Ausgezahlt am'),
+ model_name="destinataerunterstuetzung",
+ name="ausgezahlt_am",
+ field=models.DateField(blank=True, null=True, verbose_name="Ausgezahlt am"),
),
migrations.AddField(
- model_name='destinataerunterstuetzung',
- name='ausgezahlt_von',
- field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to=settings.AUTH_USER_MODEL, verbose_name='Ausgezahlt von'),
+ model_name="destinataerunterstuetzung",
+ name="ausgezahlt_von",
+ field=models.ForeignKey(
+ blank=True,
+ null=True,
+ on_delete=django.db.models.deletion.SET_NULL,
+ to=settings.AUTH_USER_MODEL,
+ verbose_name="Ausgezahlt von",
+ ),
),
migrations.AddField(
- model_name='destinataerunterstuetzung',
- name='empfaenger_iban',
- field=models.CharField(blank=True, max_length=34, verbose_name='Empfänger IBAN'),
+ model_name="destinataerunterstuetzung",
+ name="empfaenger_iban",
+ field=models.CharField(
+ blank=True, max_length=34, verbose_name="Empfänger IBAN"
+ ),
),
migrations.AddField(
- model_name='destinataerunterstuetzung',
- name='empfaenger_name',
- field=models.CharField(blank=True, max_length=200, verbose_name='Empfänger Name'),
+ model_name="destinataerunterstuetzung",
+ name="empfaenger_name",
+ field=models.CharField(
+ blank=True, max_length=200, verbose_name="Empfänger Name"
+ ),
),
migrations.AddField(
- model_name='destinataerunterstuetzung',
- name='verwendungszweck',
- field=models.CharField(blank=True, max_length=140, verbose_name='Verwendungszweck'),
+ model_name="destinataerunterstuetzung",
+ name="verwendungszweck",
+ field=models.CharField(
+ blank=True, max_length=140, verbose_name="Verwendungszweck"
+ ),
),
migrations.AlterField(
- model_name='destinataerunterstuetzung',
- name='status',
- field=models.CharField(choices=[('geplant', 'Geplant'), ('faellig', 'Fällig'), ('in_bearbeitung', 'In Bearbeitung'), ('ausgezahlt', 'Ausgezahlt'), ('storniert', 'Storniert')], default='geplant', max_length=20, verbose_name='Status'),
+ model_name="destinataerunterstuetzung",
+ name="status",
+ field=models.CharField(
+ choices=[
+ ("geplant", "Geplant"),
+ ("faellig", "Fällig"),
+ ("in_bearbeitung", "In Bearbeitung"),
+ ("ausgezahlt", "Ausgezahlt"),
+ ("storniert", "Storniert"),
+ ],
+ default="geplant",
+ max_length=20,
+ verbose_name="Status",
+ ),
),
migrations.CreateModel(
- name='UnterstuetzungWiederkehrend',
+ name="UnterstuetzungWiederkehrend",
fields=[
- ('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)),
- ('betrag', models.DecimalField(decimal_places=2, max_digits=12, verbose_name='Betrag (€)')),
- ('intervall', models.CharField(choices=[('monatlich', 'Monatlich'), ('quartalsweise', 'Vierteljährlich'), ('halbjaehrlich', 'Halbjährlich'), ('jaehrlich', 'Jährlich')], max_length=20, verbose_name='Intervall')),
- ('beschreibung', models.CharField(blank=True, max_length=255, verbose_name='Beschreibung')),
- ('empfaenger_iban', models.CharField(max_length=34, verbose_name='Empfänger IBAN')),
- ('empfaenger_name', models.CharField(max_length=200, verbose_name='Empfänger Name')),
- ('verwendungszweck', models.CharField(blank=True, max_length=140, verbose_name='Verwendungszweck')),
- ('erste_zahlung_am', models.DateField(verbose_name='Erste Zahlung am')),
- ('letzte_zahlung_am', models.DateField(blank=True, null=True, verbose_name='Letzte Zahlung am (optional)')),
- ('naechste_generierung', models.DateField(verbose_name='Nächste Generierung')),
- ('aktiv', models.BooleanField(default=True, verbose_name='Aktiv')),
- ('erstellt_am', models.DateTimeField(auto_now_add=True)),
- ('destinataer', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='wiederkehrende_unterstuetzungen', to='stiftung.destinataer', verbose_name='Destinatär')),
- ('erstellt_von', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to=settings.AUTH_USER_MODEL, verbose_name='Erstellt von')),
- ('konto', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to='stiftung.stiftungskonto', verbose_name='Zahlungskonto')),
+ (
+ "id",
+ models.UUIDField(
+ default=uuid.uuid4,
+ editable=False,
+ primary_key=True,
+ serialize=False,
+ ),
+ ),
+ (
+ "betrag",
+ models.DecimalField(
+ decimal_places=2, max_digits=12, verbose_name="Betrag (€)"
+ ),
+ ),
+ (
+ "intervall",
+ models.CharField(
+ choices=[
+ ("monatlich", "Monatlich"),
+ ("quartalsweise", "Vierteljährlich"),
+ ("halbjaehrlich", "Halbjährlich"),
+ ("jaehrlich", "Jährlich"),
+ ],
+ max_length=20,
+ verbose_name="Intervall",
+ ),
+ ),
+ (
+ "beschreibung",
+ models.CharField(
+ blank=True, max_length=255, verbose_name="Beschreibung"
+ ),
+ ),
+ (
+ "empfaenger_iban",
+ models.CharField(max_length=34, verbose_name="Empfänger IBAN"),
+ ),
+ (
+ "empfaenger_name",
+ models.CharField(max_length=200, verbose_name="Empfänger Name"),
+ ),
+ (
+ "verwendungszweck",
+ models.CharField(
+ blank=True, max_length=140, verbose_name="Verwendungszweck"
+ ),
+ ),
+ ("erste_zahlung_am", models.DateField(verbose_name="Erste Zahlung am")),
+ (
+ "letzte_zahlung_am",
+ models.DateField(
+ blank=True,
+ null=True,
+ verbose_name="Letzte Zahlung am (optional)",
+ ),
+ ),
+ (
+ "naechste_generierung",
+ models.DateField(verbose_name="Nächste Generierung"),
+ ),
+ ("aktiv", models.BooleanField(default=True, verbose_name="Aktiv")),
+ ("erstellt_am", models.DateTimeField(auto_now_add=True)),
+ (
+ "destinataer",
+ models.ForeignKey(
+ on_delete=django.db.models.deletion.CASCADE,
+ related_name="wiederkehrende_unterstuetzungen",
+ to="stiftung.destinataer",
+ verbose_name="Destinatär",
+ ),
+ ),
+ (
+ "erstellt_von",
+ models.ForeignKey(
+ blank=True,
+ null=True,
+ on_delete=django.db.models.deletion.SET_NULL,
+ to=settings.AUTH_USER_MODEL,
+ verbose_name="Erstellt von",
+ ),
+ ),
+ (
+ "konto",
+ models.ForeignKey(
+ on_delete=django.db.models.deletion.PROTECT,
+ to="stiftung.stiftungskonto",
+ verbose_name="Zahlungskonto",
+ ),
+ ),
],
options={
- 'verbose_name': 'Wiederkehrende Unterstützung',
- 'verbose_name_plural': 'Wiederkehrende Unterstützungen',
- 'ordering': ['-erstellt_am'],
+ "verbose_name": "Wiederkehrende Unterstützung",
+ "verbose_name_plural": "Wiederkehrende Unterstützungen",
+ "ordering": ["-erstellt_am"],
},
),
migrations.AddField(
- model_name='destinataerunterstuetzung',
- name='wiederkehrend_von',
- field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='stiftung.unterstuetzungwiederkehrend', verbose_name='Wiederkehrende Zahlung'),
+ model_name="destinataerunterstuetzung",
+ name="wiederkehrend_von",
+ field=models.ForeignKey(
+ blank=True,
+ null=True,
+ on_delete=django.db.models.deletion.SET_NULL,
+ to="stiftung.unterstuetzungwiederkehrend",
+ verbose_name="Wiederkehrende Zahlung",
+ ),
),
migrations.AddIndex(
- model_name='destinataerunterstuetzung',
- index=models.Index(fields=['wiederkehrend_von'], name='stiftung_de_wiederk_3d5afc_idx'),
+ model_name="destinataerunterstuetzung",
+ index=models.Index(
+ fields=["wiederkehrend_von"], name="stiftung_de_wiederk_3d5afc_idx"
+ ),
),
migrations.AddIndex(
- model_name='unterstuetzungwiederkehrend',
- index=models.Index(fields=['aktiv', 'naechste_generierung'], name='stiftung_un_aktiv_b957e5_idx'),
+ model_name="unterstuetzungwiederkehrend",
+ index=models.Index(
+ fields=["aktiv", "naechste_generierung"],
+ name="stiftung_un_aktiv_b957e5_idx",
+ ),
),
migrations.AddIndex(
- model_name='unterstuetzungwiederkehrend',
- index=models.Index(fields=['destinataer', 'aktiv'], name='stiftung_un_destina_2232fc_idx'),
+ model_name="unterstuetzungwiederkehrend",
+ index=models.Index(
+ fields=["destinataer", "aktiv"], name="stiftung_un_destina_2232fc_idx"
+ ),
),
]
diff --git a/app/stiftung/migrations/0027_helpbox_alter_appconfiguration_category.py b/app/stiftung/migrations/0027_helpbox_alter_appconfiguration_category.py
index 6c55aca..8eff4b7 100644
--- a/app/stiftung/migrations/0027_helpbox_alter_appconfiguration_category.py
+++ b/app/stiftung/migrations/0027_helpbox_alter_appconfiguration_category.py
@@ -1,38 +1,106 @@
# Generated by Django 5.0.6 on 2025-09-02 19:56
import uuid
+
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
- ('stiftung', '0026_enhance_unterstuetzung_model'),
+ ("stiftung", "0026_enhance_unterstuetzung_model"),
]
operations = [
migrations.CreateModel(
- name='HelpBox',
+ name="HelpBox",
fields=[
- ('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)),
- ('page_key', models.CharField(choices=[('destinataer_new', 'Neuer Destinatär'), ('unterstuetzung_new', 'Neue Unterstützung'), ('foerderung_new', 'Neue Förderung'), ('paechter_new', 'Neuer Pächter'), ('laenderei_new', 'Neue Länderei'), ('verpachtung_new', 'Neue Verpachtung'), ('person_new', 'Neue Person'), ('konto_new', 'Neues Konto')], max_length=50, unique=True, verbose_name='Seite')),
- ('title', models.CharField(max_length=200, verbose_name='Titel der Hilfsbox')),
- ('content', models.TextField(help_text='Sie können Markdown verwenden: **fett**, *kursiv*, `code`, [Link](url), etc.', verbose_name='Inhalt (Markdown unterstützt)')),
- ('is_active', models.BooleanField(default=True, verbose_name='Aktiv')),
- ('created_at', models.DateTimeField(auto_now_add=True, verbose_name='Erstellt am')),
- ('updated_at', models.DateTimeField(auto_now=True, verbose_name='Aktualisiert am')),
- ('created_by', models.CharField(blank=True, max_length=100, null=True, verbose_name='Erstellt von')),
- ('updated_by', models.CharField(blank=True, max_length=100, null=True, verbose_name='Aktualisiert von')),
+ (
+ "id",
+ models.UUIDField(
+ default=uuid.uuid4,
+ editable=False,
+ primary_key=True,
+ serialize=False,
+ ),
+ ),
+ (
+ "page_key",
+ models.CharField(
+ choices=[
+ ("destinataer_new", "Neuer Destinatär"),
+ ("unterstuetzung_new", "Neue Unterstützung"),
+ ("foerderung_new", "Neue Förderung"),
+ ("paechter_new", "Neuer Pächter"),
+ ("laenderei_new", "Neue Länderei"),
+ ("verpachtung_new", "Neue Verpachtung"),
+ ("person_new", "Neue Person"),
+ ("konto_new", "Neues Konto"),
+ ],
+ max_length=50,
+ unique=True,
+ verbose_name="Seite",
+ ),
+ ),
+ (
+ "title",
+ models.CharField(max_length=200, verbose_name="Titel der Hilfsbox"),
+ ),
+ (
+ "content",
+ models.TextField(
+ help_text="Sie können Markdown verwenden: **fett**, *kursiv*, `code`, [Link](url), etc.",
+ verbose_name="Inhalt (Markdown unterstützt)",
+ ),
+ ),
+ ("is_active", models.BooleanField(default=True, verbose_name="Aktiv")),
+ (
+ "created_at",
+ models.DateTimeField(auto_now_add=True, verbose_name="Erstellt am"),
+ ),
+ (
+ "updated_at",
+ models.DateTimeField(auto_now=True, verbose_name="Aktualisiert am"),
+ ),
+ (
+ "created_by",
+ models.CharField(
+ blank=True,
+ max_length=100,
+ null=True,
+ verbose_name="Erstellt von",
+ ),
+ ),
+ (
+ "updated_by",
+ models.CharField(
+ blank=True,
+ max_length=100,
+ null=True,
+ verbose_name="Aktualisiert von",
+ ),
+ ),
],
options={
- 'verbose_name': 'Hilfs-Infobox',
- 'verbose_name_plural': 'Hilfs-Infoboxen',
- 'ordering': ['page_key'],
+ "verbose_name": "Hilfs-Infobox",
+ "verbose_name_plural": "Hilfs-Infoboxen",
+ "ordering": ["page_key"],
},
),
migrations.AlterField(
- model_name='appconfiguration',
- name='category',
- field=models.CharField(choices=[('paperless', 'Paperless Integration'), ('general', 'General Settings'), ('corporate', 'Corporate Identity'), ('notifications', 'Notifications'), ('system', 'System Settings')], default='general', max_length=50, verbose_name='Category'),
+ model_name="appconfiguration",
+ name="category",
+ field=models.CharField(
+ choices=[
+ ("paperless", "Paperless Integration"),
+ ("general", "General Settings"),
+ ("corporate", "Corporate Identity"),
+ ("notifications", "Notifications"),
+ ("system", "System Settings"),
+ ],
+ default="general",
+ max_length=50,
+ verbose_name="Category",
+ ),
),
]
diff --git a/app/stiftung/migrations/0028_alter_helpbox_page_key.py b/app/stiftung/migrations/0028_alter_helpbox_page_key.py
index 4b3885f..d15deff 100644
--- a/app/stiftung/migrations/0028_alter_helpbox_page_key.py
+++ b/app/stiftung/migrations/0028_alter_helpbox_page_key.py
@@ -6,13 +6,34 @@ from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
- ('stiftung', '0027_helpbox_alter_appconfiguration_category'),
+ ("stiftung", "0027_helpbox_alter_appconfiguration_category"),
]
operations = [
migrations.AlterField(
- model_name='helpbox',
- name='page_key',
- field=models.CharField(choices=[('destinataer_new', 'Neuer Destinatär'), ('unterstuetzung_new', 'Neue Unterstützung'), ('foerderung_new', 'Neue Förderung'), ('paechter_new', 'Neuer Pächter'), ('laenderei_new', 'Neue Länderei'), ('verpachtung_new', 'Neue Verpachtung'), ('land_abrechnung_new', 'Neue Landabrechnung'), ('person_new', 'Neue Person'), ('konto_new', 'Neues Konto'), ('verwaltungskosten_new', 'Neue Verwaltungskosten'), ('rentmeister_new', 'Neuer Rentmeister'), ('dokument_new', 'Neues Dokument'), ('user_new', 'Neuer Benutzer'), ('csv_import_new', 'CSV Import'), ('destinataer_notiz_new', 'Destinatär Notiz')], max_length=50, unique=True, verbose_name='Seite'),
+ model_name="helpbox",
+ name="page_key",
+ field=models.CharField(
+ choices=[
+ ("destinataer_new", "Neuer Destinatär"),
+ ("unterstuetzung_new", "Neue Unterstützung"),
+ ("foerderung_new", "Neue Förderung"),
+ ("paechter_new", "Neuer Pächter"),
+ ("laenderei_new", "Neue Länderei"),
+ ("verpachtung_new", "Neue Verpachtung"),
+ ("land_abrechnung_new", "Neue Landabrechnung"),
+ ("person_new", "Neue Person"),
+ ("konto_new", "Neues Konto"),
+ ("verwaltungskosten_new", "Neue Verwaltungskosten"),
+ ("rentmeister_new", "Neuer Rentmeister"),
+ ("dokument_new", "Neues Dokument"),
+ ("user_new", "Neuer Benutzer"),
+ ("csv_import_new", "CSV Import"),
+ ("destinataer_notiz_new", "Destinatär Notiz"),
+ ],
+ max_length=50,
+ unique=True,
+ verbose_name="Seite",
+ ),
),
]
diff --git a/app/stiftung/models.py b/app/stiftung/models.py
index 870d0eb..0db750b 100644
--- a/app/stiftung/models.py
+++ b/app/stiftung/models.py
@@ -1,113 +1,139 @@
-import uuid
import csv
+import uuid
from io import StringIO
-from django.db import models
-from django.core.validators import MinValueValidator, MaxValueValidator
-from django.utils import timezone
+
from dateutil.relativedelta import relativedelta
+from django.core.validators import MaxValueValidator, MinValueValidator
+from django.db import models
+from django.utils import timezone
+
from stiftung.utils.date_utils import ensure_date, get_year_from_date
+
class CSVImport(models.Model):
"""Track CSV import operations for audit purposes"""
-
+
IMPORT_TYPE_CHOICES = [
- ('destinataere', 'Destinatäre'),
- ('paechter', 'Pächter'),
- ('laendereien', 'Ländereien'),
- ('verpachtungen', 'Verpachtungen'),
- ('personen', 'Personen (Legacy)'),
+ ("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'),
+ ("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")
+ 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')
-
+ 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")
+ 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")
-
+ 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']
+ 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")
+ 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)
+ 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.)'),
+ ("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"
+ 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)"
+ 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")
-
+ 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")
@@ -115,130 +141,190 @@ class Paechter(models.Model):
class Meta:
verbose_name = "Pächter"
verbose_name_plural = "Pächter"
- ordering = ['nachname', 'vorname']
+ 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')
-
+ 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
-
+ 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
+ return (
+ self.neue_verpachtungen.filter(status="aktiv").aggregate(
+ total=models.Sum("pachtzins_pauschal")
+ )["total"]
+ or 0
+ )
+
class Destinataer(models.Model):
"""Destinatäre (Beneficiaries) für Förderungen"""
-
+
FAMILIENZWIG_CHOICES = [
- ('hauptzweig', 'Hauptzweig'),
- ('nebenzweig', 'Nebenzweig'),
- ('verwandt', 'Verwandt'),
- ('anderer', 'Anderer'),
+ ("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'),
+ ("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')
+ 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")
+ 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)
+ 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")
-
+ 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 (€)"
+ 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")
-
+ 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 (€)")
+ 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")
+ 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']
+ 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
-
+ 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()
+ return self.foerderung_set.order_by("-jahr", "-betrag").first()
def erfuellt_voraussetzungen(self):
"""Prüft die Unterstützungsvoraussetzungen gemäß Angaben.
@@ -249,17 +335,21 @@ class Destinataer(models.Model):
und + 0.8 * Regelsatz je weiterer Person approximiert.
"""
from decimal import Decimal
- regelsatz = Decimal('563.00')
+
+ regelsatz = Decimal("563.00")
basis = regelsatz * 5
- zuschlag = max(0, (self.haushaltsgroesse or 1) - 1) * (regelsatz * Decimal('0.80'))
+ 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')
+ 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)
@@ -275,17 +365,20 @@ class Destinataer(models.Model):
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'),
+ ("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')
+ 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)
@@ -299,176 +392,196 @@ class Person(models.Model):
class Meta:
verbose_name = "Person (Legacy)"
verbose_name_plural = "Personen (Legacy)"
- ordering = ['nachname', 'vorname']
+ 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
+ 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")
-
+ 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")
-
+ 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,
+ max_digits=12,
+ decimal_places=2,
verbose_name="Größe in qm",
- validators=[MinValueValidator(0.01)]
+ validators=[MinValueValidator(0.01)],
)
-
+
# Landnutzung
gruenland_qm = models.DecimalField(
- max_digits=12,
- decimal_places=2,
+ max_digits=12,
+ decimal_places=2,
default=0,
verbose_name="Grünland (qm)",
- validators=[MinValueValidator(0)]
+ validators=[MinValueValidator(0)],
)
acker_qm = models.DecimalField(
- max_digits=12,
- decimal_places=2,
+ max_digits=12,
+ decimal_places=2,
default=0,
verbose_name="Acker (qm)",
- validators=[MinValueValidator(0)]
+ validators=[MinValueValidator(0)],
)
wald_qm = models.DecimalField(
- max_digits=12,
- decimal_places=2,
+ max_digits=12,
+ decimal_places=2,
default=0,
verbose_name="Wald (qm)",
- validators=[MinValueValidator(0)]
+ validators=[MinValueValidator(0)],
)
sonstiges_qm = models.DecimalField(
- max_digits=12,
- decimal_places=2,
+ max_digits=12,
+ decimal_places=2,
default=0,
verbose_name="Sonstiges (qm)",
- validators=[MinValueValidator(0)]
+ validators=[MinValueValidator(0)],
)
-
+
# Verpachtung (Legacy-Felder für Kompatibilität)
verpachtete_gesamtflaeche = models.DecimalField(
- max_digits=12,
- decimal_places=2,
+ max_digits=12,
+ decimal_places=2,
verbose_name="Verpachtete Gesamtfläche (qm)",
- validators=[MinValueValidator(0)]
+ validators=[MinValueValidator(0)],
)
flaeche_alte_liste = models.DecimalField(
- max_digits=12,
- decimal_places=2,
- null=True,
+ max_digits=12,
+ decimal_places=2,
+ null=True,
blank=True,
- verbose_name="Fläche alte Liste (qm)"
+ verbose_name="Fläche alte Liste (qm)",
)
verp_flaeche_aktuell = models.DecimalField(
- max_digits=12,
- decimal_places=2,
+ max_digits=12,
+ decimal_places=2,
verbose_name="Verp. Fläche aktuell (qm)",
- validators=[MinValueValidator(0)]
+ validators=[MinValueValidator(0)],
)
-
+
# Aktuelle Verpachtung (Neue Struktur)
aktueller_paechter = models.ForeignKey(
- 'Paechter',
- on_delete=models.SET_NULL,
- null=True,
- blank=True,
+ "Paechter",
+ on_delete=models.SET_NULL,
+ null=True,
+ blank=True,
verbose_name="Aktueller Pächter",
- related_name="gepachtete_laendereien"
+ 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"
)
- 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")
-
+ 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'),
+ ("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"
+ 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,
+ max_digits=12,
+ decimal_places=2,
+ null=True,
+ blank=True,
verbose_name="Pachtzins pro ha (€)",
- validators=[MinValueValidator(0)]
+ validators=[MinValueValidator(0)],
)
pachtzins_pauschal = models.DecimalField(
- max_digits=12,
- decimal_places=2,
- null=True,
- blank=True,
+ max_digits=12,
+ decimal_places=2,
+ null=True,
+ blank=True,
verbose_name="Pachtzins pauschal/Jahr (€)",
- validators=[MinValueValidator(0)]
+ 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 (%)"
+ 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")
-
+ 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,
+ max_digits=8,
+ decimal_places=2,
+ null=True,
blank=True,
- verbose_name="Anteil Grundsteuer (%)"
+ verbose_name="Anteil Grundsteuer (%)",
)
anteil_lwk = models.DecimalField(
- max_digits=8,
- decimal_places=2,
- null=True,
+ max_digits=8,
+ decimal_places=2,
+ null=True,
blank=True,
- verbose_name="Anteil LWK (%)"
+ 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")
-
+ 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)
@@ -476,110 +589,116 @@ class Land(models.Model):
class Meta:
verbose_name = "Land"
verbose_name_plural = "Ländereien"
- ordering = ['gemeinde', 'gemarkung', 'flur', 'flurstueck']
+ 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)
-
+ 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 (
+ 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
-
+ neue_total = (
+ self.neue_verpachtungen.filter(status="aktiv").aggregate(
+ total=Sum("verpachtete_flaeche")
+ )["total"]
+ or 0
+ )
+
if neue_total > 0:
return neue_total
-
+
# Priorität 2: Einzelverpachtung im Land-Model (verp_flaeche_aktuell)
if self.verp_flaeche_aktuell and self.verp_flaeche_aktuell > 0:
return self.verp_flaeche_aktuell
-
+
# No legacy system - return neue_total (could be 0)
return neue_total
-
+
def get_verfuegbare_flaeche(self):
"""Berechnet die noch verfügbare Fläche für neue Verpachtungen"""
return self.groesse_qm - self.get_verpachtete_flaeche_aktuell()
-
+
def get_verpachtungsgrad_neu(self):
"""Berechnet den Verpachtungsgrad basierend auf neuen Verpachtungen"""
if self.groesse_qm > 0:
return (self.get_verpachtete_flaeche_aktuell() / self.groesse_qm) * 100
return 0
-
+
def get_steuer_gesamt(self):
"""Berechnet den Gesamtsteueranteil"""
grundsteuer = self.anteil_grundsteuer or 0
lwk = self.anteil_lwk or 0
return grundsteuer + lwk
-
+
def _qm_to_hektar(self, qm_value):
"""Hilfsmethode zur Umrechnung von qm in Hektar"""
- from decimal import Decimal, ROUND_HALF_UP
+ 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')
+ 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')
-
+ 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())
@@ -587,211 +706,235 @@ class Land(models.Model):
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'),
+ ("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")
-
+ 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")
+ 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")
-
+ verlaengerung_klausel = models.BooleanField(
+ default=False, verbose_name="Automatische Verlängerung"
+ )
+
# Flächenangaben
verpachtete_flaeche = models.DecimalField(
- max_digits=12,
- decimal_places=2,
+ max_digits=12,
+ decimal_places=2,
verbose_name="Verpachtete Fläche (qm)",
- validators=[MinValueValidator(0.01)]
+ validators=[MinValueValidator(0.01)],
)
-
+
# Pachtzins
pachtzins_pauschal = models.DecimalField(
- max_digits=12,
- decimal_places=2,
+ max_digits=12,
+ decimal_places=2,
verbose_name="Pachtzins pauschal/Jahr (€)",
- validators=[MinValueValidator(0)]
+ validators=[MinValueValidator(0)],
)
pachtzins_pro_ha = models.DecimalField(
- max_digits=12,
- decimal_places=2,
- null=True,
- blank=True,
+ max_digits=12,
+ decimal_places=2,
+ null=True,
+ blank=True,
verbose_name="Pachtzins pro ha (€)",
- validators=[MinValueValidator(0)]
+ validators=[MinValueValidator(0)],
)
-
+
# Zahlungsweise
ZAHLUNGSWEISE_CHOICES = [
- ('jaehrlich', 'Jährlich'),
- ('halbjaehrlich', 'Halbjährlich'),
- ('vierteljaehrlich', 'Vierteljährlich'),
- ('monatlich', 'Monatlich'),
+ ("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"
+ 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 (%)"
+ 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")
-
+ 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")
+ 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']
-
+ 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
+ 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')
-
+ hektar = Decimal(str(self.verpachtete_flaeche)) / Decimal("10000")
+ return hektar.quantize(Decimal("0.01"), rounding=ROUND_HALF_UP)
+ return Decimal("0.00")
+
def is_aktiv(self):
"""Prüft ob der Vertrag noch aktiv ist"""
from datetime import date
-
-
+
heute = date.today()
pachtbeginn_date = ensure_date(self.pachtbeginn)
pachtende_date = ensure_date(self.pachtende)
-
+
if not pachtbeginn_date:
return False
-
+
if pachtende_date:
return pachtbeginn_date <= heute <= pachtende_date
return pachtbeginn_date <= heute # Unbefristet
-
+
def get_restlaufzeit_tage(self):
"""Berechnet die Restlaufzeit in Tagen"""
from datetime import date
-
-
+
heute = date.today()
pachtende_date = ensure_date(self.pachtende)
-
+
if pachtende_date and pachtende_date > heute:
return (pachtende_date - heute).days
return None # Unbefristet
-
+
@property
def ust_pacht_betrag(self):
"""Berechnet die USt auf Pacht (falls optiert)"""
- from decimal import Decimal, ROUND_HALF_UP
+ 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')
+ ust_betrag = (
+ Decimal(str(self.pachtzins_pauschal))
+ * Decimal(str(self.ust_satz))
+ / Decimal("100")
+ )
+ return ust_betrag.quantize(Decimal("0.01"), rounding=ROUND_HALF_UP)
+ return Decimal("0.00")
def save(self, *args, **kwargs):
"""Override save to trigger Abrechnung updates"""
is_new = self.pk is None
old_instance = None
-
+
if not is_new:
try:
old_instance = LandVerpachtung.objects.get(pk=self.pk)
except LandVerpachtung.DoesNotExist:
old_instance = None
-
+
super().save(*args, **kwargs)
-
+
# Update Abrechnungen after save
self._update_abrechnungen(old_instance, is_new)
-
+
def _update_abrechnungen(self, old_instance, is_new):
"""Update LandAbrechnung records when Verpachtung changes"""
from datetime import date
-
-
+
# Determine affected years
years_to_update = set()
-
+
pachtbeginn_year = get_year_from_date(self.pachtbeginn)
if pachtbeginn_year:
years_to_update.add(pachtbeginn_year)
-
+
pachtende_year = get_year_from_date(self.pachtende)
if pachtende_year:
years_to_update.add(pachtende_year)
-
+
# If updated, check old dates too
if old_instance:
old_pachtbeginn_year = get_year_from_date(old_instance.pachtbeginn)
if old_pachtbeginn_year:
years_to_update.add(old_pachtbeginn_year)
-
+
old_pachtende_year = get_year_from_date(old_instance.pachtende)
if old_pachtende_year:
years_to_update.add(old_pachtende_year)
-
+
# Add current year if contract is active
if self.is_aktiv():
years_to_update.add(date.today().year)
-
+
# Update each affected year
for year in years_to_update:
self._update_abrechnung_for_year(year, old_instance, is_new)
-
+
def _update_abrechnung_for_year(self, year, old_instance, is_new):
"""Update or create LandAbrechnung for specific year"""
- from decimal import Decimal
from datetime import date
-
+ 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}'
- }
+ "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
@@ -800,35 +943,48 @@ class LandVerpachtung(models.Model):
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')
-
+ 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 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}"
+ 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.bemerkungen = (
+ f"[{date.today().strftime('%d.%m.%Y')}] {change_note}"
+ )
+
abrechnung.save()
-
+
def _calculate_rent_for_year(self, year):
"""Calculate rent amount for specific year"""
- from decimal import Decimal
from datetime import date
+ from 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:
@@ -836,294 +992,331 @@ class LandVerpachtung(models.Model):
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')
-
+ 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')
-
+ 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
-
+ 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')
-
+ 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)
-
+ 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}"
+ 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.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)])
-
+ 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,
+ max_digits=12,
+ decimal_places=2,
+ default=0,
verbose_name="Pacht vereinnahmt (€)",
- validators=[MinValueValidator(0)]
+ validators=[MinValueValidator(0)],
)
umlagen_vereinnahmt = models.DecimalField(
- max_digits=12,
- decimal_places=2,
- default=0,
+ max_digits=12,
+ decimal_places=2,
+ default=0,
verbose_name="Umlagen vereinnahmt (€)",
- validators=[MinValueValidator(0)]
+ validators=[MinValueValidator(0)],
)
sonstige_einnahmen = models.DecimalField(
- max_digits=12,
- decimal_places=2,
- default=0,
+ max_digits=12,
+ decimal_places=2,
+ default=0,
verbose_name="Sonstige Einnahmen (€)",
- validators=[MinValueValidator(0)]
+ validators=[MinValueValidator(0)],
)
-
+
# Zahlungstermine (optional)
zahlungen = models.JSONField(
- null=True,
- blank=True,
+ null=True,
+ blank=True,
verbose_name="Zahlungstermine",
- help_text="Liste von Objekten {datum, betrag, art}"
+ 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_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,
+ max_digits=12,
+ decimal_places=2,
+ default=0,
verbose_name="Grundsteuer Betrag (€)",
- validators=[MinValueValidator(0)]
+ validators=[MinValueValidator(0)],
)
versicherungen_betrag = models.DecimalField(
- max_digits=12,
- decimal_places=2,
- default=0,
+ max_digits=12,
+ decimal_places=2,
+ default=0,
verbose_name="Versicherungen Betrag (€)",
- validators=[MinValueValidator(0)]
+ validators=[MinValueValidator(0)],
)
verbandsbeitraege_betrag = models.DecimalField(
- max_digits=12,
- decimal_places=2,
- default=0,
+ max_digits=12,
+ decimal_places=2,
+ default=0,
verbose_name="Verbandsbeiträge Betrag (€)",
- validators=[MinValueValidator(0)]
+ validators=[MinValueValidator(0)],
)
sonstige_abgaben_betrag = models.DecimalField(
- max_digits=12,
- decimal_places=2,
- default=0,
+ max_digits=12,
+ decimal_places=2,
+ default=0,
verbose_name="Sonstige öffentliche Abgaben (€)",
- validators=[MinValueValidator(0)]
+ validators=[MinValueValidator(0)],
)
instandhaltung_betrag = models.DecimalField(
- max_digits=12,
- decimal_places=2,
- default=0,
+ max_digits=12,
+ decimal_places=2,
+ default=0,
verbose_name="Instandhaltung/Reparaturen (€)",
- validators=[MinValueValidator(0)]
+ validators=[MinValueValidator(0)],
)
verwaltung_recht_betrag = models.DecimalField(
- max_digits=12,
- decimal_places=2,
- default=0,
+ max_digits=12,
+ decimal_places=2,
+ default=0,
verbose_name="Verwaltung/Recht (€)",
- validators=[MinValueValidator(0)]
+ validators=[MinValueValidator(0)],
)
-
+
# Umsatzsteuer/Vorsteuer
vorsteuer_aus_umlagen = models.DecimalField(
- max_digits=12,
- decimal_places=2,
- default=0,
+ max_digits=12,
+ decimal_places=2,
+ default=0,
verbose_name="Vorsteuer aus umgelegten Kosten (€)",
- validators=[MinValueValidator(0)]
+ validators=[MinValueValidator(0)],
)
-
+
# Sonstiges
offene_posten = models.DecimalField(
- max_digits=12,
- decimal_places=2,
- default=0,
- verbose_name="Offene Posten (€)"
+ max_digits=12, decimal_places=2, default=0, verbose_name="Offene Posten (€)"
)
- bemerkungen = models.TextField(null=True, blank=True, verbose_name="Bemerkungen Abrechnung")
-
+ 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)"
+ 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)"
+ 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)"
+ 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
-
+ 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)
-
+
+ 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
+ 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
+ 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')
+ ust = self.pacht_vereinnahmt * (self.land.ust_satz / Decimal("100"))
+ return ust.quantize(Decimal("0.01"), rounding=ROUND_HALF_UP)
+ return Decimal("0.00")
+
class DokumentLink(models.Model):
KONTEXT_CHOICES = [
- ('pachtvertrag', 'Pachtvertrag'),
- ('antrag', 'Antrag'),
- ('verwendungsnachweis', 'Verwendungsnachweis'),
- ('rechnung', 'Rechnung'),
- ('vertrag', 'Vertrag'),
- ('bericht', 'Bericht'),
- ('landkarte', 'Landkarte'),
- ('kataster', 'Kataster'),
- ('anderes', 'Anderes'),
+ ("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')
+ 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)")
+ 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")
+ 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")
+ 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']
+ ordering = ["titel"]
def __str__(self):
return f"{self.titel} ({self.get_kontext_display()})"
-
+
def get_paperless_url(self):
"""Gibt die URL zum Dokument in Paperless zurück (über Django Redirect)"""
return f"/api/paperless/documents/{self.paperless_document_id}/"
-
+
def get_paperless_thumbnail_url(self):
"""Gibt die URL zum Thumbnail in Paperless zurück"""
from django.conf import settings
+
if settings.PAPERLESS_API_URL:
return f"{settings.PAPERLESS_API_URL}/api/paperless/documents/{self.paperless_document_id}/thumb/"
return None
-
+
def get_verpachtung(self):
"""Gibt die verknüpfte Verpachtung zurück"""
if self.verpachtung_id:
@@ -1132,7 +1325,7 @@ class DokumentLink(models.Model):
except LandVerpachtung.DoesNotExist:
return None
return None
-
+
def get_land(self):
"""Gibt die verknüpfte Länderei zurück"""
if self.land_id:
@@ -1141,7 +1334,7 @@ class DokumentLink(models.Model):
except Land.DoesNotExist:
return None
return None
-
+
def get_paechter(self):
"""Gibt den verknüpften Pächter zurück"""
if self.paechter_id:
@@ -1150,7 +1343,7 @@ class DokumentLink(models.Model):
except Paechter.DoesNotExist:
return None
return None
-
+
def get_destinataer(self):
"""Gibt den verknüpften Destinatär zurück"""
if self.destinataer_id:
@@ -1159,7 +1352,7 @@ class DokumentLink(models.Model):
except Destinataer.DoesNotExist:
return None
return None
-
+
def get_foerderung(self):
"""Gibt die verknüpfte Förderung zurück"""
if self.foerderung_id:
@@ -1168,7 +1361,7 @@ class DokumentLink(models.Model):
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:
@@ -1178,35 +1371,58 @@ class DokumentLink(models.Model):
return None
return None
+
class Foerderung(models.Model):
KATEGORIE_CHOICES = [
- ('bildung', 'Bildung'),
- ('forschung', 'Forschung'),
- ('kultur', 'Kultur'),
- ('soziales', 'Soziales'),
- ('umwelt', 'Umwelt'),
- ('anderes', 'Anderes'),
+ ("bildung", "Bildung"),
+ ("forschung", "Forschung"),
+ ("kultur", "Kultur"),
+ ("soziales", "Soziales"),
+ ("umwelt", "Umwelt"),
+ ("anderes", "Anderes"),
]
-
+
STATUS_CHOICES = [
- ('beantragt', 'Beantragt'),
- ('genehmigt', 'Genehmigt'),
- ('ausgezahlt', 'Ausgezahlt'),
- ('abgelehnt', 'Abgelehnt'),
- ('storniert', 'Storniert'),
+ ("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)
+ 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")
+ 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)
@@ -1214,7 +1430,7 @@ class Foerderung(models.Model):
class Meta:
verbose_name = "Förderung"
verbose_name_plural = "Förderungen"
- ordering = ['-jahr', '-betrag']
+ ordering = ["-jahr", "-betrag"]
# Note: unique_together will be updated after migration
def __str__(self):
@@ -1223,232 +1439,330 @@ class Foerderung(models.Model):
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',
+ "beantragt": "orange",
+ "genehmigt": "blue",
+ "ausgezahlt": "green",
+ "abgelehnt": "red",
+ "storniert": "gray",
}
- return colors.get(self.status, 'black')
+ return colors.get(self.status, "black")
class DestinataerUnterstuetzung(models.Model):
"""Geplante/ausgeführte Unterstützungszahlungen an Destinatäre"""
+
STATUS_CHOICES = [
- ('geplant', 'Geplant'),
- ('faellig', 'Fällig'),
- ('in_bearbeitung', 'In Bearbeitung'),
- ('ausgezahlt', 'Ausgezahlt'),
- ('storniert', 'Storniert'),
+ ("geplant", "Geplant"),
+ ("faellig", "Fällig"),
+ ("in_bearbeitung", "In Bearbeitung"),
+ ("ausgezahlt", "Ausgezahlt"),
+ ("storniert", "Storniert"),
]
id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
- destinataer = models.ForeignKey('Destinataer', on_delete=models.CASCADE, related_name='unterstuetzungen', verbose_name='Destinatär')
- konto = models.ForeignKey('StiftungsKonto', on_delete=models.PROTECT, verbose_name='Zahlungskonto')
- betrag = models.DecimalField(max_digits=12, decimal_places=2, verbose_name='Betrag (€)')
- faellig_am = models.DateField(verbose_name='Fällig am')
- status = models.CharField(max_length=20, choices=STATUS_CHOICES, default='geplant', verbose_name='Status')
- beschreibung = models.CharField(max_length=255, blank=True, verbose_name='Beschreibung')
-
+ destinataer = models.ForeignKey(
+ "Destinataer",
+ on_delete=models.CASCADE,
+ related_name="unterstuetzungen",
+ verbose_name="Destinatär",
+ )
+ konto = models.ForeignKey(
+ "StiftungsKonto", on_delete=models.PROTECT, verbose_name="Zahlungskonto"
+ )
+ betrag = models.DecimalField(
+ max_digits=12, decimal_places=2, verbose_name="Betrag (€)"
+ )
+ faellig_am = models.DateField(verbose_name="Fällig am")
+ status = models.CharField(
+ max_length=20, choices=STATUS_CHOICES, default="geplant", verbose_name="Status"
+ )
+ beschreibung = models.CharField(
+ max_length=255, blank=True, verbose_name="Beschreibung"
+ )
+
# Enhanced fields for recurrent payments and IBAN tracking
- empfaenger_iban = models.CharField(max_length=34, blank=True, verbose_name='Empfänger IBAN')
- empfaenger_name = models.CharField(max_length=200, blank=True, verbose_name='Empfänger Name')
- verwendungszweck = models.CharField(max_length=140, blank=True, verbose_name='Verwendungszweck')
- ausgezahlt_am = models.DateField(null=True, blank=True, verbose_name='Ausgezahlt am')
- ausgezahlt_von = models.ForeignKey('auth.User', on_delete=models.SET_NULL, null=True, blank=True, verbose_name='Ausgezahlt von')
-
+ empfaenger_iban = models.CharField(
+ max_length=34, blank=True, verbose_name="Empfänger IBAN"
+ )
+ empfaenger_name = models.CharField(
+ max_length=200, blank=True, verbose_name="Empfänger Name"
+ )
+ verwendungszweck = models.CharField(
+ max_length=140, blank=True, verbose_name="Verwendungszweck"
+ )
+ ausgezahlt_am = models.DateField(
+ null=True, blank=True, verbose_name="Ausgezahlt am"
+ )
+ ausgezahlt_von = models.ForeignKey(
+ "auth.User",
+ on_delete=models.SET_NULL,
+ null=True,
+ blank=True,
+ verbose_name="Ausgezahlt von",
+ )
+
# Link to recurrent payment template if this was auto-generated
- wiederkehrend_von = models.ForeignKey('UnterstuetzungWiederkehrend', on_delete=models.SET_NULL, null=True, blank=True, verbose_name='Wiederkehrende Zahlung')
-
+ wiederkehrend_von = models.ForeignKey(
+ "UnterstuetzungWiederkehrend",
+ on_delete=models.SET_NULL,
+ null=True,
+ blank=True,
+ verbose_name="Wiederkehrende Zahlung",
+ )
+
erstellt_am = models.DateTimeField(auto_now_add=True)
aktualisiert_am = models.DateTimeField(auto_now=True)
class Meta:
- verbose_name = 'Destinatärunterstützung'
- verbose_name_plural = 'Destinatärunterstützungen'
- ordering = ['-faellig_am', '-erstellt_am']
+ verbose_name = "Destinatärunterstützung"
+ verbose_name_plural = "Destinatärunterstützungen"
+ ordering = ["-faellig_am", "-erstellt_am"]
indexes = [
- models.Index(fields=['status', 'faellig_am']),
- models.Index(fields=['destinataer', 'status']),
- models.Index(fields=['wiederkehrend_von']),
+ models.Index(fields=["status", "faellig_am"]),
+ models.Index(fields=["destinataer", "status"]),
+ models.Index(fields=["wiederkehrend_von"]),
]
def __str__(self):
return f"{self.destinataer.get_full_name()} – €{self.betrag} am {self.faellig_am} ({self.get_status_display()})"
-
+
def is_overdue(self):
"""Check if payment is overdue"""
from django.utils import timezone
- return self.faellig_am < timezone.now().date() and self.status in ['geplant', 'faellig']
-
+
+ return self.faellig_am < timezone.now().date() and self.status in [
+ "geplant",
+ "faellig",
+ ]
+
def can_be_marked_paid(self):
"""Check if payment can be marked as paid"""
- return self.status in ['geplant', 'faellig', 'in_bearbeitung']
+ return self.status in ["geplant", "faellig", "in_bearbeitung"]
class UnterstuetzungWiederkehrend(models.Model):
"""Template for recurring support payments"""
+
INTERVALL_CHOICES = [
- ('monatlich', 'Monatlich'),
- ('quartalsweise', 'Vierteljährlich'),
- ('halbjaehrlich', 'Halbjährlich'),
- ('jaehrlich', 'Jährlich'),
+ ("monatlich", "Monatlich"),
+ ("quartalsweise", "Vierteljährlich"),
+ ("halbjaehrlich", "Halbjährlich"),
+ ("jaehrlich", "Jährlich"),
]
-
+
id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
- destinataer = models.ForeignKey('Destinataer', on_delete=models.CASCADE, related_name='wiederkehrende_unterstuetzungen', verbose_name='Destinatär')
- konto = models.ForeignKey('StiftungsKonto', on_delete=models.PROTECT, verbose_name='Zahlungskonto')
- betrag = models.DecimalField(max_digits=12, decimal_places=2, verbose_name='Betrag (€)')
- intervall = models.CharField(max_length=20, choices=INTERVALL_CHOICES, verbose_name='Intervall')
- beschreibung = models.CharField(max_length=255, blank=True, verbose_name='Beschreibung')
-
+ destinataer = models.ForeignKey(
+ "Destinataer",
+ on_delete=models.CASCADE,
+ related_name="wiederkehrende_unterstuetzungen",
+ verbose_name="Destinatär",
+ )
+ konto = models.ForeignKey(
+ "StiftungsKonto", on_delete=models.PROTECT, verbose_name="Zahlungskonto"
+ )
+ betrag = models.DecimalField(
+ max_digits=12, decimal_places=2, verbose_name="Betrag (€)"
+ )
+ intervall = models.CharField(
+ max_length=20, choices=INTERVALL_CHOICES, verbose_name="Intervall"
+ )
+ beschreibung = models.CharField(
+ max_length=255, blank=True, verbose_name="Beschreibung"
+ )
+
# IBAN and payment details
- empfaenger_iban = models.CharField(max_length=34, verbose_name='Empfänger IBAN')
- empfaenger_name = models.CharField(max_length=200, verbose_name='Empfänger Name')
- verwendungszweck = models.CharField(max_length=140, blank=True, verbose_name='Verwendungszweck')
-
+ empfaenger_iban = models.CharField(max_length=34, verbose_name="Empfänger IBAN")
+ empfaenger_name = models.CharField(max_length=200, verbose_name="Empfänger Name")
+ verwendungszweck = models.CharField(
+ max_length=140, blank=True, verbose_name="Verwendungszweck"
+ )
+
# Schedule settings
- erste_zahlung_am = models.DateField(verbose_name='Erste Zahlung am')
- letzte_zahlung_am = models.DateField(null=True, blank=True, verbose_name='Letzte Zahlung am (optional)')
- naechste_generierung = models.DateField(verbose_name='Nächste Generierung')
-
- aktiv = models.BooleanField(default=True, verbose_name='Aktiv')
+ erste_zahlung_am = models.DateField(verbose_name="Erste Zahlung am")
+ letzte_zahlung_am = models.DateField(
+ null=True, blank=True, verbose_name="Letzte Zahlung am (optional)"
+ )
+ naechste_generierung = models.DateField(verbose_name="Nächste Generierung")
+
+ aktiv = models.BooleanField(default=True, verbose_name="Aktiv")
erstellt_am = models.DateTimeField(auto_now_add=True)
- erstellt_von = models.ForeignKey('auth.User', on_delete=models.SET_NULL, null=True, blank=True, verbose_name='Erstellt von')
-
+ erstellt_von = models.ForeignKey(
+ "auth.User",
+ on_delete=models.SET_NULL,
+ null=True,
+ blank=True,
+ verbose_name="Erstellt von",
+ )
+
class Meta:
- verbose_name = 'Wiederkehrende Unterstützung'
- verbose_name_plural = 'Wiederkehrende Unterstützungen'
- ordering = ['-erstellt_am']
+ verbose_name = "Wiederkehrende Unterstützung"
+ verbose_name_plural = "Wiederkehrende Unterstützungen"
+ ordering = ["-erstellt_am"]
indexes = [
- models.Index(fields=['aktiv', 'naechste_generierung']),
- models.Index(fields=['destinataer', 'aktiv']),
+ models.Index(fields=["aktiv", "naechste_generierung"]),
+ models.Index(fields=["destinataer", "aktiv"]),
]
def __str__(self):
return f"{self.destinataer.get_full_name()} – {self.get_intervall_display()} €{self.betrag}"
-
+
def generiere_naechste_zahlung(self):
"""Generate the next scheduled payment"""
from datetime import timedelta
+
from dateutil.relativedelta import relativedelta
-
+
if not self.aktiv:
return None
-
+
heute = timezone.now().date()
if self.naechste_generierung > heute:
return None # Not yet time to generate
-
+
# Check if we've reached the end date
- if self.letzte_zahlung_am and self.naechste_generierung > self.letzte_zahlung_am:
+ if (
+ self.letzte_zahlung_am
+ and self.naechste_generierung > self.letzte_zahlung_am
+ ):
return None
-
+
# Create the next payment
neue_zahlung = DestinataerUnterstuetzung.objects.create(
destinataer=self.destinataer,
konto=self.konto,
betrag=self.betrag,
faellig_am=self.naechste_generierung,
- beschreibung=self.beschreibung or f"{self.get_intervall_display()} Unterstützung",
+ beschreibung=self.beschreibung
+ or f"{self.get_intervall_display()} Unterstützung",
empfaenger_iban=self.empfaenger_iban,
empfaenger_name=self.empfaenger_name,
verwendungszweck=self.verwendungszweck,
wiederkehrend_von=self,
- status='geplant'
+ status="geplant",
)
-
+
# Calculate next generation date
- if self.intervall == 'monatlich':
- self.naechste_generierung = self.naechste_generierung + relativedelta(months=1)
- elif self.intervall == 'quartalsweise':
- self.naechste_generierung = self.naechste_generierung + relativedelta(months=3)
- elif self.intervall == 'halbjaehrlich':
- self.naechste_generierung = self.naechste_generierung + relativedelta(months=6)
- elif self.intervall == 'jaehrlich':
- self.naechste_generierung = self.naechste_generierung + relativedelta(years=1)
-
+ if self.intervall == "monatlich":
+ self.naechste_generierung = self.naechste_generierung + relativedelta(
+ months=1
+ )
+ elif self.intervall == "quartalsweise":
+ self.naechste_generierung = self.naechste_generierung + relativedelta(
+ months=3
+ )
+ elif self.intervall == "halbjaehrlich":
+ self.naechste_generierung = self.naechste_generierung + relativedelta(
+ months=6
+ )
+ elif self.intervall == "jaehrlich":
+ self.naechste_generierung = self.naechste_generierung + relativedelta(
+ years=1
+ )
+
self.save()
return neue_zahlung
class DestinataerNotiz(models.Model):
"""Zeitgestempelte Notizen/Telefonvermerke zu einem Destinatär, optional mit Datei."""
+
id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
- destinataer = models.ForeignKey('Destinataer', on_delete=models.CASCADE, related_name='notizen_eintraege', verbose_name='Destinatär')
- titel = models.CharField(max_length=200, blank=True, verbose_name='Titel')
- text = models.TextField(blank=True, verbose_name='Notiz')
- datei = models.FileField(upload_to='destinataer_notizen/', null=True, blank=True, verbose_name='Anhang')
- erstellt_von = models.ForeignKey('auth.User', on_delete=models.SET_NULL, null=True, blank=True, verbose_name='Erstellt von')
- erstellt_am = models.DateTimeField(auto_now_add=True, verbose_name='Erstellt am')
+ 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']
+ 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.'),
+ ("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")
+ 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 (€)"
+ 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)"
+ 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']
-
+ ordering = ["nachname", "vorname"]
+
def __str__(self):
name_parts = []
if self.anrede:
@@ -1459,13 +1773,13 @@ class Rentmeister(models.Model):
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 = []
@@ -1480,104 +1794,153 @@ class Rentmeister(models.Model):
class StiftungsKonto(models.Model):
"""Bankkonten der Stiftung"""
+
KONTO_TYP_CHOICES = [
- ('girokonto', 'Girokonto'),
- ('sparkonto', 'Sparkonto'),
- ('festgeld', 'Festgeld'),
- ('tagesgeld', 'Tagesgeld'),
- ('depot', 'Depot'),
- ('sonstiges', 'Sonstiges'),
+ ("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")
+ 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 (%)")
+ 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']
-
+ 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'),
+ ("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'),
+ ("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")
-
+ 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")
-
+ 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")
-
+ 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")
-
+ 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")
+ 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")
-
+ 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")
-
+ 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
-
+ 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)
@@ -1585,93 +1948,132 @@ class BankTransaction(models.Model):
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'),
+ ("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'),
+ ("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 (€)")
+ 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")
-
+ 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")
-
+ 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")
-
+ 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")
+ 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']
-
+ 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',
+ "geplant": "secondary",
+ "bestellt": "warning",
+ "erhalten": "info",
+ "in_bearbeitung": "primary",
+ "bezahlt": "success",
+ "storniert": "danger",
}
- return colors.get(self.status, 'secondary')
-
+ 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'
-
+ 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:
@@ -1681,198 +2083,228 @@ class Verwaltungskosten(models.Model):
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'),
-
+ ("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'),
-
+ ("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'),
-
+ ("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'),
-
+ ("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'),
-
+ ("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'),
+ ("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'),
+ ("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'),
+ ("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
+ 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")
+ 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
-
+ 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")
+ 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")
-
+ 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']
+ ordering = ["-timestamp"]
indexes = [
- models.Index(fields=['timestamp']),
- models.Index(fields=['user', 'timestamp']),
- models.Index(fields=['entity_type', 'timestamp']),
- models.Index(fields=['action', 'timestamp']),
+ 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'
+ 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'),
+ ("pending", "Wartend"),
+ ("running", "Läuft"),
+ ("completed", "Abgeschlossen"),
+ ("failed", "Fehlgeschlagen"),
]
-
+
TYPE_CHOICES = [
- ('full', 'Vollständiges Backup'),
- ('database', 'Nur Datenbank'),
- ('files', 'Nur Dateien'),
+ ("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")
-
+ 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_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")
-
+ 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)")
+ 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")
-
+ 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']
-
+ 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']:
+ for unit in ["B", "KB", "MB", "GB"]:
if size < 1024:
return f"{size:.1f} {unit}"
size /= 1024
@@ -1881,61 +2313,70 @@ class BackupJob(models.Model):
class AppConfiguration(models.Model):
"""Application configuration settings that can be managed through the admin interface"""
-
+
SETTING_TYPE_CHOICES = [
- ('text', 'Text'),
- ('number', 'Number'),
- ('boolean', 'Boolean'),
- ('url', 'URL'),
- ('tag', 'Tag Name'),
- ('tag_id', 'Tag ID'),
+ ("text", "Text"),
+ ("number", "Number"),
+ ("boolean", "Boolean"),
+ ("url", "URL"),
+ ("tag", "Tag Name"),
+ ("tag_id", "Tag ID"),
]
-
+
CATEGORY_CHOICES = [
- ('paperless', 'Paperless Integration'),
- ('general', 'General Settings'),
- ('corporate', 'Corporate Identity'),
- ('notifications', 'Notifications'),
- ('system', 'System Settings'),
+ ("paperless", "Paperless Integration"),
+ ("general", "General Settings"),
+ ("corporate", "Corporate Identity"),
+ ("notifications", "Notifications"),
+ ("system", "System Settings"),
]
-
+
id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
key = models.CharField(max_length=100, unique=True, verbose_name="Setting Key")
display_name = models.CharField(max_length=200, verbose_name="Display Name")
description = models.TextField(blank=True, null=True, verbose_name="Description")
value = models.TextField(verbose_name="Value")
default_value = models.TextField(verbose_name="Default Value")
- setting_type = models.CharField(max_length=20, choices=SETTING_TYPE_CHOICES, default='text', verbose_name="Type")
- category = models.CharField(max_length=50, choices=CATEGORY_CHOICES, default='general', verbose_name="Category")
+ setting_type = models.CharField(
+ max_length=20, choices=SETTING_TYPE_CHOICES, default="text", verbose_name="Type"
+ )
+ category = models.CharField(
+ max_length=50,
+ choices=CATEGORY_CHOICES,
+ default="general",
+ verbose_name="Category",
+ )
is_active = models.BooleanField(default=True, verbose_name="Active")
- is_system = models.BooleanField(default=False, verbose_name="System Setting (read-only)")
+ is_system = models.BooleanField(
+ default=False, verbose_name="System Setting (read-only)"
+ )
order = models.IntegerField(default=0, verbose_name="Display Order")
-
+
# Metadata
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
-
+
class Meta:
verbose_name = "App Configuration"
verbose_name_plural = "App Configurations"
- ordering = ['category', 'order', 'display_name']
-
+ ordering = ["category", "order", "display_name"]
+
def __str__(self):
return f"{self.display_name} ({self.key})"
-
+
def get_typed_value(self):
"""Return the value converted to the appropriate type"""
- if self.setting_type == 'boolean':
- return self.value.lower() in ('true', '1', 'yes', 'on')
- elif self.setting_type == 'number':
+ if self.setting_type == "boolean":
+ return self.value.lower() in ("true", "1", "yes", "on")
+ elif self.setting_type == "number":
try:
- if '.' in self.value:
+ if "." in self.value:
return float(self.value)
return int(self.value)
except (ValueError, TypeError):
return 0
return self.value
-
+
@classmethod
def get_setting(cls, key, default=None):
"""Get a setting value by key"""
@@ -1944,20 +2385,28 @@ class AppConfiguration(models.Model):
return setting.get_typed_value()
except cls.DoesNotExist:
return default
-
+
@classmethod
- def set_setting(cls, key, value, display_name=None, description=None, setting_type='text', category='general'):
+ def set_setting(
+ cls,
+ key,
+ value,
+ display_name=None,
+ description=None,
+ setting_type="text",
+ category="general",
+ ):
"""Set or update a setting value"""
setting, created = cls.objects.get_or_create(
key=key,
defaults={
- 'display_name': display_name or key,
- 'description': description,
- 'value': str(value),
- 'default_value': str(value),
- 'setting_type': setting_type,
- 'category': category,
- }
+ "display_name": display_name or key,
+ "description": description,
+ "value": str(value),
+ "default_value": str(value),
+ "setting_type": setting_type,
+ "category": category,
+ },
)
if not created:
setting.value = str(value)
@@ -1967,59 +2416,54 @@ class AppConfiguration(models.Model):
class HelpBox(models.Model):
"""Editierbare Hilfe-Infoboxen für Formulare"""
-
+
PAGE_CHOICES = [
- ('destinataer_new', 'Neuer Destinatär'),
- ('unterstuetzung_new', 'Neue Unterstützung'),
- ('foerderung_new', 'Neue Förderung'),
- ('paechter_new', 'Neuer Pächter'),
- ('laenderei_new', 'Neue Länderei'),
- ('verpachtung_new', 'Neue Verpachtung'),
- ('land_abrechnung_new', 'Neue Landabrechnung'),
- ('person_new', 'Neue Person'),
- ('konto_new', 'Neues Konto'),
- ('verwaltungskosten_new', 'Neue Verwaltungskosten'),
- ('rentmeister_new', 'Neuer Rentmeister'),
- ('dokument_new', 'Neues Dokument'),
- ('user_new', 'Neuer Benutzer'),
- ('csv_import_new', 'CSV Import'),
- ('destinataer_notiz_new', 'Destinatär Notiz'),
+ ("destinataer_new", "Neuer Destinatär"),
+ ("unterstuetzung_new", "Neue Unterstützung"),
+ ("foerderung_new", "Neue Förderung"),
+ ("paechter_new", "Neuer Pächter"),
+ ("laenderei_new", "Neue Länderei"),
+ ("verpachtung_new", "Neue Verpachtung"),
+ ("land_abrechnung_new", "Neue Landabrechnung"),
+ ("person_new", "Neue Person"),
+ ("konto_new", "Neues Konto"),
+ ("verwaltungskosten_new", "Neue Verwaltungskosten"),
+ ("rentmeister_new", "Neuer Rentmeister"),
+ ("dokument_new", "Neues Dokument"),
+ ("user_new", "Neuer Benutzer"),
+ ("csv_import_new", "CSV Import"),
+ ("destinataer_notiz_new", "Destinatär Notiz"),
]
-
+
id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
page_key = models.CharField(
- max_length=50,
- choices=PAGE_CHOICES,
- unique=True,
- verbose_name="Seite"
- )
- title = models.CharField(
- max_length=200,
- verbose_name="Titel der Hilfsbox"
+ max_length=50, choices=PAGE_CHOICES, unique=True, verbose_name="Seite"
)
+ title = models.CharField(max_length=200, verbose_name="Titel der Hilfsbox")
content = models.TextField(
verbose_name="Inhalt (Markdown unterstützt)",
- help_text="Sie können Markdown verwenden: **fett**, *kursiv*, `code`, [Link](url), etc."
+ help_text="Sie können Markdown verwenden: **fett**, *kursiv*, `code`, [Link](url), etc.",
)
- is_active = models.BooleanField(
- default=True,
- verbose_name="Aktiv"
- )
-
+ is_active = models.BooleanField(default=True, verbose_name="Aktiv")
+
# Metadata
created_at = models.DateTimeField(auto_now_add=True, verbose_name="Erstellt am")
updated_at = models.DateTimeField(auto_now=True, verbose_name="Aktualisiert am")
- created_by = models.CharField(max_length=100, null=True, blank=True, verbose_name="Erstellt von")
- updated_by = models.CharField(max_length=100, null=True, blank=True, verbose_name="Aktualisiert von")
-
+ created_by = models.CharField(
+ max_length=100, null=True, blank=True, verbose_name="Erstellt von"
+ )
+ updated_by = models.CharField(
+ max_length=100, null=True, blank=True, verbose_name="Aktualisiert von"
+ )
+
class Meta:
verbose_name = "Hilfs-Infobox"
verbose_name_plural = "Hilfs-Infoboxen"
- ordering = ['page_key']
-
+ ordering = ["page_key"]
+
def __str__(self):
return f"{self.get_page_key_display()}: {self.title}"
-
+
@classmethod
def get_help_for_page(cls, page_key):
"""Hole die aktive Hilfs-Infobox für eine bestimmte Seite"""
diff --git a/app/stiftung/serializers.py b/app/stiftung/serializers.py
index f7783de..f090c00 100644
--- a/app/stiftung/serializers.py
+++ b/app/stiftung/serializers.py
@@ -1,11 +1,14 @@
from rest_framework import serializers
-from .models import Person, Foerderung
+
+from .models import Foerderung, Person
+
class PersonSerializer(serializers.ModelSerializer):
class Meta:
model = Person
fields = "__all__"
+
class FoerderungSerializer(serializers.ModelSerializer):
class Meta:
model = Foerderung
diff --git a/app/stiftung/templatetags/help_tags.py b/app/stiftung/templatetags/help_tags.py
index 12f852d..172d8d2 100644
--- a/app/stiftung/templatetags/help_tags.py
+++ b/app/stiftung/templatetags/help_tags.py
@@ -1,28 +1,33 @@
import markdown
from django import template
from django.utils.safestring import mark_safe
+
from stiftung.models import HelpBox
register = template.Library()
-@register.inclusion_tag('stiftung/help_box.html')
+
+@register.inclusion_tag("stiftung/help_box.html")
def help_box(page_key, user=None):
"""Rendere eine Hilfs-Infobox für eine bestimmte Seite"""
help_obj = HelpBox.get_help_for_page(page_key)
-
+
context = {
- 'help_obj': help_obj,
- 'page_key': page_key,
- 'can_edit': user and (user.username == 'root' or user.is_superuser) if user else False
+ "help_obj": help_obj,
+ "page_key": page_key,
+ "can_edit": (
+ user and (user.username == "root" or user.is_superuser) if user else False
+ ),
}
-
+
if help_obj:
# Konvertiere Markdown zu HTML
- md = markdown.Markdown(extensions=['nl2br', 'fenced_code'])
- context['content_html'] = mark_safe(md.convert(help_obj.content))
-
+ md = markdown.Markdown(extensions=["nl2br", "fenced_code"])
+ context["content_html"] = mark_safe(md.convert(help_obj.content))
+
return context
+
@register.simple_tag
def help_box_exists(page_key):
"""Prüfe, ob eine Hilfs-Infobox für eine Seite existiert"""
diff --git a/app/stiftung/templatetags/pdf_tags.py b/app/stiftung/templatetags/pdf_tags.py
index 613972b..96b76e0 100644
--- a/app/stiftung/templatetags/pdf_tags.py
+++ b/app/stiftung/templatetags/pdf_tags.py
@@ -1,6 +1,7 @@
"""
PDF-specific template tags and filters
"""
+
from django import template
from django.utils.safestring import mark_safe
@@ -15,14 +16,14 @@ def lookup(obj, field_name):
"""
if obj is None:
return None
-
+
# Handle dict-like objects
- if hasattr(obj, '__getitem__') and not isinstance(obj, str):
+ if hasattr(obj, "__getitem__") and not isinstance(obj, str):
try:
return obj[field_name]
except (KeyError, TypeError):
pass
-
+
# Handle objects with attributes
if hasattr(obj, field_name):
attr = getattr(obj, field_name)
@@ -34,17 +35,17 @@ def lookup(obj, field_name):
# Method requires arguments, return as is
return attr
return attr
-
+
# Try to handle nested field access (e.g., "person.name")
- if '.' in field_name:
- parts = field_name.split('.')
+ if "." in field_name:
+ parts = field_name.split(".")
current = obj
for part in parts:
if current is None:
return None
current = lookup(current, part)
return current
-
+
return None
@@ -55,14 +56,14 @@ def get_display_value(obj, field_name):
Usage: {{ object|get_display_value:"field_name" }}
"""
value = lookup(obj, field_name)
-
+
# Try to get display value for choice fields
- display_method = f'get_{field_name}_display'
+ display_method = f"get_{field_name}_display"
if hasattr(obj, display_method):
display_value = getattr(obj, display_method)()
if display_value:
return display_value
-
+
return value
@@ -73,9 +74,9 @@ def format_currency(value):
Usage: {{ value|format_currency }}
"""
if value is None:
- return '-'
+ return "-"
try:
- return f"€{float(value):,.2f}".replace(',', ' ')
+ return f"€{float(value):,.2f}".replace(",", " ")
except (ValueError, TypeError):
return str(value)
@@ -87,11 +88,11 @@ def format_status_badge(status):
Usage: {{ status|format_status_badge }}
"""
if not status:
- return '-'
-
+ return "-"
+
status_lower = str(status).lower()
- css_class = f'status-{status_lower}'
-
+ css_class = f"status-{status_lower}"
+
return mark_safe(f'{status}')
@@ -102,13 +103,13 @@ def truncate_field(value, max_length=50):
Usage: {{ value|truncate_field:30 }}
"""
if value is None:
- return '-'
-
+ return "-"
+
str_value = str(value)
if len(str_value) <= max_length:
return str_value
-
- return str_value[:max_length-3] + '...'
+
+ return str_value[: max_length - 3] + "..."
@register.simple_tag
@@ -117,29 +118,29 @@ def get_field_value(obj, field_config):
Get formatted field value based on field configuration
Usage: {% get_field_value object field_config %}
"""
- field_name = field_config.get('field_name')
- field_type = field_config.get('field_type', 'text')
-
+ field_name = field_config.get("field_name")
+ field_type = field_config.get("field_type", "text")
+
value = lookup(obj, field_name)
-
+
if value is None:
- return '-'
-
- if field_type == 'currency':
+ return "-"
+
+ if field_type == "currency":
return format_currency(value)
- elif field_type == 'date':
+ elif field_type == "date":
try:
- return value.strftime('%d.%m.%Y')
+ return value.strftime("%d.%m.%Y")
except (AttributeError, ValueError):
return str(value)
- elif field_type == 'datetime':
+ elif field_type == "datetime":
try:
- return value.strftime('%d.%m.%Y %H:%M')
+ return value.strftime("%d.%m.%Y %H:%M")
except (AttributeError, ValueError):
return str(value)
- elif field_type == 'status':
+ elif field_type == "status":
return format_status_badge(value)
- elif field_type == 'boolean':
- return 'Ja' if value else 'Nein'
+ elif field_type == "boolean":
+ return "Ja" if value else "Nein"
else:
return truncate_field(value)
diff --git a/app/stiftung/urls.py b/app/stiftung/urls.py
index b51a0e1..c87a323 100644
--- a/app/stiftung/urls.py
+++ b/app/stiftung/urls.py
@@ -1,160 +1,337 @@
from django.urls import path
+
from . import views
-app_name = 'stiftung'
+app_name = "stiftung"
urlpatterns = [
# Dashboard (Startseite)
- path('', views.dashboard, name='dashboard'),
-
+ path("", views.dashboard, name="dashboard"),
# Home (für Kompatibilität mit bestehenden Templates)
- path('home/', views.home, name='home'),
-
+ path("home/", views.home, name="home"),
# CSV Import URLs
- path('import/', views.csv_import_list, name='csv_import_list'),
- path('import/neu/', views.csv_import_create, name='csv_import_create'),
-
+ path("import/", views.csv_import_list, name="csv_import_list"),
+ path("import/neu/", views.csv_import_create, name="csv_import_create"),
# Destinatär URLs (Förderungsempfänger)
- path('destinataere/', views.destinataer_list, name='destinataer_list'),
- path('destinataere//', views.destinataer_detail, name='destinataer_detail'),
- path('destinataere/neu/', views.destinataer_create, name='destinataer_create'),
- path('destinataere//bearbeiten/', views.destinataer_update, name='destinataer_update'),
- path('destinataere//loeschen/', views.destinataer_delete, name='destinataer_delete'),
- path('destinataere//notiz/', views.destinataer_notiz_create, name='destinataer_notiz_create'),
- path('destinataere//export/', views.destinataer_export, name='destinataer_export'),
-
+ path("destinataere/", views.destinataer_list, name="destinataer_list"),
+ path(
+ "destinataere//", views.destinataer_detail, name="destinataer_detail"
+ ),
+ path("destinataere/neu/", views.destinataer_create, name="destinataer_create"),
+ path(
+ "destinataere//bearbeiten/",
+ views.destinataer_update,
+ name="destinataer_update",
+ ),
+ path(
+ "destinataere//loeschen/",
+ views.destinataer_delete,
+ name="destinataer_delete",
+ ),
+ path(
+ "destinataere//notiz/",
+ views.destinataer_notiz_create,
+ name="destinataer_notiz_create",
+ ),
+ path(
+ "destinataere//export/",
+ views.destinataer_export,
+ name="destinataer_export",
+ ),
# Paechter URLs (Landpächter)
- path('paechter/', views.paechter_list, name='paechter_list'),
- path('paechter//', views.paechter_detail, name='paechter_detail'),
- path('paechter/neu/', views.paechter_create, name='paechter_create'),
- path('paechter//bearbeiten/', views.paechter_update, name='paechter_update'),
- path('paechter//loeschen/', views.paechter_delete, name='paechter_delete'),
- path('paechter//export/', views.paechter_export, name='paechter_export'),
-
+ path("paechter/", views.paechter_list, name="paechter_list"),
+ path("paechter//", views.paechter_detail, name="paechter_detail"),
+ path("paechter/neu/", views.paechter_create, name="paechter_create"),
+ path(
+ "paechter//bearbeiten/", views.paechter_update, name="paechter_update"
+ ),
+ path("paechter//loeschen/", views.paechter_delete, name="paechter_delete"),
+ path("paechter//export/", views.paechter_export, name="paechter_export"),
# Legacy Person URLs removed (Destinatäre ersetzen Personen)
-
# Land URLs
- path('laendereien/', views.land_list, name='land_list'),
- path('laendereien//', views.land_detail, name='land_detail'),
- path('laendereien/neu/', views.land_create, name='land_create'),
- path('laendereien//bearbeiten/', views.land_update, name='land_update'),
- path('laendereien//loeschen/', views.land_delete, name='land_delete'),
- path('laendereien//export/', views.land_export, name='land_export'),
-
+ path("laendereien/", views.land_list, name="land_list"),
+ path("laendereien//", views.land_detail, name="land_detail"),
+ path("laendereien/neu/", views.land_create, name="land_create"),
+ path("laendereien//bearbeiten/", views.land_update, name="land_update"),
+ path("laendereien//loeschen/", views.land_delete, name="land_delete"),
+ path("laendereien//export/", views.land_export, name="land_export"),
# Landabrechnung URLs
- path('landabrechnungen/', views.land_abrechnung_list, name='land_abrechnung_list'),
- path('landabrechnungen//', views.land_abrechnung_detail, name='land_abrechnung_detail'),
- path('landabrechnungen/neu/', views.land_abrechnung_create, name='land_abrechnung_create'),
- path('landabrechnungen//bearbeiten/', views.land_abrechnung_update, name='land_abrechnung_update'),
- path('landabrechnungen//loeschen/', views.land_abrechnung_delete, name='land_abrechnung_delete'),
-
+ path("landabrechnungen/", views.land_abrechnung_list, name="land_abrechnung_list"),
+ path(
+ "landabrechnungen//",
+ views.land_abrechnung_detail,
+ name="land_abrechnung_detail",
+ ),
+ path(
+ "landabrechnungen/neu/",
+ views.land_abrechnung_create,
+ name="land_abrechnung_create",
+ ),
+ path(
+ "landabrechnungen//bearbeiten/",
+ views.land_abrechnung_update,
+ name="land_abrechnung_update",
+ ),
+ path(
+ "landabrechnungen//loeschen/",
+ views.land_abrechnung_delete,
+ name="land_abrechnung_delete",
+ ),
# Vereinheitlichte Verpachtung URLs (direkt im Land)
- path('laendereien//verpachtung/neu/', views.land_verpachtung_create, name='land_verpachtung_create'),
- path('laendereien//verpachtung/bearbeiten/', views.land_verpachtung_edit, name='land_verpachtung_edit'),
- path('laendereien//verpachtung/beenden/', views.land_verpachtung_end, name='land_verpachtung_end'),
-
+ path(
+ "laendereien//verpachtung/neu/",
+ views.land_verpachtung_create,
+ name="land_verpachtung_create",
+ ),
+ path(
+ "laendereien//verpachtung/bearbeiten/",
+ views.land_verpachtung_edit,
+ name="land_verpachtung_edit",
+ ),
+ path(
+ "laendereien//verpachtung/beenden/",
+ views.land_verpachtung_end,
+ name="land_verpachtung_end",
+ ),
# LandVerpachtung URLs (neue Verpachtungen)
- path('laendereien/verpachtungen//', views.land_verpachtung_detail, name='land_verpachtung_detail'),
- path('laendereien/verpachtungen//bearbeiten/', views.land_verpachtung_update, name='land_verpachtung_update'),
- path('laendereien/verpachtungen//beenden/', views.land_verpachtung_end_direct, name='land_verpachtung_end_direct'),
-
+ path(
+ "laendereien/verpachtungen//",
+ views.land_verpachtung_detail,
+ name="land_verpachtung_detail",
+ ),
+ path(
+ "laendereien/verpachtungen//bearbeiten/",
+ views.land_verpachtung_update,
+ name="land_verpachtung_update",
+ ),
+ path(
+ "laendereien/verpachtungen//beenden/",
+ views.land_verpachtung_end_direct,
+ name="land_verpachtung_end_direct",
+ ),
# Förderung URLs
- path('foerderungen/', views.foerderung_list, name='foerderung_list'),
- path('foerderungen//', views.foerderung_detail, name='foerderung_detail'),
- path('foerderungen/neu/', views.foerderung_create, name='foerderung_create'),
- path('foerderungen//bearbeiten/', views.foerderung_update, name='foerderung_update'),
- path('foerderungen//loeschen/', views.foerderung_delete, name='foerderung_delete'),
-
+ path("foerderungen/", views.foerderung_list, name="foerderung_list"),
+ path("foerderungen//", views.foerderung_detail, name="foerderung_detail"),
+ path("foerderungen/neu/", views.foerderung_create, name="foerderung_create"),
+ path(
+ "foerderungen//bearbeiten/",
+ views.foerderung_update,
+ name="foerderung_update",
+ ),
+ path(
+ "foerderungen//loeschen/",
+ views.foerderung_delete,
+ name="foerderung_delete",
+ ),
# Dokumente URLs
- path('dokumente/', views.dokument_list, name='dokument_list'),
- path('dokumente//', views.dokument_detail, name='dokument_detail'),
- path('dokumente/neu/', views.dokument_create, name='dokument_create'),
- path('dokumente//bearbeiten/', views.dokument_update, name='dokument_update'),
- path('dokumente//loeschen/', views.dokument_delete, name='dokument_delete'),
-
+ path("dokumente/", views.dokument_list, name="dokument_list"),
+ path("dokumente//", views.dokument_detail, name="dokument_detail"),
+ path("dokumente/neu/", views.dokument_create, name="dokument_create"),
+ path(
+ "dokumente//bearbeiten/", views.dokument_update, name="dokument_update"
+ ),
+ path(
+ "dokumente//loeschen/", views.dokument_delete, name="dokument_delete"
+ ),
# Dokumentenverwaltung (Paperless-Integration, Verwaltung & Verknüpfung)
- path('dokumente/verwaltung/', views.dokument_management, name='dokument_management'),
-
+ path(
+ "dokumente/verwaltung/", views.dokument_management, name="dokument_management"
+ ),
# Legacy document URLs removed - use dokument_management instead
-
# Dokument-Verknüpfung
- path('api/link-document/search/', views.link_document_search, name='link_document_search'),
- path('api/link-document/create/', views.link_document_create, name='link_document_create'),
- path('api/link-document/list/', views.link_document_list, name='link_document_list'),
- path('api/link-document/update/', views.link_document_update, name='link_document_update'),
- path('api/link-document/delete//', views.link_document_delete, name='link_document_delete'),
+ path(
+ "api/link-document/search/",
+ views.link_document_search,
+ name="link_document_search",
+ ),
+ path(
+ "api/link-document/create/",
+ views.link_document_create,
+ name="link_document_create",
+ ),
+ path(
+ "api/link-document/list/", views.link_document_list, name="link_document_list"
+ ),
+ path(
+ "api/link-document/update/",
+ views.link_document_update,
+ name="link_document_update",
+ ),
+ path(
+ "api/link-document/delete//",
+ views.link_document_delete,
+ name="link_document_delete",
+ ),
# Legacy dokument_verknuepfung URL removed - use dokument_management instead
-
# Jahresbericht URLs
- path('berichte/', views.bericht_list, name='bericht_list'),
- path('berichte/jahresbericht/', views.jahresbericht_generate_redirect, name='jahresbericht_generate_redirect'),
- path('berichte/jahresbericht//', views.jahresbericht_generate, name='jahresbericht_generate'),
- path('berichte/jahresbericht//pdf/', views.jahresbericht_pdf, name='jahresbericht_pdf'),
-
+ path("berichte/", views.bericht_list, name="bericht_list"),
+ path(
+ "berichte/jahresbericht/",
+ views.jahresbericht_generate_redirect,
+ name="jahresbericht_generate_redirect",
+ ),
+ path(
+ "berichte/jahresbericht//",
+ views.jahresbericht_generate,
+ name="jahresbericht_generate",
+ ),
+ path(
+ "berichte/jahresbericht//pdf/",
+ views.jahresbericht_pdf,
+ name="jahresbericht_pdf",
+ ),
# Geschäftsführung URLs
- path('geschaeftsfuehrung/', views.geschaeftsfuehrung, name='geschaeftsfuehrung'),
- path('geschaeftsfuehrung/konten/', views.konto_list, name='konto_list'),
- path('geschaeftsfuehrung/konten/neu/', views.konto_create, name='konto_create'),
- path('geschaeftsfuehrung/konten//', views.konto_detail, name='konto_detail'),
- path('geschaeftsfuehrung/konten//bearbeiten/', views.konto_edit, name='konto_edit'),
- path('geschaeftsfuehrung/verwaltungskosten/', views.verwaltungskosten_list, name='verwaltungskosten_list'),
- path('geschaeftsfuehrung/verwaltungskosten/neu/', views.verwaltungskosten_create, name='verwaltungskosten_create'),
- path('geschaeftsfuehrung/verwaltungskosten//bearbeiten/', views.verwaltungskosten_edit, name='verwaltungskosten_edit'),
- path('verwaltungskosten/mark-paid/', views.mark_expense_paid, name='mark_expense_paid'),
- path('geschaeftsfuehrung/rentmeister/', views.rentmeister_list, name='rentmeister_list'),
- path('geschaeftsfuehrung/rentmeister/neu/', views.rentmeister_create, name='rentmeister_create'),
- path('geschaeftsfuehrung/rentmeister//', views.rentmeister_detail, name='rentmeister_detail'),
- path('geschaeftsfuehrung/rentmeister//bearbeiten/', views.rentmeister_edit, name='rentmeister_edit'),
- path('geschaeftsfuehrung/rentmeister//ausgaben/', views.rentmeister_ausgaben, name='rentmeister_ausgaben'),
-
+ path("geschaeftsfuehrung/", views.geschaeftsfuehrung, name="geschaeftsfuehrung"),
+ path("geschaeftsfuehrung/konten/", views.konto_list, name="konto_list"),
+ path("geschaeftsfuehrung/konten/neu/", views.konto_create, name="konto_create"),
+ path(
+ "geschaeftsfuehrung/konten//", views.konto_detail, name="konto_detail"
+ ),
+ path(
+ "geschaeftsfuehrung/konten//bearbeiten/",
+ views.konto_edit,
+ name="konto_edit",
+ ),
+ path(
+ "geschaeftsfuehrung/verwaltungskosten/",
+ views.verwaltungskosten_list,
+ name="verwaltungskosten_list",
+ ),
+ path(
+ "geschaeftsfuehrung/verwaltungskosten/neu/",
+ views.verwaltungskosten_create,
+ name="verwaltungskosten_create",
+ ),
+ path(
+ "geschaeftsfuehrung/verwaltungskosten//bearbeiten/",
+ views.verwaltungskosten_edit,
+ name="verwaltungskosten_edit",
+ ),
+ path(
+ "verwaltungskosten/mark-paid/",
+ views.mark_expense_paid,
+ name="mark_expense_paid",
+ ),
+ path(
+ "geschaeftsfuehrung/rentmeister/",
+ views.rentmeister_list,
+ name="rentmeister_list",
+ ),
+ path(
+ "geschaeftsfuehrung/rentmeister/neu/",
+ views.rentmeister_create,
+ name="rentmeister_create",
+ ),
+ path(
+ "geschaeftsfuehrung/rentmeister//",
+ views.rentmeister_detail,
+ name="rentmeister_detail",
+ ),
+ path(
+ "geschaeftsfuehrung/rentmeister//bearbeiten/",
+ views.rentmeister_edit,
+ name="rentmeister_edit",
+ ),
+ path(
+ "geschaeftsfuehrung/rentmeister//ausgaben/",
+ views.rentmeister_ausgaben,
+ name="rentmeister_ausgaben",
+ ),
# Administration URLs
- path('administration/', views.administration, name='administration'),
- path('administration/settings/', views.app_settings, name='app_settings'),
- path('administration/audit-log/', views.audit_log_list, name='audit_log_list'),
- path('administration/backup/', views.backup_management, name='backup_management'),
- path('administration/backup//download/', views.backup_download, name='backup_download'),
- path('administration/backup/restore/', views.backup_restore, name='backup_restore'),
- path('administration/unterstuetzungen/', views.unterstuetzungen_list, name='unterstuetzungen_list'),
- path('administration/unterstuetzungen//bearbeiten/', views.unterstuetzung_edit, name='unterstuetzung_edit'),
- path('administration/unterstuetzungen//loeschen/', views.unterstuetzung_delete, name='unterstuetzung_delete'),
-
+ path("administration/", views.administration, name="administration"),
+ path("administration/settings/", views.app_settings, name="app_settings"),
+ path("administration/audit-log/", views.audit_log_list, name="audit_log_list"),
+ path("administration/backup/", views.backup_management, name="backup_management"),
+ path(
+ "administration/backup//download/",
+ views.backup_download,
+ name="backup_download",
+ ),
+ path("administration/backup/restore/", views.backup_restore, name="backup_restore"),
+ path(
+ "administration/unterstuetzungen/",
+ views.unterstuetzungen_list,
+ name="unterstuetzungen_list",
+ ),
+ path(
+ "administration/unterstuetzungen//bearbeiten/",
+ views.unterstuetzung_edit,
+ name="unterstuetzung_edit",
+ ),
+ path(
+ "administration/unterstuetzungen//loeschen/",
+ views.unterstuetzung_delete,
+ name="unterstuetzung_delete",
+ ),
# Unterstützungen URLs (direct access from Destinataer)
- path('unterstuetzungen/', views.unterstuetzungen_all, name='unterstuetzungen_all'),
- path('unterstuetzungen/neu/', views.unterstuetzung_create, name='unterstuetzung_create'),
- path('unterstuetzungen//', views.unterstuetzung_detail, name='unterstuetzung_detail'),
- path('unterstuetzungen//bezahlt/', views.unterstuetzung_mark_paid, name='unterstuetzung_mark_paid'),
- path('unterstuetzungen/wiederkehrend/', views.wiederkehrende_unterstuetzungen, name='wiederkehrende_unterstuetzungen'),
-
+ path("unterstuetzungen/", views.unterstuetzungen_all, name="unterstuetzungen_all"),
+ path(
+ "unterstuetzungen/neu/",
+ views.unterstuetzung_create,
+ name="unterstuetzung_create",
+ ),
+ path(
+ "unterstuetzungen//",
+ views.unterstuetzung_detail,
+ name="unterstuetzung_detail",
+ ),
+ path(
+ "unterstuetzungen//bezahlt/",
+ views.unterstuetzung_mark_paid,
+ name="unterstuetzung_mark_paid",
+ ),
+ path(
+ "unterstuetzungen/wiederkehrend/",
+ views.wiederkehrende_unterstuetzungen,
+ name="wiederkehrende_unterstuetzungen",
+ ),
# AJAX endpoints
- path('api/destinataer//info/', views.get_destinataer_info, name='get_destinataer_info'),
-
+ path(
+ "api/destinataer//info/",
+ views.get_destinataer_info,
+ name="get_destinataer_info",
+ ),
# Authentication URLs
- path('login/', views.user_login, name='login'),
- path('logout/', views.user_logout, name='logout'),
-
+ path("login/", views.user_login, name="login"),
+ path("logout/", views.user_logout, name="logout"),
# User Management URLs
- path('administration/users/', views.user_management, name='user_management'),
- path('administration/users/create/', views.user_create, name='user_create'),
- path('administration/users//', views.user_detail, name='user_detail'),
- path('administration/users//edit/', views.user_edit, name='user_edit'),
- path('administration/users//password/', views.user_change_password, name='user_change_password'),
- path('administration/users//permissions/', views.user_permissions, name='user_permissions'),
- path('administration/users//delete/', views.user_delete, name='user_delete'),
-
+ path("administration/users/", views.user_management, name="user_management"),
+ path("administration/users/create/", views.user_create, name="user_create"),
+ path("administration/users//", views.user_detail, name="user_detail"),
+ path("administration/users//edit/", views.user_edit, name="user_edit"),
+ path(
+ "administration/users//password/",
+ views.user_change_password,
+ name="user_change_password",
+ ),
+ path(
+ "administration/users//permissions/",
+ views.user_permissions,
+ name="user_permissions",
+ ),
+ path(
+ "administration/users//delete/", views.user_delete, name="user_delete"
+ ),
# Hilfsbox URLs
- path('help-box/edit/', views.edit_help_box, name='edit_help_box'),
- path('help-box/admin/', views.edit_help_box, name='help_boxes_admin'),
-
+ path("help-box/edit/", views.edit_help_box, name="edit_help_box"),
+ path("help-box/admin/", views.edit_help_box, name="help_boxes_admin"),
# API URLs
- path('api/land-stats/', views.land_stats_api, name='land_stats_api'),
- path('api/health/', views.health_check, name='health_check'),
- path('api/paperless/ping/', views.paperless_ping, name='paperless_ping'),
- path('api/paperless/documents/', views.paperless_documents, name='paperless_documents'),
- path('api/paperless/tags/', views.paperless_tags_only, name='paperless_tags_only'),
- path('api/paperless/debug/', views.paperless_debug, name='paperless_debug'),
- path('api/paperless/documents//', views.paperless_document_redirect, name='paperless_document_redirect'),
-
+ path("api/land-stats/", views.land_stats_api, name="land_stats_api"),
+ path("api/health/", views.health_check, name="health_check"),
+ path("api/paperless/ping/", views.paperless_ping, name="paperless_ping"),
+ path(
+ "api/paperless/documents/",
+ views.paperless_documents,
+ name="paperless_documents",
+ ),
+ path("api/paperless/tags/", views.paperless_tags_only, name="paperless_tags_only"),
+ path("api/paperless/debug/", views.paperless_debug, name="paperless_debug"),
+ path(
+ "api/paperless/documents//",
+ views.paperless_document_redirect,
+ name="paperless_document_redirect",
+ ),
# Gramps integration (probe)
- path('api/gramps/search/', views.gramps_search_api, name='gramps_search_api'),
- path('api/gramps/debug/', views.gramps_debug_api, name='gramps_debug_api'),
+ path("api/gramps/search/", views.gramps_search_api, name="gramps_search_api"),
+ path("api/gramps/debug/", views.gramps_debug_api, name="gramps_debug_api"),
]
diff --git a/app/stiftung/utils/config.py b/app/stiftung/utils/config.py
index d6c612d..8862cb5 100644
--- a/app/stiftung/utils/config.py
+++ b/app/stiftung/utils/config.py
@@ -1,61 +1,65 @@
"""
Configuration utilities for the Stiftung application
"""
+
from django.conf import settings
+
from stiftung.models import AppConfiguration
def get_config(key, default=None, fallback_to_settings=True):
"""
Get a configuration value from the database or fall back to Django settings
-
+
Args:
key: The configuration key
default: Default value if not found
fallback_to_settings: If True, try to get from Django settings using the key in uppercase
-
+
Returns:
The configuration value
"""
# Try to get from AppConfiguration first
value = AppConfiguration.get_setting(key, None)
-
+
# Fall back to Django settings if value is None or empty string
if not value and fallback_to_settings:
settings_key = key.upper()
return getattr(settings, settings_key, default)
-
+
return value if value is not None else default
def get_paperless_config():
"""
Get all Paperless-related configuration values
-
+
Returns:
dict: Dictionary containing all Paperless configuration
"""
return {
- 'api_url': get_config('paperless_api_url', 'http://192.168.178.167:30070'),
- 'api_token': get_config('paperless_api_token', ''),
- 'destinataere_tag': get_config('paperless_destinataere_tag', 'Stiftung_Destinatäre'),
- 'destinataere_tag_id': get_config('paperless_destinataere_tag_id', '210'),
- 'land_tag': get_config('paperless_land_tag', 'Stiftung_Land_und_Pächter'),
- 'land_tag_id': get_config('paperless_land_tag_id', '204'),
- 'admin_tag': get_config('paperless_admin_tag', 'Stiftung_Administration'),
- 'admin_tag_id': get_config('paperless_admin_tag_id', '216'),
+ "api_url": get_config("paperless_api_url", "http://192.168.178.167:30070"),
+ "api_token": get_config("paperless_api_token", ""),
+ "destinataere_tag": get_config(
+ "paperless_destinataere_tag", "Stiftung_Destinatäre"
+ ),
+ "destinataere_tag_id": get_config("paperless_destinataere_tag_id", "210"),
+ "land_tag": get_config("paperless_land_tag", "Stiftung_Land_und_Pächter"),
+ "land_tag_id": get_config("paperless_land_tag_id", "204"),
+ "admin_tag": get_config("paperless_admin_tag", "Stiftung_Administration"),
+ "admin_tag_id": get_config("paperless_admin_tag_id", "216"),
}
def set_config(key, value, **kwargs):
"""
Set a configuration value
-
+
Args:
key: The configuration key
value: The value to set
**kwargs: Additional parameters for AppConfiguration.set_setting
-
+
Returns:
AppConfiguration: The configuration object
"""
@@ -65,9 +69,9 @@ def set_config(key, value, **kwargs):
def is_paperless_configured():
"""
Check if Paperless is properly configured
-
+
Returns:
bool: True if API URL and token are configured
"""
config = get_paperless_config()
- return bool(config['api_url'] and config['api_token'])
+ return bool(config["api_url"] and config["api_token"])
diff --git a/app/stiftung/utils/pdf_generator.py b/app/stiftung/utils/pdf_generator.py
index fe19e38..aa3d34c 100644
--- a/app/stiftung/utils/pdf_generator.py
+++ b/app/stiftung/utils/pdf_generator.py
@@ -1,18 +1,21 @@
"""
PDF generation utilities with corporate identity support
"""
-import os
+
import base64
+import os
from io import BytesIO
+
from django.conf import settings
-from django.template.loader import render_to_string
from django.http import HttpResponse
+from django.template.loader import render_to_string
from django.utils import timezone
# Try to import WeasyPrint, fall back gracefully if not available
try:
- from weasyprint import HTML, CSS
+ from weasyprint import CSS, HTML
from weasyprint.text.fonts import FontConfiguration
+
WEASYPRINT_AVAILABLE = True
IMPORT_ERROR = None
except ImportError as e:
@@ -35,72 +38,84 @@ from stiftung.models import AppConfiguration
class PDFGenerator:
"""Corporate identity PDF generator"""
-
+
def __init__(self):
if WEASYPRINT_AVAILABLE:
self.font_config = FontConfiguration()
else:
self.font_config = None
-
+
def is_available(self):
"""Check if PDF generation is available"""
return WEASYPRINT_AVAILABLE
-
+
def get_corporate_settings(self):
"""Get corporate identity settings from configuration"""
return {
- 'stiftung_name': AppConfiguration.get_setting('corporate_stiftung_name', 'Stiftung'),
- 'logo_path': AppConfiguration.get_setting('corporate_logo_path', ''),
- 'primary_color': AppConfiguration.get_setting('corporate_primary_color', '#2c3e50'),
- 'secondary_color': AppConfiguration.get_setting('corporate_secondary_color', '#3498db'),
- 'address_line1': AppConfiguration.get_setting('corporate_address_line1', ''),
- 'address_line2': AppConfiguration.get_setting('corporate_address_line2', ''),
- 'phone': AppConfiguration.get_setting('corporate_phone', ''),
- 'email': AppConfiguration.get_setting('corporate_email', ''),
- 'website': AppConfiguration.get_setting('corporate_website', ''),
- 'footer_text': AppConfiguration.get_setting('corporate_footer_text', 'Dieser Bericht wurde automatisch generiert.'),
+ "stiftung_name": AppConfiguration.get_setting(
+ "corporate_stiftung_name", "Stiftung"
+ ),
+ "logo_path": AppConfiguration.get_setting("corporate_logo_path", ""),
+ "primary_color": AppConfiguration.get_setting(
+ "corporate_primary_color", "#2c3e50"
+ ),
+ "secondary_color": AppConfiguration.get_setting(
+ "corporate_secondary_color", "#3498db"
+ ),
+ "address_line1": AppConfiguration.get_setting(
+ "corporate_address_line1", ""
+ ),
+ "address_line2": AppConfiguration.get_setting(
+ "corporate_address_line2", ""
+ ),
+ "phone": AppConfiguration.get_setting("corporate_phone", ""),
+ "email": AppConfiguration.get_setting("corporate_email", ""),
+ "website": AppConfiguration.get_setting("corporate_website", ""),
+ "footer_text": AppConfiguration.get_setting(
+ "corporate_footer_text", "Dieser Bericht wurde automatisch generiert."
+ ),
}
-
+
def get_logo_base64(self, logo_path):
"""Convert logo to base64 for embedding in PDF"""
if not logo_path:
return None
-
+
# Try different possible paths
possible_paths = [
logo_path,
os.path.join(settings.MEDIA_ROOT, logo_path),
- os.path.join(settings.STATIC_ROOT or '', logo_path),
- os.path.join(settings.BASE_DIR, 'static', logo_path),
+ os.path.join(settings.STATIC_ROOT or "", logo_path),
+ os.path.join(settings.BASE_DIR, "static", logo_path),
]
-
+
for path in possible_paths:
if os.path.exists(path):
try:
- with open(path, 'rb') as img_file:
- img_data = base64.b64encode(img_file.read()).decode('utf-8')
+ with open(path, "rb") as img_file:
+ img_data = base64.b64encode(img_file.read()).decode("utf-8")
# Determine MIME type
ext = os.path.splitext(path)[1].lower()
- if ext in ['.jpg', '.jpeg']:
- mime_type = 'image/jpeg'
- elif ext == '.png':
- mime_type = 'image/png'
- elif ext == '.svg':
- mime_type = 'image/svg+xml'
+ if ext in [".jpg", ".jpeg"]:
+ mime_type = "image/jpeg"
+ elif ext == ".png":
+ mime_type = "image/png"
+ elif ext == ".svg":
+ mime_type = "image/svg+xml"
else:
- mime_type = 'image/png' # default
-
+ mime_type = "image/png" # default
+
return f"data:{mime_type};base64,{img_data}"
except Exception:
continue
-
+
return None
-
+
def get_base_css(self, corporate_settings):
"""Generate base CSS for corporate identity"""
- primary_color = corporate_settings.get('primary_color', '#2c3e50')
- secondary_color = corporate_settings.get('secondary_color', '#3498db')
-
+ primary_color = corporate_settings.get("primary_color", "#2c3e50")
+ secondary_color = corporate_settings.get("secondary_color", "#3498db")
+
return f"""
@page {{
size: A4;
@@ -291,7 +306,7 @@ class PDFGenerator:
page-break-before: always;
}}
"""
-
+
def generate_pdf_response(self, html_content, filename, css_content=None):
"""Generate PDF response from HTML content"""
if not WEASYPRINT_AVAILABLE:
@@ -320,27 +335,30 @@ class PDFGenerator: