feat: add comprehensive GitHub workflow and development tools
This commit is contained in:
0
app/stiftung/__init__.py
Normal file
0
app/stiftung/__init__.py
Normal file
547
app/stiftung/admin.py
Normal file
547
app/stiftung/admin.py
Normal file
@@ -0,0 +1,547 @@
|
||||
from django.contrib import admin
|
||||
from django.utils.html import format_html
|
||||
from django.urls import reverse
|
||||
from django.db.models import Sum, Count
|
||||
from django.utils.safestring import mark_safe
|
||||
from .models import (
|
||||
Person, Paechter, Destinataer, DokumentLink, Foerderung, Land, Verpachtung, CSVImport,
|
||||
Rentmeister, StiftungsKonto, Verwaltungskosten, BankTransaction, AuditLog, BackupJob
|
||||
)
|
||||
|
||||
@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(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']
|
||||
|
||||
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',)
|
||||
}),
|
||||
)
|
||||
|
||||
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'
|
||||
|
||||
@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']
|
||||
|
||||
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',)
|
||||
}),
|
||||
)
|
||||
|
||||
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'
|
||||
|
||||
@admin.register(Land)
|
||||
class LandAdmin(admin.ModelAdmin):
|
||||
list_display = [
|
||||
'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']
|
||||
|
||||
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',)
|
||||
}),
|
||||
)
|
||||
|
||||
def verpachtungsgrad_display(self, obj):
|
||||
grad = obj.get_verpachtungsgrad()
|
||||
if grad > 90:
|
||||
color = 'green'
|
||||
elif grad > 70:
|
||||
color = 'orange'
|
||||
else:
|
||||
color = 'red'
|
||||
return format_html('<span style="color: {};">{:.1f}%</span>', color, grad)
|
||||
verpachtungsgrad_display.short_description = 'Verpachtungsgrad'
|
||||
|
||||
def gesamtflaeche_berechnet(self, obj):
|
||||
return f"{obj.get_gesamtflaeche():.2f} qm"
|
||||
gesamtflaeche_berechnet.short_description = 'Berechnete Gesamtfläche'
|
||||
|
||||
def verpachtungsgrad_berechnet(self, obj):
|
||||
return f"{obj.get_verpachtungsgrad():.1f}%"
|
||||
verpachtungsgrad_berechnet.short_description = 'Verpachtungsgrad'
|
||||
|
||||
@admin.register(Verpachtung)
|
||||
class VerpachtungAdmin(admin.ModelAdmin):
|
||||
list_display = [
|
||||
'vertragsnummer', 'land', 'paechter', 'pachtbeginn', 'pachtende',
|
||||
'pachtzins_jaehrlich', 'verpachtete_flaeche', 'status', 'restlaufzeit'
|
||||
]
|
||||
list_filter = ['status', 'pachtbeginn', 'pachtende', 'land__gemeinde']
|
||||
search_fields = ['vertragsnummer', 'land__gemeinde', 'paechter__nachname', 'paechter__vorname']
|
||||
ordering = ['-pachtbeginn']
|
||||
readonly_fields = ['id', 'vertragsdauer_tage', 'restlaufzeit_tage', 'is_aktiv_status']
|
||||
|
||||
fieldsets = (
|
||||
('Vertragsdaten', {
|
||||
'fields': ('vertragsnummer', 'land', 'paechter', 'pachtbeginn', 'pachtende', 'verlaengerung')
|
||||
}),
|
||||
('Finanzielle Bedingungen', {
|
||||
'fields': ('pachtzins_pro_qm', 'pachtzins_jaehrlich')
|
||||
}),
|
||||
('Flächenangaben', {
|
||||
'fields': ('verpachtete_flaeche',)
|
||||
}),
|
||||
('Status', {
|
||||
'fields': ('status',)
|
||||
}),
|
||||
('Dokumentation', {
|
||||
'fields': ('verwendungsnachweis', 'bemerkungen')
|
||||
}),
|
||||
('System', {
|
||||
'fields': ('id', 'erstellt_am', 'aktualisiert_am'),
|
||||
'classes': ('collapse',)
|
||||
}),
|
||||
)
|
||||
|
||||
def restlaufzeit(self, obj):
|
||||
tage = obj.get_restlaufzeit_tage()
|
||||
if tage > 0:
|
||||
if tage < 30:
|
||||
color = 'red'
|
||||
elif tage < 90:
|
||||
color = 'orange'
|
||||
else:
|
||||
color = 'green'
|
||||
return format_html('<span style="color: {};">{} Tage</span>', color, tage)
|
||||
return 'Abgelaufen'
|
||||
restlaufzeit.short_description = 'Restlaufzeit'
|
||||
|
||||
def vertragsdauer_tage(self, obj):
|
||||
return f"{obj.get_vertragsdauer_tage()} Tage"
|
||||
vertragsdauer_tage.short_description = 'Vertragsdauer'
|
||||
|
||||
def restlaufzeit_tage(self, obj):
|
||||
return f"{obj.get_restlaufzeit_tage()} Tage"
|
||||
restlaufzeit_tage.short_description = 'Restlaufzeit'
|
||||
|
||||
def is_aktiv_status(self, obj):
|
||||
if obj.is_aktiv():
|
||||
return format_html('<span style="color: green;">✓ Aktiv</span>')
|
||||
return format_html('<span style="color: red;">✗ Inaktiv</span>')
|
||||
is_aktiv_status.short_description = 'Aktueller Status'
|
||||
|
||||
@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']
|
||||
|
||||
fieldsets = (
|
||||
('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']
|
||||
|
||||
fieldsets = (
|
||||
('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(
|
||||
'<a href="{}">{}</a>',
|
||||
reverse('admin:stiftung_dokumentlink_change', args=[obj.verwendungsnachweis.id]),
|
||||
obj.verwendungsnachweis.titel
|
||||
)
|
||||
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
|
||||
return f"€{total:,.2f}"
|
||||
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']
|
||||
|
||||
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']
|
||||
})
|
||||
)
|
||||
|
||||
|
||||
@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']
|
||||
|
||||
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']
|
||||
})
|
||||
)
|
||||
|
||||
|
||||
@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'
|
||||
|
||||
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']
|
||||
})
|
||||
)
|
||||
|
||||
|
||||
@admin.register(BankTransaction)
|
||||
class BankTransactionAdmin(admin.ModelAdmin):
|
||||
list_display = [
|
||||
'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']
|
||||
|
||||
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',)
|
||||
}),
|
||||
)
|
||||
|
||||
def get_queryset(self, request):
|
||||
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'
|
||||
|
||||
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
|
||||
|
||||
|
||||
# Customize admin site
|
||||
admin.site.site_header = "Stiftungsverwaltung Administration"
|
||||
admin.site.site_title = "Stiftungsverwaltung Admin"
|
||||
admin.site.index_title = "Willkommen zur Stiftungsverwaltung"
|
||||
5
app/stiftung/apps.py
Normal file
5
app/stiftung/apps.py
Normal file
@@ -0,0 +1,5 @@
|
||||
from django.apps import AppConfig
|
||||
|
||||
class StiftungConfig(AppConfig):
|
||||
default_auto_field = 'django.db.models.BigAutoField'
|
||||
name = 'stiftung'
|
||||
281
app/stiftung/audit.py
Normal file
281
app/stiftung/audit.py
Normal file
@@ -0,0 +1,281 @@
|
||||
"""
|
||||
Audit Logging Utilities
|
||||
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 stiftung.models import AuditLog
|
||||
|
||||
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')
|
||||
if x_forwarded_for:
|
||||
ip = x_forwarded_for.split(',')[0]
|
||||
else:
|
||||
ip = request.META.get('REMOTE_ADDR')
|
||||
return ip
|
||||
|
||||
|
||||
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.)
|
||||
entity_type: Type of entity (destinataer, land, etc.)
|
||||
entity_id: ID of the entity
|
||||
entity_name: Human-readable name of the entity
|
||||
description: Description of the action
|
||||
changes: Dictionary of field changes (optional)
|
||||
"""
|
||||
user = request.user if request.user.is_authenticated else None
|
||||
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 ''
|
||||
|
||||
# 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_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
|
||||
)
|
||||
|
||||
return audit_entry
|
||||
|
||||
|
||||
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',
|
||||
entity_type=entity_type,
|
||||
entity_id=entity_id,
|
||||
entity_name=entity_name,
|
||||
description=description
|
||||
)
|
||||
|
||||
|
||||
def log_update(request, entity_type, entity_id, entity_name, changes, description=None):
|
||||
"""Log entity update with field changes"""
|
||||
if not description:
|
||||
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',
|
||||
entity_type=entity_type,
|
||||
entity_id=entity_id,
|
||||
entity_name=entity_name,
|
||||
description=description,
|
||||
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"
|
||||
|
||||
return log_action(
|
||||
request=request,
|
||||
action='delete',
|
||||
entity_type=entity_type,
|
||||
entity_id=entity_id,
|
||||
entity_name=entity_name,
|
||||
description=description
|
||||
)
|
||||
|
||||
|
||||
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',
|
||||
entity_type=entity_type,
|
||||
entity_id=entity_id,
|
||||
entity_name=entity_name,
|
||||
description=description
|
||||
)
|
||||
|
||||
|
||||
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',
|
||||
entity_type=entity_type,
|
||||
entity_id=entity_id,
|
||||
entity_name=entity_name,
|
||||
description=description
|
||||
)
|
||||
|
||||
|
||||
def log_system_action(request, action, description, details=None):
|
||||
"""Log system-level actions like backup, restore, etc."""
|
||||
return log_action(
|
||||
request=request,
|
||||
action=action,
|
||||
entity_type='system',
|
||||
entity_id='',
|
||||
entity_name='System',
|
||||
description=description,
|
||||
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
|
||||
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']
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
return changes
|
||||
|
||||
|
||||
class AuditLogMixin:
|
||||
"""
|
||||
Mixin for views that provides audit logging functionality
|
||||
"""
|
||||
audit_entity_type = None
|
||||
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:
|
||||
return self.model.__name__.lower()
|
||||
|
||||
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__'):
|
||||
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)
|
||||
)
|
||||
|
||||
def log_update_action(self, old_instance, new_instance):
|
||||
"""Log update of an instance"""
|
||||
changes = track_model_changes(old_instance, new_instance)
|
||||
if changes: # Only log if there are actual changes
|
||||
log_update(
|
||||
request=self.request,
|
||||
entity_type=self.get_audit_entity_type(),
|
||||
entity_id=new_instance.pk,
|
||||
entity_name=self.get_audit_entity_name(new_instance),
|
||||
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)
|
||||
)
|
||||
|
||||
|
||||
# Simple login/logout audit helpers expected by views
|
||||
def log_login(request, user):
|
||||
"""Log a successful user login."""
|
||||
try:
|
||||
return log_action(
|
||||
request=request,
|
||||
action='login',
|
||||
entity_type='user',
|
||||
entity_id=user.pk,
|
||||
entity_name=user.get_username(),
|
||||
description=f"User '{user.get_username()}' logged in"
|
||||
)
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
|
||||
def log_logout(request, user):
|
||||
"""Log a successful user logout."""
|
||||
try:
|
||||
username = user.get_username() if user else 'Unknown'
|
||||
return log_action(
|
||||
request=request,
|
||||
action='logout',
|
||||
entity_type='user',
|
||||
entity_id=getattr(user, 'pk', ''),
|
||||
entity_name=username,
|
||||
description=f"User '{username}' logged out"
|
||||
)
|
||||
except Exception:
|
||||
return None
|
||||
320
app/stiftung/backup_utils.py
Normal file
320
app/stiftung/backup_utils.py
Normal file
@@ -0,0 +1,320 @@
|
||||
"""
|
||||
Backup and Restore Utilities
|
||||
Handles creation and restoration of complete system backups
|
||||
"""
|
||||
|
||||
import os
|
||||
import shutil
|
||||
import subprocess
|
||||
import tempfile
|
||||
import tarfile
|
||||
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'
|
||||
os.makedirs(backup_dir, exist_ok=True)
|
||||
return backup_dir
|
||||
|
||||
|
||||
def run_backup(backup_job_id):
|
||||
"""
|
||||
Run a backup job
|
||||
This runs in a separate thread to avoid blocking the web interface
|
||||
"""
|
||||
try:
|
||||
backup_job = BackupJob.objects.get(id=backup_job_id)
|
||||
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')
|
||||
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')
|
||||
os.makedirs(staging_dir)
|
||||
|
||||
# 1. Database backup
|
||||
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']:
|
||||
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.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.error_message = str(e)
|
||||
backup_job.completed_at = timezone.now()
|
||||
backup_job.save()
|
||||
|
||||
|
||||
def create_database_backup(staging_dir):
|
||||
"""Create a database backup using pg_dump"""
|
||||
try:
|
||||
db_backup_file = os.path.join(staging_dir, 'database.sql')
|
||||
|
||||
# Get database settings
|
||||
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')
|
||||
]
|
||||
|
||||
# Set environment variables for authentication
|
||||
env = os.environ.copy()
|
||||
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
|
||||
|
||||
|
||||
def create_files_backup(staging_dir):
|
||||
"""Create backup of application files"""
|
||||
try:
|
||||
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
|
||||
]
|
||||
|
||||
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
|
||||
|
||||
|
||||
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',
|
||||
}
|
||||
|
||||
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='.')
|
||||
|
||||
|
||||
def run_restore(restore_job_id, backup_file_path):
|
||||
"""
|
||||
Run a restore job
|
||||
This runs in a separate thread
|
||||
"""
|
||||
try:
|
||||
restore_job = BackupJob.objects.get(id=restore_job_id)
|
||||
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')
|
||||
os.makedirs(extract_dir)
|
||||
|
||||
# Extract tar.gz
|
||||
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')
|
||||
if not os.path.exists(metadata_file):
|
||||
raise Exception("Invalid backup: missing metadata")
|
||||
|
||||
# Read metadata
|
||||
import json
|
||||
with open(metadata_file, 'r') as f:
|
||||
metadata = json.load(f)
|
||||
|
||||
# Restore database
|
||||
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')
|
||||
if os.path.exists(files_dir):
|
||||
restore_files(files_dir)
|
||||
|
||||
# Update job status
|
||||
restore_job.status = 'completed'
|
||||
restore_job.completed_at = timezone.now()
|
||||
restore_job.save()
|
||||
|
||||
except Exception as e:
|
||||
restore_job.status = 'failed'
|
||||
restore_job.error_message = str(e)
|
||||
restore_job.completed_at = timezone.now()
|
||||
restore_job.save()
|
||||
|
||||
|
||||
def restore_database(db_backup_file):
|
||||
"""Restore database from backup"""
|
||||
try:
|
||||
# Get database settings
|
||||
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
|
||||
]
|
||||
|
||||
# Set environment variables for authentication
|
||||
env = os.environ.copy()
|
||||
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 ''
|
||||
# escalate only if we see ERROR
|
||||
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}")
|
||||
|
||||
|
||||
def restore_files(files_dir):
|
||||
"""Restore application files"""
|
||||
try:
|
||||
# Restore paths
|
||||
restore_mappings = {
|
||||
'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')}"
|
||||
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}")
|
||||
|
||||
|
||||
def cleanup_old_backups(keep_count=10):
|
||||
"""Clean up old backup files, keeping only the newest ones"""
|
||||
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'):
|
||||
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}")
|
||||
801
app/stiftung/forms.py
Normal file
801
app/stiftung/forms.py
Normal file
@@ -0,0 +1,801 @@
|
||||
from django import forms
|
||||
from django.core.exceptions import ValidationError
|
||||
from .models import (
|
||||
Rentmeister, StiftungsKonto, Verwaltungskosten, Person,
|
||||
Paechter, Destinataer, Land, Verpachtung, DokumentLink, Foerderung, BankTransaction,
|
||||
DestinataerUnterstuetzung, DestinataerNotiz, LandAbrechnung,
|
||||
)
|
||||
import re
|
||||
|
||||
|
||||
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'
|
||||
]
|
||||
|
||||
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}),
|
||||
}
|
||||
|
||||
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',
|
||||
}
|
||||
|
||||
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',
|
||||
}
|
||||
|
||||
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
|
||||
|
||||
def clean_iban(self):
|
||||
"""Validierung der IBAN"""
|
||||
iban = self.cleaned_data.get('iban')
|
||||
if iban:
|
||||
# Entferne Leerzeichen und konvertiere zu Großbuchstaben
|
||||
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.')
|
||||
|
||||
# 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.')
|
||||
return plz
|
||||
|
||||
def clean(self):
|
||||
"""Übergreifende Validierung"""
|
||||
cleaned_data = super().clean()
|
||||
seit_datum = cleaned_data.get('seit_datum')
|
||||
bis_datum = cleaned_data.get('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.')
|
||||
|
||||
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'
|
||||
]
|
||||
|
||||
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}),
|
||||
}
|
||||
|
||||
|
||||
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'
|
||||
]
|
||||
|
||||
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}),
|
||||
}
|
||||
|
||||
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)
|
||||
|
||||
# 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
|
||||
|
||||
|
||||
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'
|
||||
]
|
||||
|
||||
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'}),
|
||||
}
|
||||
|
||||
labels = {
|
||||
'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
|
||||
|
||||
|
||||
class PaechterForm(forms.ModelForm):
|
||||
"""Form für das Erstellen und Bearbeiten von Pächtern"""
|
||||
|
||||
class Meta:
|
||||
model = Paechter
|
||||
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}),
|
||||
}
|
||||
|
||||
|
||||
class DestinataerForm(forms.ModelForm):
|
||||
"""Form für das Erstellen und Bearbeiten von Destinatären"""
|
||||
|
||||
class Meta:
|
||||
model = Destinataer
|
||||
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}),
|
||||
# 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'}),
|
||||
}
|
||||
|
||||
|
||||
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',
|
||||
# Gerichtliche Zuständigkeit
|
||||
'amtsgericht',
|
||||
# Verwaltungsstruktur
|
||||
'gemeinde', 'gemarkung', 'flur', 'flurstueck', 'adresse',
|
||||
# Flächenangaben
|
||||
'groesse_qm', 'gruenland_qm', 'acker_qm', 'wald_qm', 'sonstiges_qm',
|
||||
# Legacy Verpachtung (für Kompatibilität)
|
||||
'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',
|
||||
# Umsatzsteuer
|
||||
'ust_option', 'ust_satz',
|
||||
# Umlagen
|
||||
'grundsteuer_umlage', 'versicherungen_umlage', 'verbandsbeitraege_umlage', 'jagdpacht_anteil_umlage',
|
||||
# Legacy Steuern
|
||||
'anteil_grundsteuer', 'anteil_lwk',
|
||||
# Status
|
||||
'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'}),
|
||||
# Gerichtliche Zuständigkeit
|
||||
'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'}),
|
||||
# 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'}),
|
||||
# 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'}),
|
||||
# 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'}),
|
||||
# Umsatzsteuer
|
||||
'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'}),
|
||||
# Legacy
|
||||
'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}),
|
||||
}
|
||||
|
||||
|
||||
class LandAbrechnungForm(forms.ModelForm):
|
||||
"""Form für das Erstellen und Bearbeiten von Landabrechnungen"""
|
||||
|
||||
class Meta:
|
||||
model = LandAbrechnung
|
||||
fields = [
|
||||
'land', 'abrechnungsjahr',
|
||||
# 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',
|
||||
# Umsatzsteuer
|
||||
'vorsteuer_aus_umlagen',
|
||||
# Sonstiges
|
||||
'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'}),
|
||||
# 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'}),
|
||||
# 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'}),
|
||||
# Umsatzsteuer
|
||||
'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}),
|
||||
}
|
||||
|
||||
|
||||
class VerpachtungForm(forms.ModelForm):
|
||||
"""Form für das Erstellen und Bearbeiten von Verpachtungen"""
|
||||
|
||||
class Meta:
|
||||
model = Verpachtung
|
||||
fields = '__all__'
|
||||
widgets = {
|
||||
'paechter': forms.Select(attrs={'class': 'form-select'}),
|
||||
'land': forms.Select(attrs={'class': 'form-select'}),
|
||||
'pacht_pro_qm': forms.NumberInput(attrs={'class': 'form-control', 'step': '0.01'}),
|
||||
'pachtbeginn': forms.DateInput(attrs={'class': 'form-control', 'type': 'date'}),
|
||||
'pachtende': 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 DokumentLinkForm(forms.ModelForm):
|
||||
"""Form für das Erstellen und Bearbeiten von Dokumentverknüpfungen"""
|
||||
|
||||
class Meta:
|
||||
model = DokumentLink
|
||||
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'}),
|
||||
}
|
||||
|
||||
|
||||
class FoerderungForm(forms.ModelForm):
|
||||
"""Form für das Erstellen und Bearbeiten von Förderungen"""
|
||||
|
||||
class Meta:
|
||||
model = Foerderung
|
||||
fields = '__all__'
|
||||
widgets = {
|
||||
'person': forms.Select(attrs={'class': 'form-select'}),
|
||||
'destinataer': forms.Select(attrs={'class': 'form-select'}),
|
||||
'jahr': forms.NumberInput(attrs={'class': 'form-control'}),
|
||||
'betrag': forms.NumberInput(attrs={'class': 'form-control', 'step': '0.01'}),
|
||||
'zweck': forms.TextInput(attrs={'class': 'form-control'}),
|
||||
'kategorie': forms.Select(attrs={'class': 'form-select'}),
|
||||
'status': forms.Select(attrs={'class': 'form-select'}),
|
||||
'ausgezahlt_am': forms.DateInput(attrs={'class': 'form-control', 'type': 'date'}),
|
||||
'notizen': forms.Textarea(attrs={'class': 'form-control', 'rows': 3}),
|
||||
}
|
||||
|
||||
|
||||
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'
|
||||
]
|
||||
|
||||
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'}),
|
||||
}
|
||||
|
||||
|
||||
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']
|
||||
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'}),
|
||||
}
|
||||
|
||||
|
||||
class DestinataerNotizForm(forms.ModelForm):
|
||||
class Meta:
|
||||
model = DestinataerNotiz
|
||||
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'}),
|
||||
}
|
||||
|
||||
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
|
||||
|
||||
def clean(self):
|
||||
cleaned = super().clean()
|
||||
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.')
|
||||
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"
|
||||
)
|
||||
|
||||
datei = forms.FileField(
|
||||
widget=forms.FileInput(attrs={'class': 'form-control', 'accept': '.csv,.txt'}),
|
||||
label="Bankdatei",
|
||||
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'),
|
||||
],
|
||||
initial='utf-8',
|
||||
widget=forms.Select(attrs={'class': 'form-select'}),
|
||||
label="Zeichenkodierung"
|
||||
)
|
||||
|
||||
delimiter = forms.ChoiceField(
|
||||
choices=[
|
||||
(';', 'Semikolon (;)'),
|
||||
(',', 'Komma (,)'),
|
||||
('\t', 'Tab'),
|
||||
],
|
||||
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)"
|
||||
)
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# 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'})
|
||||
)
|
||||
|
||||
email = forms.EmailField(
|
||||
label="E-Mail-Adresse",
|
||||
help_text="E-Mail-Adresse des Benutzers",
|
||||
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'})
|
||||
)
|
||||
|
||||
last_name = forms.CharField(
|
||||
label="Nachname",
|
||||
max_length=150,
|
||||
required=False,
|
||||
widget=forms.TextInput(attrs={'class': 'form-control'})
|
||||
)
|
||||
|
||||
password1 = forms.CharField(
|
||||
label="Passwort",
|
||||
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"
|
||||
)
|
||||
|
||||
is_active = forms.BooleanField(
|
||||
label="Aktiv",
|
||||
required=False,
|
||||
initial=True,
|
||||
help_text="Benutzer kann sich anmelden",
|
||||
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'})
|
||||
)
|
||||
|
||||
def clean_username(self):
|
||||
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.")
|
||||
return username
|
||||
|
||||
def clean_email(self):
|
||||
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.")
|
||||
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.")
|
||||
|
||||
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']
|
||||
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'}),
|
||||
}
|
||||
labels = {
|
||||
'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',
|
||||
}
|
||||
|
||||
|
||||
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"
|
||||
)
|
||||
|
||||
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"
|
||||
)
|
||||
|
||||
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.")
|
||||
|
||||
return cleaned_data
|
||||
|
||||
|
||||
class UserPermissionForm(forms.Form):
|
||||
"""Form für die Zuweisung von Berechtigungen"""
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
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')
|
||||
|
||||
# Create checkbox fields for each permission
|
||||
for perm in app_permissions:
|
||||
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'})
|
||||
)
|
||||
|
||||
# Set initial values if user is provided
|
||||
if user:
|
||||
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'
|
||||
},
|
||||
'documents': {
|
||||
'name': 'Dokumentenverwaltung',
|
||||
'permissions': [],
|
||||
'icon': 'fas fa-folder-open'
|
||||
},
|
||||
'financial': {
|
||||
'name': 'Finanzverwaltung',
|
||||
'permissions': [],
|
||||
'icon': 'fas fa-euro-sign'
|
||||
},
|
||||
'administration': {
|
||||
'name': 'Administration',
|
||||
'permissions': [],
|
||||
'icon': 'fas fa-cogs'
|
||||
},
|
||||
'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_'):
|
||||
# Extract permission ID from field name
|
||||
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))
|
||||
else:
|
||||
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
|
||||
1
app/stiftung/management/__init__.py
Normal file
1
app/stiftung/management/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
# Management package
|
||||
1
app/stiftung/management/commands/__init__.py
Normal file
1
app/stiftung/management/commands/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
# Management commands package
|
||||
92
app/stiftung/management/commands/migrate_verpachtungen.py
Normal file
92
app/stiftung/management/commands/migrate_verpachtungen.py
Normal file
@@ -0,0 +1,92 @@
|
||||
from django.core.management.base import BaseCommand
|
||||
from django.db import transaction
|
||||
from stiftung.models import Land, Verpachtung, Paechter
|
||||
import logging
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
class Command(BaseCommand):
|
||||
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',
|
||||
)
|
||||
|
||||
def handle(self, *args, **options):
|
||||
dry_run = options['dry_run']
|
||||
|
||||
if dry_run:
|
||||
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')
|
||||
|
||||
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'
|
||||
)
|
||||
)
|
||||
skipped_count += 1
|
||||
continue
|
||||
|
||||
# Migration durchführen
|
||||
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.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'
|
||||
)
|
||||
)
|
||||
else:
|
||||
self.stdout.write(
|
||||
self.style.SUCCESS(
|
||||
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 = []
|
||||
if paechter.strasse:
|
||||
parts.append(paechter.strasse)
|
||||
if paechter.plz and paechter.ort:
|
||||
parts.append(f"{paechter.plz} {paechter.ort}")
|
||||
elif paechter.ort:
|
||||
parts.append(paechter.ort)
|
||||
|
||||
return '\n'.join(parts) if parts else ''
|
||||
119
app/stiftung/management/commands/unify_verpachtungen.py
Normal file
119
app/stiftung/management/commands/unify_verpachtungen.py
Normal file
@@ -0,0 +1,119 @@
|
||||
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
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
class Command(BaseCommand):
|
||||
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',
|
||||
)
|
||||
parser.add_argument(
|
||||
'--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']
|
||||
|
||||
if dry_run:
|
||||
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')
|
||||
|
||||
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 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.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':
|
||||
# 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
|
||||
).first()
|
||||
|
||||
if not existing:
|
||||
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}'
|
||||
)
|
||||
abrechnungen_created += 1
|
||||
|
||||
# Zusammenfassung
|
||||
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')
|
||||
if create_abrechnungen:
|
||||
self.stdout.write(f'DRY RUN: {abrechnungen_created} Abrechnungen würden erstellt')
|
||||
else:
|
||||
self.stdout.write(f'✓ {land_updates} Länder aktualisiert')
|
||||
if create_abrechnungen:
|
||||
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')
|
||||
|
||||
def _get_paechter_anschrift(self, paechter):
|
||||
"""Erstellt eine Anschrift aus den Pächter-Daten"""
|
||||
parts = []
|
||||
if paechter.strasse:
|
||||
parts.append(paechter.strasse)
|
||||
if paechter.plz and paechter.ort:
|
||||
parts.append(f"{paechter.plz} {paechter.ort}")
|
||||
elif paechter.ort:
|
||||
parts.append(paechter.ort)
|
||||
|
||||
return '\n'.join(parts) if parts else ''
|
||||
214
app/stiftung/middleware.py
Normal file
214
app/stiftung/middleware.py
Normal file
@@ -0,0 +1,214 @@
|
||||
"""
|
||||
Audit Middleware
|
||||
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.dispatch import receiver
|
||||
from stiftung.audit import log_action, track_model_changes, get_client_ip
|
||||
|
||||
# Thread-local storage for request context
|
||||
_local = threading.local()
|
||||
|
||||
|
||||
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')
|
||||
return response
|
||||
|
||||
|
||||
def get_current_request():
|
||||
"""Get the current request from thread-local storage"""
|
||||
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
|
||||
}
|
||||
|
||||
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):
|
||||
return instance.get_full_name()
|
||||
elif hasattr(instance, '__str__'):
|
||||
return str(instance)
|
||||
else:
|
||||
return f"{instance.__class__.__name__} #{instance.pk}"
|
||||
|
||||
|
||||
# Signal handlers for automatic audit logging
|
||||
@receiver(pre_save)
|
||||
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'):
|
||||
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':
|
||||
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'):
|
||||
_local.user_changes = {}
|
||||
_local.user_changes[instance.pk] = old_instance
|
||||
except sender.DoesNotExist:
|
||||
pass
|
||||
|
||||
|
||||
@receiver(post_save)
|
||||
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'):
|
||||
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':
|
||||
return
|
||||
|
||||
# Skip certain system models
|
||||
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',
|
||||
entity_type=entity_type,
|
||||
entity_id=entity_id,
|
||||
entity_name=entity_name,
|
||||
description=description
|
||||
)
|
||||
else:
|
||||
# Log update with changes
|
||||
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',
|
||||
entity_type=entity_type,
|
||||
entity_id=entity_id,
|
||||
entity_name=entity_name,
|
||||
description=description,
|
||||
changes=changes
|
||||
)
|
||||
|
||||
|
||||
@receiver(post_delete)
|
||||
def log_model_delete(sender, instance, **kwargs):
|
||||
"""Log model deletion"""
|
||||
request = get_current_request()
|
||||
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':
|
||||
return
|
||||
|
||||
# Skip certain system models
|
||||
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"
|
||||
log_action(
|
||||
request=request,
|
||||
action='delete',
|
||||
entity_type=entity_type,
|
||||
entity_id=entity_id,
|
||||
entity_name=entity_name,
|
||||
description=description
|
||||
)
|
||||
|
||||
|
||||
# Authentication logging
|
||||
@receiver(user_logged_in)
|
||||
def log_user_login(sender, request, user, **kwargs):
|
||||
"""Log user login"""
|
||||
log_action(
|
||||
request=request,
|
||||
action='login',
|
||||
entity_type='user',
|
||||
entity_id=str(user.pk),
|
||||
entity_name=user.username,
|
||||
description=f"Benutzer {user.username} hat sich angemeldet"
|
||||
)
|
||||
|
||||
|
||||
@receiver(user_logged_out)
|
||||
def log_user_logout(sender, request, user, **kwargs):
|
||||
"""Log user logout"""
|
||||
if user: # user might be None if session expired
|
||||
log_action(
|
||||
request=request,
|
||||
action='logout',
|
||||
entity_type='user',
|
||||
entity_id=str(user.pk),
|
||||
entity_name=user.username,
|
||||
description=f"Benutzer {user.username} hat sich abgemeldet"
|
||||
)
|
||||
47
app/stiftung/migrations/0001_initial.py
Normal file
47
app/stiftung/migrations/0001_initial.py
Normal file
@@ -0,0 +1,47 @@
|
||||
# Generated by Django 5.0.6 on 2025-08-13 20:59
|
||||
|
||||
import django.db.models.deletion
|
||||
import uuid
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
initial = True
|
||||
|
||||
dependencies = [
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
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)),
|
||||
],
|
||||
),
|
||||
migrations.CreateModel(
|
||||
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)),
|
||||
],
|
||||
),
|
||||
migrations.CreateModel(
|
||||
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')),
|
||||
],
|
||||
),
|
||||
]
|
||||
@@ -0,0 +1,112 @@
|
||||
# Generated by Django 5.0.6 on 2025-08-13 21:07
|
||||
|
||||
import django.core.validators
|
||||
import django.db.models.deletion
|
||||
import django.utils.timezone
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('stiftung', '0001_initial'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterModelOptions(
|
||||
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'},
|
||||
),
|
||||
migrations.AlterModelOptions(
|
||||
name='person',
|
||||
options={'ordering': ['nachname', 'vorname'], 'verbose_name': 'Person', 'verbose_name_plural': 'Personen'},
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='dokumentlink',
|
||||
name='beschreibung',
|
||||
field=models.TextField(blank=True, null=True),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='foerderung',
|
||||
name='antragsdatum',
|
||||
field=models.DateField(default=django.utils.timezone.now),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='foerderung',
|
||||
name='bemerkungen',
|
||||
field=models.TextField(blank=True, null=True),
|
||||
),
|
||||
migrations.AddField(
|
||||
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),
|
||||
),
|
||||
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),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='person',
|
||||
name='adresse',
|
||||
field=models.TextField(blank=True, null=True),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='person',
|
||||
name='aktiv',
|
||||
field=models.BooleanField(default=True),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='person',
|
||||
name='notizen',
|
||||
field=models.TextField(blank=True, null=True),
|
||||
),
|
||||
migrations.AddField(
|
||||
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),
|
||||
),
|
||||
migrations.AlterField(
|
||||
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)]),
|
||||
),
|
||||
migrations.AlterField(
|
||||
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'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
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')},
|
||||
),
|
||||
]
|
||||
@@ -0,0 +1,78 @@
|
||||
# Generated by Django 5.0.6 on 2025-08-13 21:43
|
||||
|
||||
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'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
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)),
|
||||
],
|
||||
options={
|
||||
'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),
|
||||
),
|
||||
migrations.CreateModel(
|
||||
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')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'Verpachtung',
|
||||
'verbose_name_plural': 'Verpachtungen',
|
||||
'ordering': ['-pachtbeginn'],
|
||||
},
|
||||
),
|
||||
]
|
||||
36
app/stiftung/migrations/0004_csvimport.py
Normal file
36
app/stiftung/migrations/0004_csvimport.py
Normal file
@@ -0,0 +1,36 @@
|
||||
# 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'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
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')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'CSV Import',
|
||||
'verbose_name_plural': 'CSV Imports',
|
||||
'ordering': ['-started_at'],
|
||||
},
|
||||
),
|
||||
]
|
||||
@@ -0,0 +1,93 @@
|
||||
# Generated by Django 5.0.6 on 2025-08-14 10:38
|
||||
|
||||
import django.db.models.deletion
|
||||
import uuid
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('stiftung', '0004_csvimport'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
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')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'Destinatär',
|
||||
'verbose_name_plural': 'Destinatäre',
|
||||
'ordering': ['nachname', 'vorname'],
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
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')),
|
||||
],
|
||||
options={
|
||||
'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)'},
|
||||
),
|
||||
migrations.AlterUniqueTogether(
|
||||
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)'),
|
||||
),
|
||||
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'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='verpachtung',
|
||||
name='paechter',
|
||||
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='stiftung.paechter', verbose_name='Pächter'),
|
||||
),
|
||||
]
|
||||
@@ -0,0 +1,22 @@
|
||||
# Generated by Django 5.0.6 on 2025-08-14 12:00
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('stiftung', '0005_destinataer_paechter_alter_person_options_and_more'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RemoveField(
|
||||
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'),
|
||||
),
|
||||
]
|
||||
@@ -0,0 +1,56 @@
|
||||
# Generated by Django 5.0.6 on 2025-08-14 12:10
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('stiftung', '0006_remove_paechter_familienzweig_and_more'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RemoveField(
|
||||
model_name='destinataer',
|
||||
name='adresse',
|
||||
),
|
||||
migrations.RemoveField(
|
||||
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'),
|
||||
),
|
||||
migrations.AddField(
|
||||
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'),
|
||||
),
|
||||
migrations.AddField(
|
||||
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'),
|
||||
),
|
||||
migrations.AddField(
|
||||
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'),
|
||||
),
|
||||
]
|
||||
@@ -0,0 +1,43 @@
|
||||
# Generated by Django 5.0.6 on 2025-08-14 19:27
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('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'),
|
||||
),
|
||||
migrations.AddField(
|
||||
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'),
|
||||
),
|
||||
migrations.AddField(
|
||||
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'),
|
||||
),
|
||||
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),
|
||||
),
|
||||
]
|
||||
@@ -0,0 +1,18 @@
|
||||
# Generated by Django 5.0.6 on 2025-08-23 21:39
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('stiftung', '0008_dokumentlink_destinataer_id_and_more'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='dokumentlink',
|
||||
name='paperless_document_id',
|
||||
field=models.IntegerField(),
|
||||
),
|
||||
]
|
||||
@@ -0,0 +1,100 @@
|
||||
# Generated by Django 5.0.6 on 2025-08-24 17:48
|
||||
|
||||
import django.db.models.deletion
|
||||
import uuid
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('stiftung', '0009_alter_dokumentlink_paperless_document_id'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
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)),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'Rentmeister',
|
||||
'verbose_name_plural': 'Rentmeister',
|
||||
'ordering': ['nachname', 'vorname'],
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
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)),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'Stiftungskonto',
|
||||
'verbose_name_plural': 'Stiftungskonten',
|
||||
'ordering': ['bank_name', 'kontoname'],
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
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')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'Verwaltungskosten',
|
||||
'verbose_name_plural': 'Verwaltungskosten',
|
||||
'ordering': ['-datum', '-erstellt_am'],
|
||||
},
|
||||
),
|
||||
]
|
||||
44
app/stiftung/migrations/0011_banktransaction.py
Normal file
44
app/stiftung/migrations/0011_banktransaction.py
Normal file
@@ -0,0 +1,44 @@
|
||||
# Generated by Django 5.0.6 on 2025-08-24 19:27
|
||||
|
||||
import django.db.models.deletion
|
||||
import uuid
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('stiftung', '0010_rentmeister_stiftungskonto_verwaltungskosten'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
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')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'Banktransaktion',
|
||||
'verbose_name_plural': 'Banktransaktionen',
|
||||
'ordering': ['-datum', '-importiert_am'],
|
||||
'unique_together': {('konto', 'datum', 'betrag', 'referenz')},
|
||||
},
|
||||
),
|
||||
]
|
||||
@@ -0,0 +1,34 @@
|
||||
# Generated by Django 5.0.6 on 2025-08-24 20:03
|
||||
|
||||
import django.db.models.deletion
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('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'),
|
||||
),
|
||||
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'),
|
||||
),
|
||||
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)'),
|
||||
),
|
||||
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'),
|
||||
),
|
||||
]
|
||||
@@ -0,0 +1,18 @@
|
||||
# Generated by Django 5.0.6 on 2025-08-24 20:52
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('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'),
|
||||
),
|
||||
]
|
||||
18
app/stiftung/migrations/0014_dokumentlink_rentmeister_id.py
Normal file
18
app/stiftung/migrations/0014_dokumentlink_rentmeister_id.py
Normal file
@@ -0,0 +1,18 @@
|
||||
# Generated by Django 5.0.6 on 2025-08-26 08:15
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('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'),
|
||||
),
|
||||
]
|
||||
63
app/stiftung/migrations/0015_backupjob_auditlog.py
Normal file
63
app/stiftung/migrations/0015_backupjob_auditlog.py
Normal file
@@ -0,0 +1,63 @@
|
||||
# Generated by Django 5.0.6 on 2025-08-26 08:33
|
||||
|
||||
import django.db.models.deletion
|
||||
import uuid
|
||||
from django.conf import settings
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('stiftung', '0014_dokumentlink_rentmeister_id'),
|
||||
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
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')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'Backup-Job',
|
||||
'verbose_name_plural': 'Backup-Jobs',
|
||||
'ordering': ['-created_at'],
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
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')),
|
||||
],
|
||||
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')],
|
||||
},
|
||||
),
|
||||
]
|
||||
24
app/stiftung/migrations/0016_applicationpermission.py
Normal file
24
app/stiftung/migrations/0016_applicationpermission.py
Normal file
@@ -0,0 +1,24 @@
|
||||
# Generated by Django 5.0.6 on 2025-08-26 08:50
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('stiftung', '0015_backupjob_auditlog'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='ApplicationPermission',
|
||||
fields=[
|
||||
('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': (),
|
||||
},
|
||||
),
|
||||
]
|
||||
@@ -0,0 +1,54 @@
|
||||
# Generated by Django 5.0.6 on 2025-08-29 13:15
|
||||
|
||||
import django.db.models.deletion
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('stiftung', '0016_applicationpermission'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
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'),
|
||||
),
|
||||
migrations.AddField(
|
||||
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 (€)'),
|
||||
),
|
||||
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'),
|
||||
),
|
||||
migrations.AddField(
|
||||
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'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='destinataer',
|
||||
name='vermoegen',
|
||||
field=models.DecimalField(blank=True, decimal_places=2, max_digits=12, null=True, verbose_name='Vermögen (€)'),
|
||||
),
|
||||
]
|
||||
@@ -0,0 +1,18 @@
|
||||
# Generated by Django 5.0.6 on 2025-08-29 13:27
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('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 (€)'),
|
||||
),
|
||||
]
|
||||
35
app/stiftung/migrations/0019_destinataerunterstuetzung.py
Normal file
35
app/stiftung/migrations/0019_destinataerunterstuetzung.py
Normal file
@@ -0,0 +1,35 @@
|
||||
# Generated by Django 5.0.6 on 2025-08-29 13:40
|
||||
|
||||
import django.db.models.deletion
|
||||
import uuid
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('stiftung', '0018_destinataer_vierteljaehrlicher_betrag'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
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')),
|
||||
],
|
||||
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')],
|
||||
},
|
||||
),
|
||||
]
|
||||
34
app/stiftung/migrations/0020_destinataernotiz.py
Normal file
34
app/stiftung/migrations/0020_destinataernotiz.py
Normal file
@@ -0,0 +1,34 @@
|
||||
# Generated by Django 5.0.6 on 2025-08-29 16:05
|
||||
|
||||
import django.db.models.deletion
|
||||
import uuid
|
||||
from django.conf import settings
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('stiftung', '0019_destinataerunterstuetzung'),
|
||||
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
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')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'Destinatär-Notiz',
|
||||
'verbose_name_plural': 'Destinatär-Notizen',
|
||||
'ordering': ['-erstellt_am'],
|
||||
},
|
||||
),
|
||||
]
|
||||
@@ -0,0 +1,134 @@
|
||||
# Generated by Django 5.0.6 on 2025-08-30 14:20
|
||||
|
||||
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'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
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'),
|
||||
),
|
||||
migrations.AddField(
|
||||
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'),
|
||||
),
|
||||
migrations.AddField(
|
||||
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'),
|
||||
),
|
||||
migrations.AddField(
|
||||
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 (€)'),
|
||||
),
|
||||
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 (€)'),
|
||||
),
|
||||
migrations.AddField(
|
||||
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'),
|
||||
),
|
||||
migrations.AddField(
|
||||
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 (%)'),
|
||||
),
|
||||
migrations.AddField(
|
||||
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'),
|
||||
),
|
||||
migrations.AddField(
|
||||
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'),
|
||||
),
|
||||
migrations.CreateModel(
|
||||
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')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'Landabrechnung',
|
||||
'verbose_name_plural': 'Landabrechnungen',
|
||||
'ordering': ['-abrechnungsjahr', 'land__gemeinde', 'land__gemarkung'],
|
||||
'unique_together': {('land', 'abrechnungsjahr')},
|
||||
},
|
||||
),
|
||||
]
|
||||
@@ -0,0 +1,57 @@
|
||||
# Generated by Django 5.0.6 on 2025-08-30 16:59
|
||||
|
||||
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'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
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)'),
|
||||
),
|
||||
migrations.CreateModel(
|
||||
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')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'Landverpachtung',
|
||||
'verbose_name_plural': 'Landverpachtungen',
|
||||
'ordering': ['-pachtbeginn', 'land'],
|
||||
},
|
||||
),
|
||||
]
|
||||
0
app/stiftung/migrations/__init__.py
Normal file
0
app/stiftung/migrations/__init__.py
Normal file
1667
app/stiftung/models.py
Normal file
1667
app/stiftung/models.py
Normal file
File diff suppressed because it is too large
Load Diff
12
app/stiftung/serializers.py
Normal file
12
app/stiftung/serializers.py
Normal file
@@ -0,0 +1,12 @@
|
||||
from rest_framework import serializers
|
||||
from .models import Person, Foerderung
|
||||
|
||||
class PersonSerializer(serializers.ModelSerializer):
|
||||
class Meta:
|
||||
model = Person
|
||||
fields = "__all__"
|
||||
|
||||
class FoerderungSerializer(serializers.ModelSerializer):
|
||||
class Meta:
|
||||
model = Foerderung
|
||||
fields = "__all__"
|
||||
148
app/stiftung/urls.py
Normal file
148
app/stiftung/urls.py
Normal file
@@ -0,0 +1,148 @@
|
||||
from django.urls import path
|
||||
from . import views
|
||||
|
||||
app_name = 'stiftung'
|
||||
|
||||
urlpatterns = [
|
||||
# Dashboard (Startseite)
|
||||
path('', views.dashboard, name='dashboard'),
|
||||
|
||||
# Home (für Kompatibilität mit bestehenden Templates)
|
||||
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'),
|
||||
|
||||
# Destinatär URLs (Förderungsempfänger)
|
||||
path('destinataere/', views.destinataer_list, name='destinataer_list'),
|
||||
path('destinataere/<uuid:pk>/', views.destinataer_detail, name='destinataer_detail'),
|
||||
path('destinataere/neu/', views.destinataer_create, name='destinataer_create'),
|
||||
path('destinataere/<uuid:pk>/bearbeiten/', views.destinataer_update, name='destinataer_update'),
|
||||
path('destinataere/<uuid:pk>/loeschen/', views.destinataer_delete, name='destinataer_delete'),
|
||||
path('destinataere/<uuid:pk>/notiz/', views.destinataer_notiz_create, name='destinataer_notiz_create'),
|
||||
path('destinataere/<uuid:pk>/export/', views.destinataer_export, name='destinataer_export'),
|
||||
|
||||
# Paechter URLs (Landpächter)
|
||||
path('paechter/', views.paechter_list, name='paechter_list'),
|
||||
path('paechter/<uuid:pk>/', views.paechter_detail, name='paechter_detail'),
|
||||
path('paechter/neu/', views.paechter_create, name='paechter_create'),
|
||||
path('paechter/<uuid:pk>/bearbeiten/', views.paechter_update, name='paechter_update'),
|
||||
path('paechter/<uuid:pk>/loeschen/', views.paechter_delete, name='paechter_delete'),
|
||||
path('paechter/<uuid:pk>/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/<uuid:pk>/', views.land_detail, name='land_detail'),
|
||||
path('laendereien/neu/', views.land_create, name='land_create'),
|
||||
path('laendereien/<uuid:pk>/bearbeiten/', views.land_update, name='land_update'),
|
||||
path('laendereien/<uuid:pk>/loeschen/', views.land_delete, name='land_delete'),
|
||||
path('laendereien/<uuid:pk>/export/', views.land_export, name='land_export'),
|
||||
|
||||
# Landabrechnung URLs
|
||||
path('landabrechnungen/', views.land_abrechnung_list, name='land_abrechnung_list'),
|
||||
path('landabrechnungen/<uuid:pk>/', views.land_abrechnung_detail, name='land_abrechnung_detail'),
|
||||
path('landabrechnungen/neu/', views.land_abrechnung_create, name='land_abrechnung_create'),
|
||||
path('landabrechnungen/<uuid:pk>/bearbeiten/', views.land_abrechnung_update, name='land_abrechnung_update'),
|
||||
path('landabrechnungen/<uuid:pk>/loeschen/', views.land_abrechnung_delete, name='land_abrechnung_delete'),
|
||||
|
||||
# Vereinheitlichte Verpachtung URLs (direkt im Land)
|
||||
path('laendereien/<uuid:land_pk>/verpachtung/neu/', views.land_verpachtung_create, name='land_verpachtung_create'),
|
||||
path('laendereien/<uuid:land_pk>/verpachtung/bearbeiten/', views.land_verpachtung_edit, name='land_verpachtung_edit'),
|
||||
path('laendereien/<uuid:land_pk>/verpachtung/beenden/', views.land_verpachtung_end, name='land_verpachtung_end'),
|
||||
|
||||
# Verpachtung URLs
|
||||
path('verpachtungen/', views.verpachtung_list, name='verpachtung_list'),
|
||||
path('verpachtungen/<uuid:pk>/', views.verpachtung_detail, name='verpachtung_detail'),
|
||||
path('verpachtungen/neu/', views.verpachtung_create, name='verpachtung_create'),
|
||||
path('verpachtungen/<uuid:pk>/bearbeiten/', views.verpachtung_update, name='verpachtung_update'),
|
||||
path('verpachtungen/<uuid:pk>/loeschen/', views.verpachtung_delete, name='verpachtung_delete'),
|
||||
path('verpachtungen/<uuid:pk>/export/', views.verpachtung_export, name='verpachtung_export'),
|
||||
|
||||
# Förderung URLs
|
||||
path('foerderungen/', views.foerderung_list, name='foerderung_list'),
|
||||
path('foerderungen/<uuid:pk>/', views.foerderung_detail, name='foerderung_detail'),
|
||||
path('foerderungen/neu/', views.foerderung_create, name='foerderung_create'),
|
||||
path('foerderungen/<uuid:pk>/bearbeiten/', views.foerderung_update, name='foerderung_update'),
|
||||
path('foerderungen/<uuid:pk>/loeschen/', views.foerderung_delete, name='foerderung_delete'),
|
||||
|
||||
# Dokumente URLs
|
||||
path('dokumente/', views.dokument_list, name='dokument_list'),
|
||||
path('dokumente/<uuid:pk>/', views.dokument_detail, name='dokument_detail'),
|
||||
path('dokumente/neu/', views.dokument_create, name='dokument_create'),
|
||||
path('dokumente/<uuid:pk>/bearbeiten/', views.dokument_update, name='dokument_update'),
|
||||
path('dokumente/<uuid:pk>/loeschen/', views.dokument_delete, name='dokument_delete'),
|
||||
|
||||
# Dokumentenverwaltung (Paperless-Integration, Verwaltung & Verknüpfung)
|
||||
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/<uuid:link_id>/', 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/<int:jahr>/', views.jahresbericht_generate, name='jahresbericht_generate'),
|
||||
path('berichte/jahresbericht/<int:jahr>/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/<uuid:pk>/', views.konto_detail, name='konto_detail'),
|
||||
path('geschaeftsfuehrung/konten/<uuid:pk>/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/<uuid:pk>/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/<uuid:pk>/', views.rentmeister_detail, name='rentmeister_detail'),
|
||||
path('geschaeftsfuehrung/rentmeister/<uuid:pk>/bearbeiten/', views.rentmeister_edit, name='rentmeister_edit'),
|
||||
path('geschaeftsfuehrung/rentmeister/<uuid:pk>/ausgaben/', views.rentmeister_ausgaben, name='rentmeister_ausgaben'),
|
||||
|
||||
# Administration URLs
|
||||
path('administration/', views.administration, name='administration'),
|
||||
path('administration/audit-log/', views.audit_log_list, name='audit_log_list'),
|
||||
path('administration/backup/', views.backup_management, name='backup_management'),
|
||||
path('administration/backup/<uuid:backup_id>/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/<uuid:pk>/bearbeiten/', views.unterstuetzung_edit, name='unterstuetzung_edit'),
|
||||
path('administration/unterstuetzungen/<uuid:pk>/loeschen/', views.unterstuetzung_delete, name='unterstuetzung_delete'),
|
||||
|
||||
# Authentication URLs
|
||||
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/<int:pk>/', views.user_detail, name='user_detail'),
|
||||
path('administration/users/<int:pk>/edit/', views.user_edit, name='user_edit'),
|
||||
path('administration/users/<int:pk>/password/', views.user_change_password, name='user_change_password'),
|
||||
path('administration/users/<int:pk>/permissions/', views.user_permissions, name='user_permissions'),
|
||||
path('administration/users/<int:pk>/delete/', views.user_delete, name='user_delete'),
|
||||
|
||||
# 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/<int:doc_id>/', 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'),
|
||||
]
|
||||
4897
app/stiftung/views.py
Normal file
4897
app/stiftung/views.py
Normal file
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user