- forms.py → forms/ Package (8 Domänen: destinataere, land, finanzen, foerderung, dokumente, veranstaltung, system, geschichte) - admin.py → admin/ Package (7 Domänen, alle 22 @admin.register dekoriert) - views.py (8845 Zeilen) → views/ Package (10 Domänen: dashboard, destinataere, land, paechter, finanzen, foerderung, dokumente, unterstuetzungen, veranstaltung, geschichte, system) - __init__.py in jedem Package re-exportiert alle Symbole für Rückwärtskompatibilität - urls.py bleibt unverändert (funktioniert durch Re-Exports) - Django system check: 0 Fehler, alle URL-Auflösungen funktionieren Keine funktionalen Änderungen – reine Strukturverbesserung für Vision 2026. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
580 lines
16 KiB
Python
580 lines
16 KiB
Python
from django.contrib import admin
|
|
from django.db.models import Sum
|
|
from django.utils import timezone
|
|
from django.utils.html import format_html
|
|
|
|
from .. import models
|
|
from ..models import (AppConfiguration, AuditLog, BackupJob, CSVImport, Person,
|
|
UnterstuetzungWiederkehrend, VierteljahresNachweis)
|
|
|
|
|
|
@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"]
|
|
|
|
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")}),
|
|
)
|
|
|
|
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:
|
|
color = "success"
|
|
elif rate >= 70:
|
|
color = "warning"
|
|
else:
|
|
color = "danger"
|
|
return format_html('<span class="badge bg-{}">{:.1f}%</span>', 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"]
|
|
|
|
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",)}),
|
|
)
|
|
|
|
def iban_display(self, obj):
|
|
if obj.iban:
|
|
return format_html(
|
|
'<span style="font-family: monospace;">{}</span>', obj.iban
|
|
)
|
|
return "-"
|
|
|
|
iban_display.short_description = "IBAN"
|
|
|
|
def get_queryset(self, request):
|
|
return (
|
|
super()
|
|
.get_queryset(request)
|
|
.annotate(total_foerderungen=Sum("foerderung__betrag"))
|
|
)
|
|
|
|
|
|
@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"
|
|
|
|
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"]}),
|
|
)
|
|
|
|
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"]
|
|
|
|
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"]}),
|
|
)
|
|
|
|
def backup_size_display(self, obj):
|
|
return obj.get_size_display()
|
|
|
|
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"]
|
|
|
|
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",)},
|
|
),
|
|
)
|
|
|
|
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(
|
|
'<a href="{}" target="_blank">{}</a>',
|
|
value,
|
|
value[:50] + "..." if len(value) > 50 else value,
|
|
)
|
|
elif len(value) > 100:
|
|
return value[:100] + "..."
|
|
return 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"])
|
|
return readonly
|
|
|
|
|
|
@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"]
|
|
|
|
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",),
|
|
},
|
|
),
|
|
)
|
|
|
|
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
|
|
obj.updated_by = request.user.username
|
|
super().save_model(request, obj, form, change)
|
|
|
|
|
|
@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"]
|
|
|
|
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",)},
|
|
),
|
|
)
|
|
|
|
|
|
@admin.register(VierteljahresNachweis)
|
|
class VierteljahresNachweisAdmin(admin.ModelAdmin):
|
|
list_display = [
|
|
"destinataer",
|
|
"jahr",
|
|
"quartal",
|
|
"status",
|
|
"completion_percentage",
|
|
"faelligkeitsdatum",
|
|
"is_overdue_display",
|
|
"eingereicht_am",
|
|
"geprueft_von",
|
|
]
|
|
list_filter = [
|
|
"jahr",
|
|
"quartal",
|
|
"status",
|
|
"studiennachweis_erforderlich",
|
|
"studiennachweis_eingereicht",
|
|
"einkommenssituation_bestaetigt",
|
|
"vermogenssituation_bestaetigt",
|
|
"faelligkeitsdatum",
|
|
]
|
|
search_fields = [
|
|
"destinataer__vorname",
|
|
"destinataer__nachname",
|
|
"destinataer__email",
|
|
]
|
|
readonly_fields = [
|
|
"id",
|
|
"erstellt_am",
|
|
"aktualisiert_am",
|
|
"completion_percentage",
|
|
"is_overdue_display",
|
|
]
|
|
ordering = ["-jahr", "-quartal", "destinataer__nachname"]
|
|
|
|
fieldsets = (
|
|
(
|
|
"Grundinformationen",
|
|
{
|
|
"fields": (
|
|
"destinataer",
|
|
"jahr",
|
|
"quartal",
|
|
"status",
|
|
"faelligkeitsdatum",
|
|
)
|
|
},
|
|
),
|
|
(
|
|
"Studiennachweis",
|
|
{
|
|
"fields": (
|
|
"studiennachweis_erforderlich",
|
|
"studiennachweis_eingereicht",
|
|
"studiennachweis_datei",
|
|
"studiennachweis_bemerkung",
|
|
),
|
|
"classes": ("collapse",),
|
|
},
|
|
),
|
|
(
|
|
"Einkommenssituation",
|
|
{
|
|
"fields": (
|
|
"einkommenssituation_bestaetigt",
|
|
"einkommenssituation_text",
|
|
"einkommenssituation_datei",
|
|
),
|
|
"classes": ("collapse",),
|
|
},
|
|
),
|
|
(
|
|
"Vermögenssituation",
|
|
{
|
|
"fields": (
|
|
"vermogenssituation_bestaetigt",
|
|
"vermogenssituation_text",
|
|
"vermogenssituation_datei",
|
|
),
|
|
"classes": ("collapse",),
|
|
},
|
|
),
|
|
(
|
|
"Weitere Dokumente",
|
|
{
|
|
"fields": (
|
|
"weitere_dokumente",
|
|
"weitere_dokumente_beschreibung",
|
|
),
|
|
"classes": ("collapse",),
|
|
},
|
|
),
|
|
(
|
|
"Verwaltung & Prüfung",
|
|
{
|
|
"fields": (
|
|
"interne_notizen",
|
|
"eingereicht_am",
|
|
"geprueft_am",
|
|
"geprueft_von",
|
|
),
|
|
"classes": ("collapse",),
|
|
},
|
|
),
|
|
(
|
|
"Metadaten",
|
|
{
|
|
"fields": (
|
|
"id",
|
|
"erstellt_am",
|
|
"aktualisiert_am",
|
|
"completion_percentage",
|
|
"is_overdue_display",
|
|
)
|
|
},
|
|
),
|
|
)
|
|
|
|
def completion_percentage(self, obj):
|
|
"""Show completion percentage as colored badge"""
|
|
percentage = obj.get_completion_percentage()
|
|
if percentage == 100:
|
|
color = "success"
|
|
elif percentage >= 70:
|
|
color = "info"
|
|
elif percentage >= 30:
|
|
color = "warning"
|
|
else:
|
|
color = "danger"
|
|
|
|
return format_html(
|
|
'<span class="badge bg-{}">{} %</span>',
|
|
color,
|
|
percentage
|
|
)
|
|
completion_percentage.short_description = "Fortschritt"
|
|
|
|
def is_overdue_display(self, obj):
|
|
"""Display overdue status with icon"""
|
|
if obj.is_overdue():
|
|
return format_html(
|
|
'<span class="text-danger"><i class="fas fa-exclamation-triangle"></i> Ja</span>'
|
|
)
|
|
return format_html(
|
|
'<span class="text-success"><i class="fas fa-check"></i> Nein</span>'
|
|
)
|
|
is_overdue_display.short_description = "Überfällig"
|
|
|
|
actions = ["mark_as_approved", "mark_as_needs_revision"]
|
|
|
|
def mark_as_approved(self, request, queryset):
|
|
"""Bulk action to approve submitted confirmations"""
|
|
count = 0
|
|
for nachweis in queryset.filter(status="eingereicht"):
|
|
nachweis.status = "geprueft"
|
|
nachweis.geprueft_am = timezone.now()
|
|
nachweis.geprueft_von = request.user
|
|
nachweis.save()
|
|
count += 1
|
|
|
|
if count:
|
|
self.message_user(
|
|
request,
|
|
f"{count} Nachweise wurden als geprüft und freigegeben markiert."
|
|
)
|
|
else:
|
|
self.message_user(
|
|
request,
|
|
"Keine eingereichten Nachweise gefunden.",
|
|
level="warning"
|
|
)
|
|
mark_as_approved.short_description = "Ausgewählte Nachweise freigeben"
|
|
|
|
def mark_as_needs_revision(self, request, queryset):
|
|
"""Bulk action to mark confirmations as needing revision"""
|
|
count = queryset.exclude(status__in=["offen", "nachbesserung"]).update(
|
|
status="nachbesserung"
|
|
)
|
|
if count:
|
|
self.message_user(
|
|
request,
|
|
f"{count} Nachweise wurden als nachbesserungsbedürftig markiert."
|
|
)
|
|
mark_as_needs_revision.short_description = "Nachbesserung erforderlich markieren"
|