fix: configure CI database connection properly

- Add dotenv loading to Django settings
- Update CI workflow to use correct environment variables
- Set POSTGRES_* variables instead of DATABASE_URL
- Add environment variables to all Django management commands
- Fixes CI test failures due to database connection issues
This commit is contained in:
Stiftung Development
2025-09-06 18:47:23 +02:00
parent dcc91b9f49
commit 35ba089a84
64 changed files with 7040 additions and 1419 deletions

View File

@@ -58,28 +58,64 @@ jobs:
- name: Set up environment
run: |
cp env-template.txt .env
echo "DEBUG=True" >> .env
echo "SECRET_KEY=test-secret-key-for-ci" >> .env
echo "DATABASE_URL=postgresql://postgres:postgres@localhost:5432/test_stiftung" >> .env
echo "DJANGO_DEBUG=1" >> .env
echo "DJANGO_SECRET_KEY=test-secret-key-for-ci" >> .env
echo "POSTGRES_DB=test_stiftung" >> .env
echo "POSTGRES_USER=postgres" >> .env
echo "POSTGRES_PASSWORD=postgres" >> .env
echo "DB_HOST=localhost" >> .env
echo "DB_PORT=5432" >> .env
echo "REDIS_URL=redis://localhost:6379/0" >> .env
- name: Run migrations
working-directory: ./app
env:
DJANGO_DEBUG: "1"
DJANGO_SECRET_KEY: "test-secret-key-for-ci"
POSTGRES_DB: "test_stiftung"
POSTGRES_USER: "postgres"
POSTGRES_PASSWORD: "postgres"
DB_HOST: "localhost"
DB_PORT: "5432"
run: |
python manage.py migrate
- name: Run tests
working-directory: ./app
env:
DJANGO_DEBUG: "1"
DJANGO_SECRET_KEY: "test-secret-key-for-ci"
POSTGRES_DB: "test_stiftung"
POSTGRES_USER: "postgres"
POSTGRES_PASSWORD: "postgres"
DB_HOST: "localhost"
DB_PORT: "5432"
run: |
python manage.py test
- name: Check Django configuration
working-directory: ./app
env:
DJANGO_DEBUG: "1"
DJANGO_SECRET_KEY: "test-secret-key-for-ci"
POSTGRES_DB: "test_stiftung"
POSTGRES_USER: "postgres"
POSTGRES_PASSWORD: "postgres"
DB_HOST: "localhost"
DB_PORT: "5432"
run: |
python manage.py check --deploy
- name: Collect static files
working-directory: ./app
env:
DJANGO_DEBUG: "1"
DJANGO_SECRET_KEY: "test-secret-key-for-ci"
POSTGRES_DB: "test_stiftung"
POSTGRES_USER: "postgres"
POSTGRES_PASSWORD: "postgres"
DB_HOST: "localhost"
DB_PORT: "5432"
run: |
python manage.py collectstatic --noinput

View File

@@ -4,7 +4,8 @@ ENV PYTHONDONTWRITEBYTECODE=1 \
RUN apt-get update && apt-get install -y --no-install-recommends \
build-essential libpq-dev postgresql-client \
libpango-1.0-0 libcairo2 libffi-dev \
libpango-1.0-0 libpangoft2-1.0-0 libcairo2 libffi-dev \
libgdk-pixbuf-xlib-2.0-0 libharfbuzz0b libfribidi0 \
libjpeg-dev libpng-dev \
&& rm -rf /var/lib/apt/lists/*

View File

@@ -0,0 +1,32 @@
#!/usr/bin/env python
import os
import django
# Setup Django environment
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'core.settings')
django.setup()
from stiftung.models import UnterstuetzungWiederkehrend, DestinataerUnterstuetzung
# Find orphaned templates
orphaned = []
for template in UnterstuetzungWiederkehrend.objects.all():
count = DestinataerUnterstuetzung.objects.filter(wiederkehrend_von=template).count()
if count == 0:
orphaned.append((template, count))
print(f'Verwaiste Vorlagen gefunden: {len(orphaned)}')
if orphaned:
print('Details:')
for template, count in orphaned[:10]: # Show first 10
print(f'- ID {template.id}: {template.destinataer} - {template.beschreibung} ({template.betrag}€)')
else:
print('Keine verwaisten Vorlagen!')
# Also show all templates with their payment counts
print('\n--- Alle wiederkehrende Vorlagen ---')
all_templates = UnterstuetzungWiederkehrend.objects.all()
for template in all_templates[:10]: # Show first 10
count = DestinataerUnterstuetzung.objects.filter(wiederkehrend_von=template).count()
status = "VERWAIST" if count == 0 else f"{count} Zahlungen"
print(f'ID {template.id}: {template.destinataer} - {template.beschreibung} ({template.betrag}€) - {status}')

View File

@@ -1,5 +1,9 @@
import os
from pathlib import Path
from dotenv import load_dotenv
# Load environment variables from .env file
load_dotenv()
BASE_DIR = Path(__file__).resolve().parent.parent

View File

@@ -7,3 +7,5 @@ weasyprint==62.3
python-dotenv==1.0.1
requests==2.32.3
gunicorn==22.0.0
python-dateutil==2.9.0
markdown==3.6

View File

@@ -3,9 +3,11 @@ 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 . import models
from .models import (
Person, Paechter, Destinataer, DokumentLink, Foerderung, Land, Verpachtung, CSVImport,
Rentmeister, StiftungsKonto, Verwaltungskosten, BankTransaction, AuditLog, BackupJob
Person, Paechter, Destinataer, DokumentLink, Foerderung, Land, CSVImport,
Rentmeister, StiftungsKonto, Verwaltungskosten, BankTransaction, AuditLog, BackupJob, AppConfiguration,
DestinataerUnterstuetzung, UnterstuetzungWiederkehrend
)
@admin.register(CSVImport)
@@ -214,66 +216,6 @@ class LandAdmin(admin.ModelAdmin):
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']
@@ -541,6 +483,134 @@ class BackupJobAdmin(admin.ModelAdmin):
return False # Use the web interface for creating backups
@admin.register(AppConfiguration)
class AppConfigurationAdmin(admin.ModelAdmin):
list_display = ['display_name', 'key', 'value_display', 'category', 'setting_type', 'is_active', 'updated_at']
list_filter = ['category', 'setting_type', 'is_active']
search_fields = ['key', 'display_name', 'description']
readonly_fields = ['id', 'created_at', 'updated_at']
ordering = ['category', 'order', 'display_name']
fieldsets = (
('Basic Information', {
'fields': ('key', 'display_name', 'description', 'category', 'setting_type')
}),
('Value Configuration', {
'fields': ('value', 'default_value')
}),
('Options', {
'fields': ('is_active', 'is_system', 'order')
}),
('Metadata', {
'fields': ('created_at', 'updated_at'),
'classes': ('collapse',)
}),
)
def value_display(self, obj):
"""Display value with type formatting"""
value = obj.value
if obj.setting_type == 'boolean':
icon = '' if obj.get_typed_value() else ''
return format_html('{} {}', icon, value)
elif obj.setting_type == 'url':
return format_html('<a href="{}" target="_blank">{}</a>', value, value[:50] + '...' if len(value) > 50 else value)
elif len(value) > 100:
return value[:100] + '...'
return value
value_display.short_description = 'Current Value'
def get_readonly_fields(self, request, obj=None):
readonly = list(self.readonly_fields)
if obj and obj.is_system:
readonly.extend(['key', 'setting_type', 'is_system'])
return readonly
@admin.register(DestinataerUnterstuetzung)
class DestinataerUnterstuetzungAdmin(admin.ModelAdmin):
list_display = ['__str__', 'destinataer', 'betrag', 'faellig_am', 'status', 'wiederkehrend_von', 'ausgezahlt_am']
list_filter = ['status', 'faellig_am', 'erstellt_am', 'konto']
search_fields = ['destinataer__vorname', 'destinataer__nachname', 'beschreibung', 'empfaenger_name']
readonly_fields = ['id', 'erstellt_am', 'aktualisiert_am']
fieldsets = (
('Grundinformationen', {
'fields': ('destinataer', 'konto', 'betrag', 'faellig_am', 'status', 'beschreibung')
}),
('Überweisungsdaten', {
'fields': ('empfaenger_iban', 'empfaenger_name', 'verwendungszweck')
}),
('Zahlungsinformationen', {
'fields': ('ausgezahlt_am', 'ausgezahlt_von')
}),
('Wiederkehrend', {
'fields': ('wiederkehrend_von',)
}),
('Metadaten', {
'fields': ('id', 'erstellt_am', 'aktualisiert_am'),
'classes': ('collapse',)
}),
)
@admin.register(UnterstuetzungWiederkehrend)
class UnterstuetzungWiederkehrendAdmin(admin.ModelAdmin):
list_display = ['__str__', 'destinataer', 'betrag', 'intervall', 'aktiv', 'naechste_generierung']
list_filter = ['intervall', 'aktiv', 'erstellt_am']
search_fields = ['destinataer__vorname', 'destinataer__nachname', 'beschreibung', 'empfaenger_name']
readonly_fields = ['id', 'erstellt_am']
fieldsets = (
('Grundinformationen', {
'fields': ('destinataer', 'konto', 'betrag', 'intervall', 'beschreibung', 'aktiv')
}),
('Überweisungsdaten', {
'fields': ('empfaenger_iban', 'empfaenger_name', 'verwendungszweck')
}),
('Zeitplanung', {
'fields': ('erste_zahlung_am', 'letzte_zahlung_am', 'naechste_generierung')
}),
('Metadaten', {
'fields': ('id', 'erstellt_von', 'erstellt_am'),
'classes': ('collapse',)
}),
)
@admin.register(models.HelpBox)
class HelpBoxAdmin(admin.ModelAdmin):
list_display = ['get_page_display', 'title', 'is_active', 'updated_at', 'updated_by']
list_filter = ['page_key', 'is_active', 'updated_at']
search_fields = ['title', 'content']
fieldsets = (
('Grundinformationen', {
'fields': ('page_key', 'title', 'is_active')
}),
('Inhalt', {
'fields': ('content',),
'description': 'Markdown wird unterstützt: **fett**, *kursiv*, `code`, [Link](url), Listen mit - oder 1.'
}),
('Metadaten', {
'fields': ('created_at', 'updated_at', 'created_by', 'updated_by'),
'classes': ('collapse',)
}),
)
readonly_fields = ['created_at', 'updated_at']
def get_page_display(self, obj):
return obj.get_page_key_display()
get_page_display.short_description = "Seite"
def save_model(self, request, obj, form, change):
if not change: # Neues Objekt
obj.created_by = request.user.username
obj.updated_by = request.user.username
super().save_model(request, obj, form, change)
# Customize admin site
admin.site.site_header = "Stiftungsverwaltung Administration"
admin.site.site_title = "Stiftungsverwaltung Admin"

View File

@@ -1,9 +1,10 @@
from django import forms
from django.core.exceptions import ValidationError
from django.utils import timezone
from .models import (
Rentmeister, StiftungsKonto, Verwaltungskosten, Person,
Paechter, Destinataer, Land, Verpachtung, DokumentLink, Foerderung, BankTransaction,
DestinataerUnterstuetzung, DestinataerNotiz, LandAbrechnung,
Paechter, Destinataer, Land, DokumentLink, Foerderung, BankTransaction,
DestinataerUnterstuetzung, UnterstuetzungWiederkehrend, DestinataerNotiz, LandAbrechnung,
)
import re
@@ -105,10 +106,24 @@ class RentmeisterForm(forms.ModelForm):
def clean(self):
"""Übergreifende Validierung"""
from django.utils.dateparse import parse_date
cleaned_data = super().clean()
seit_datum = cleaned_data.get('seit_datum')
bis_datum = cleaned_data.get('bis_datum')
# Helper function to ensure we have date objects
def ensure_date(date_value):
if not date_value:
return None
if isinstance(date_value, str):
return parse_date(date_value)
return date_value
# Convert to date objects if they're strings
seit_datum = ensure_date(seit_datum)
bis_datum = ensure_date(bis_datum)
# Prüfe Datum-Logik
if seit_datum and bis_datum and bis_datum <= seit_datum:
raise ValidationError('Das End-Datum muss nach dem Start-Datum liegen.')
@@ -404,23 +419,6 @@ class LandAbrechnungForm(forms.ModelForm):
}
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"""
@@ -438,21 +436,165 @@ class DokumentLinkForm(forms.ModelForm):
class FoerderungForm(forms.ModelForm):
"""Form für das Erstellen und Bearbeiten von Förderungen"""
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
# Add empty option for optional fields
self.fields['verwendungsnachweis'].empty_label = "--- Kein Dokument verknüpfen ---"
# Ensure destinataer has proper choices
from .models import Destinataer, DokumentLink
from django.utils import timezone
self.fields['destinataer'].queryset = Destinataer.objects.all().order_by('nachname', 'vorname')
self.fields['verwendungsnachweis'].queryset = DokumentLink.objects.all().order_by('titel')
# Set current year as default for new forms
if not self.instance.pk:
self.fields['jahr'].initial = timezone.now().year
class Meta:
model = Foerderung
fields = '__all__'
fields = [
'destinataer', 'jahr', 'betrag', 'kategorie', 'status',
'antragsdatum', 'entscheidungsdatum', 'verwendungsnachweis', 'bemerkungen'
]
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}),
'antragsdatum': forms.DateInput(attrs={'class': 'form-control', 'type': 'date'}),
'entscheidungsdatum': forms.DateInput(attrs={'class': 'form-control', 'type': 'date'}),
'verwendungsnachweis': forms.Select(attrs={'class': 'form-select'}),
'bemerkungen': forms.Textarea(attrs={'class': 'form-control', 'rows': 3}),
}
labels = {
'destinataer': 'Destinatär',
'verwendungsnachweis': 'Verknüpftes Dokument',
'bemerkungen': 'Bemerkungen/Beschreibung',
'antragsdatum': 'Antragsdatum',
'entscheidungsdatum': 'Entscheidungsdatum',
}
help_texts = {
'verwendungsnachweis': 'Optionale Verknüpfung zu einem Dokument aus dem Paperless-System',
'entscheidungsdatum': 'Datum der Bewilligung/Ablehnung (optional)',
'bemerkungen': 'Zusätzliche Informationen zur Förderung',
}
class UnterstuetzungForm(forms.ModelForm):
"""Form für das Erstellen und Bearbeiten von Unterstützungen"""
# Special field for creating recurring payments
ist_wiederkehrend = forms.BooleanField(
required=False,
label='Wiederkehrende Zahlung',
help_text='Aktivieren Sie diese Option um automatisch wiederkehrende Zahlungen zu erstellen'
)
intervall = forms.ChoiceField(
choices=[('', '--- Wählen Sie ein Intervall ---')] + UnterstuetzungWiederkehrend.INTERVALL_CHOICES,
required=False,
widget=forms.Select(attrs={'class': 'form-select'}),
label='Zahlungsintervall'
)
letzte_zahlung_am = forms.DateField(
required=False,
widget=forms.DateInput(attrs={'class': 'form-control', 'type': 'date'}),
label='Letzte Zahlung am (optional)',
help_text='Leer lassen für unbegrenzte Wiederholung'
)
class Meta:
model = DestinataerUnterstuetzung
fields = [
'destinataer', 'konto', 'faellig_am', 'betrag', 'status',
'beschreibung', 'empfaenger_iban', 'empfaenger_name', 'verwendungszweck'
]
widgets = {
'destinataer': forms.Select(attrs={'class': 'form-select'}),
'konto': forms.Select(attrs={'class': 'form-select'}),
'faellig_am': forms.DateInput(attrs={'class': 'form-control', 'type': 'date'}),
'betrag': forms.NumberInput(attrs={'class': 'form-control', 'step': '0.01'}),
'status': forms.Select(attrs={'class': 'form-select'}),
'beschreibung': forms.TextInput(attrs={'class': 'form-control'}),
'empfaenger_iban': forms.TextInput(attrs={'class': 'form-control', 'placeholder': 'DE89 3704 0044 0532 0130 00'}),
'empfaenger_name': forms.TextInput(attrs={'class': 'form-control'}),
'verwendungszweck': forms.TextInput(attrs={'class': 'form-control', 'maxlength': '140'}),
}
labels = {
'destinataer': 'Destinatär',
'konto': 'Zahlungskonto',
'faellig_am': 'Fällig am',
'betrag': 'Betrag (€)',
'status': 'Status',
'beschreibung': 'Beschreibung',
'empfaenger_iban': 'Empfänger IBAN',
'empfaenger_name': 'Empfänger Name',
'verwendungszweck': 'Verwendungszweck',
}
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
# Add onchange event to destinataer field for AJAX IBAN fetching
self.fields['destinataer'].widget.attrs['onchange'] = 'updateDestinataerInfo()'
def clean(self):
cleaned_data = super().clean()
ist_wiederkehrend = cleaned_data.get('ist_wiederkehrend')
intervall = cleaned_data.get('intervall')
if ist_wiederkehrend and not intervall:
raise forms.ValidationError('Bitte wählen Sie ein Zahlungsintervall für wiederkehrende Zahlungen.')
return cleaned_data
class UnterstuetzungWiederkehrendForm(forms.ModelForm):
"""Form für das Bearbeiten von wiederkehrenden Unterstützungsvorlagen"""
class Meta:
model = UnterstuetzungWiederkehrend
fields = [
'destinataer', 'konto', 'betrag', 'intervall', 'beschreibung',
'empfaenger_iban', 'empfaenger_name', 'verwendungszweck',
'erste_zahlung_am', 'letzte_zahlung_am', 'aktiv'
]
widgets = {
'destinataer': forms.Select(attrs={'class': 'form-select'}),
'konto': forms.Select(attrs={'class': 'form-select'}),
'betrag': forms.NumberInput(attrs={'class': 'form-control', 'step': '0.01'}),
'intervall': forms.Select(attrs={'class': 'form-select'}),
'beschreibung': forms.TextInput(attrs={'class': 'form-control'}),
'empfaenger_iban': forms.TextInput(attrs={'class': 'form-control', 'placeholder': 'DE89 3704 0044 0532 0130 00'}),
'empfaenger_name': forms.TextInput(attrs={'class': 'form-control'}),
'verwendungszweck': forms.TextInput(attrs={'class': 'form-control', 'maxlength': '140'}),
'erste_zahlung_am': forms.DateInput(attrs={'class': 'form-control', 'type': 'date'}),
'letzte_zahlung_am': forms.DateInput(attrs={'class': 'form-control', 'type': 'date'}),
'aktiv': forms.CheckboxInput(attrs={'class': 'form-check-input'}),
}
class UnterstuetzungMarkAsPaidForm(forms.Form):
"""Simple form to mark an Unterstützung as paid"""
ausgezahlt_am = forms.DateField(
widget=forms.DateInput(attrs={'class': 'form-control', 'type': 'date'}),
label='Ausgezahlt am',
initial=timezone.now().date()
)
bemerkung = forms.CharField(
widget=forms.Textarea(attrs={'class': 'form-control', 'rows': 3}),
label='Bemerkung (optional)',
required=False,
help_text='Optionale Notiz zur Zahlung'
)
class BankTransactionForm(forms.ModelForm):
"""Form für das Bearbeiten von Banktransaktionen"""

View File

@@ -0,0 +1,133 @@
"""
Management command to generate due recurring support payments.
This command should be run daily via cron or similar scheduling system.
"""
from django.core.management.base import BaseCommand
from django.utils import timezone
from datetime import timedelta
from stiftung.models import UnterstuetzungWiederkehrend
import logging
logger = logging.getLogger(__name__)
class Command(BaseCommand):
help = 'Generate due recurring support payments'
def add_arguments(self, parser):
parser.add_argument(
'--dry-run',
action='store_true',
help='Show what would be generated without actually creating payments',
)
parser.add_argument(
'--days-ahead',
type=int,
default=0,
help='Generate payments that are due within this many days (default: 0 = only today)',
)
def handle(self, *args, **options):
dry_run = options['dry_run']
days_ahead = options['days_ahead']
heute = timezone.now().date()
cutoff_date = heute + timedelta(days=days_ahead)
self.stdout.write(
self.style.SUCCESS(
f'Checking for recurring payments due up to {cutoff_date.strftime("%d.%m.%Y")}...'
)
)
if dry_run:
self.stdout.write(self.style.WARNING('DRY RUN MODE - No payments will be created'))
# Get all active recurring payment templates that are due
templates = UnterstuetzungWiederkehrend.objects.filter(
aktiv=True,
naechste_generierung__lte=cutoff_date
).select_related('destinataer', 'konto')
generated_count = 0
error_count = 0
for template in templates:
try:
if dry_run:
self.stdout.write(
f'Would generate: {template.destinataer.get_full_name()} - '
f'{template.betrag} due {template.naechste_generierung.strftime("%d.%m.%Y")}'
)
generated_count += 1
else:
# Actually generate the payment
neue_zahlung = template.generiere_naechste_zahlung()
if neue_zahlung:
self.stdout.write(
self.style.SUCCESS(
f'Generated: {neue_zahlung.destinataer.get_full_name()} - '
f'{neue_zahlung.betrag} due {neue_zahlung.faellig_am.strftime("%d.%m.%Y")}'
)
)
generated_count += 1
logger.info(f'Generated recurring payment: {neue_zahlung.pk}')
else:
self.stdout.write(
self.style.WARNING(
f'No payment generated for {template.destinataer.get_full_name()} '
f'(may have reached end date or not yet due)'
)
)
except Exception as e:
error_count += 1
self.stdout.write(
self.style.ERROR(
f'Error generating payment for {template.destinataer.get_full_name()}: {str(e)}'
)
)
logger.error(f'Error generating recurring payment for template {template.pk}: {str(e)}')
# Summary
self.stdout.write('\n' + '='*50)
if dry_run:
self.stdout.write(
self.style.SUCCESS(
f'DRY RUN COMPLETE: {generated_count} payments would be generated'
)
)
else:
self.stdout.write(
self.style.SUCCESS(
f'GENERATION COMPLETE: {generated_count} payments generated'
)
)
if error_count > 0:
self.stdout.write(
self.style.ERROR(f'{error_count} errors encountered')
)
# Also check for overdue payments and report them
from stiftung.models import DestinataerUnterstuetzung
overdue_payments = DestinataerUnterstuetzung.objects.filter(
faellig_am__lt=heute,
status__in=['geplant', 'faellig']
).select_related('destinataer')
if overdue_payments.exists():
self.stdout.write('\n' + '='*50)
self.stdout.write(
self.style.WARNING(
f'WARNING: {overdue_payments.count()} overdue payments found:'
)
)
for payment in overdue_payments[:10]: # Limit to first 10
days_overdue = (heute - payment.faellig_am).days
self.stdout.write(
f' - {payment.destinataer.get_full_name()}: €{payment.betrag} '
f'({days_overdue} days overdue)'
)
if overdue_payments.count() > 10:
self.stdout.write(f' ... and {overdue_payments.count() - 10} more')

View File

@@ -0,0 +1,124 @@
from django.core.management.base import BaseCommand
from stiftung.models import AppConfiguration
class Command(BaseCommand):
help = 'Initialize default app configuration settings'
def handle(self, *args, **options):
# Paperless Integration Settings
paperless_settings = [
{
'key': 'paperless_api_url',
'display_name': 'Paperless API URL',
'description': 'The base URL for your Paperless-NGX API (e.g., http://paperless.example.com:8000)',
'value': 'http://192.168.178.167:30070',
'default_value': 'http://192.168.178.167:30070',
'setting_type': 'url',
'category': 'paperless',
'order': 1
},
{
'key': 'paperless_api_token',
'display_name': 'Paperless API Token',
'description': 'The authentication token for Paperless API access',
'value': '',
'default_value': '',
'setting_type': 'text',
'category': 'paperless',
'order': 2
},
{
'key': 'paperless_destinataere_tag',
'display_name': 'Destinatäre Tag Name',
'description': 'The tag name used to identify Destinatäre documents in Paperless',
'value': 'Stiftung_Destinatäre',
'default_value': 'Stiftung_Destinatäre',
'setting_type': 'tag',
'category': 'paperless',
'order': 3
},
{
'key': 'paperless_destinataere_tag_id',
'display_name': 'Destinatäre Tag ID',
'description': 'The numeric ID of the Destinatäre tag in Paperless',
'value': '210',
'default_value': '210',
'setting_type': 'tag_id',
'category': 'paperless',
'order': 4
},
{
'key': 'paperless_land_tag',
'display_name': 'Land & Pächter Tag Name',
'description': 'The tag name used to identify Land and Pächter documents in Paperless',
'value': 'Stiftung_Land_und_Pächter',
'default_value': 'Stiftung_Land_und_Pächter',
'setting_type': 'tag',
'category': 'paperless',
'order': 5
},
{
'key': 'paperless_land_tag_id',
'display_name': 'Land & Pächter Tag ID',
'description': 'The numeric ID of the Land & Pächter tag in Paperless',
'value': '204',
'default_value': '204',
'setting_type': 'tag_id',
'category': 'paperless',
'order': 6
},
{
'key': 'paperless_admin_tag',
'display_name': 'Administration Tag Name',
'description': 'The tag name used to identify Administration documents in Paperless',
'value': 'Stiftung_Administration',
'default_value': 'Stiftung_Administration',
'setting_type': 'tag',
'category': 'paperless',
'order': 7
},
{
'key': 'paperless_admin_tag_id',
'display_name': 'Administration Tag ID',
'description': 'The numeric ID of the Administration tag in Paperless',
'value': '216',
'default_value': '216',
'setting_type': 'tag_id',
'category': 'paperless',
'order': 8
}
]
created_count = 0
updated_count = 0
for setting_data in paperless_settings:
setting, created = AppConfiguration.objects.get_or_create(
key=setting_data['key'],
defaults=setting_data
)
if created:
created_count += 1
self.stdout.write(
self.style.SUCCESS(f'Created setting: {setting.display_name}')
)
else:
# Update existing setting with new defaults if needed
if not setting.description:
setting.description = setting_data['description']
setting.save()
updated_count += 1
self.stdout.write(
self.style.SUCCESS(
f'Configuration initialized successfully! '
f'Created {created_count} new settings, updated {updated_count} existing settings.'
)
)
self.stdout.write(
self.style.WARNING(
'You can now manage these settings in the Django Admin under "App Configurations"'
)
)

View File

@@ -0,0 +1,149 @@
"""
Management command to initialize corporate identity settings
"""
from django.core.management.base import BaseCommand
from stiftung.models import AppConfiguration
class Command(BaseCommand):
help = 'Initialize corporate identity settings for PDF generation'
def handle(self, *args, **options):
corporate_settings = [
{
'key': 'corporate_stiftung_name',
'display_name': 'Name der Stiftung',
'description': 'Der offizielle Name der Stiftung für PDF-Dokumente',
'value': 'Stiftung',
'default_value': 'Stiftung',
'setting_type': 'text',
'category': 'corporate',
'order': 1
},
{
'key': 'corporate_logo_path',
'display_name': 'Logo-Pfad',
'description': 'Pfad zur Logo-Datei (relativ zu MEDIA_ROOT oder STATIC_ROOT)',
'value': '',
'default_value': '',
'setting_type': 'text',
'category': 'corporate',
'order': 2
},
{
'key': 'corporate_primary_color',
'display_name': 'Primärfarbe',
'description': 'Hauptfarbe für Überschriften und Akzente (Hex-Code)',
'value': '#2c3e50',
'default_value': '#2c3e50',
'setting_type': 'text',
'category': 'corporate',
'order': 3
},
{
'key': 'corporate_secondary_color',
'display_name': 'Sekundärfarbe',
'description': 'Zweitfarbe für Akzente und Details (Hex-Code)',
'value': '#3498db',
'default_value': '#3498db',
'setting_type': 'text',
'category': 'corporate',
'order': 4
},
{
'key': 'corporate_address_line1',
'display_name': 'Adresse Zeile 1',
'description': 'Erste Zeile der Stiftungsadresse',
'value': '',
'default_value': '',
'setting_type': 'text',
'category': 'corporate',
'order': 5
},
{
'key': 'corporate_address_line2',
'display_name': 'Adresse Zeile 2',
'description': 'Zweite Zeile der Stiftungsadresse (PLZ, Ort)',
'value': '',
'default_value': '',
'setting_type': 'text',
'category': 'corporate',
'order': 6
},
{
'key': 'corporate_phone',
'display_name': 'Telefonnummer',
'description': 'Telefonnummer der Stiftung',
'value': '',
'default_value': '',
'setting_type': 'text',
'category': 'corporate',
'order': 7
},
{
'key': 'corporate_email',
'display_name': 'E-Mail-Adresse',
'description': 'Offizielle E-Mail-Adresse der Stiftung',
'value': '',
'default_value': '',
'setting_type': 'text',
'category': 'corporate',
'order': 8
},
{
'key': 'corporate_website',
'display_name': 'Website',
'description': 'Website der Stiftung',
'value': '',
'default_value': '',
'setting_type': 'url',
'category': 'corporate',
'order': 9
},
{
'key': 'corporate_footer_text',
'display_name': 'Fußzeilen-Text',
'description': 'Text für die Fußzeile in PDF-Dokumenten',
'value': 'Dieser Bericht wurde automatisch generiert.',
'default_value': 'Dieser Bericht wurde automatisch generiert.',
'setting_type': 'text',
'category': 'corporate',
'order': 10
},
]
created_count = 0
updated_count = 0
for setting_data in corporate_settings:
setting, created = AppConfiguration.objects.get_or_create(
key=setting_data['key'],
defaults=setting_data
)
if created:
created_count += 1
self.stdout.write(
self.style.SUCCESS(f'Created setting: {setting.display_name}')
)
else:
# Update existing setting with new defaults if needed
if not setting.description:
setting.description = setting_data['description']
setting.save()
updated_count += 1
self.stdout.write(
self.style.SUCCESS(
f'Corporate identity settings initialized! '
f'Created {created_count} new settings, updated {updated_count} existing settings.'
)
)
if created_count > 0:
self.stdout.write(
self.style.WARNING(
'Please configure your corporate identity settings in '
'Administration -> Application Settings before generating PDFs.'
)
)

View File

@@ -0,0 +1,253 @@
#!/usr/bin/env python3
"""
Management command to synchronize existing Verpachtungen with LandAbrechnungen.
This command will:
1. Find all existing Verpachtungen (both legacy and new LandVerpachtung)
2. Calculate the financial impact for each year they're active
3. Update or create corresponding LandAbrechnung records
4. Provide a summary of changes made
Usage:
python manage.py sync_abrechnungen [--dry-run] [--year YEAR]
"""
from django.core.management.base import BaseCommand, CommandError
from django.db import transaction
from decimal import Decimal
from datetime import date
from stiftung.models import Verpachtung, LandVerpachtung, LandAbrechnung
class Command(BaseCommand):
help = 'Synchronize existing Verpachtungen with LandAbrechnungen'
def add_arguments(self, parser):
parser.add_argument(
'--dry-run',
action='store_true',
help='Show what would be done without making changes',
)
parser.add_argument(
'--year',
type=int,
help='Only sync data for specific year',
)
parser.add_argument(
'--force',
action='store_true',
help='Force update even if Abrechnungen already exist',
)
def handle(self, *args, **options):
dry_run = options['dry_run']
target_year = options['year']
force = options['force']
self.stdout.write(
self.style.SUCCESS('🔄 Starting Abrechnung synchronization...')
)
if dry_run:
self.stdout.write(self.style.WARNING('📋 DRY RUN MODE - No changes will be made'))
# Statistics
stats = {
'legacy_contracts': 0,
'new_contracts': 0,
'abrechnungen_created': 0,
'abrechnungen_updated': 0,
'total_rent_amount': Decimal('0.00'),
'years_processed': set(),
}
try:
with transaction.atomic():
# Process Legacy Verpachtungen
self.stdout.write('\n📄 Processing Legacy Verpachtungen...')
legacy_verpachtungen = Verpachtung.objects.all()
for verpachtung in legacy_verpachtungen:
stats['legacy_contracts'] += 1
years_affected = self._get_affected_years(
verpachtung.pachtbeginn,
verpachtung.verlaengerung or verpachtung.pachtende,
target_year
)
for year in years_affected:
stats['years_processed'].add(year)
rent_amount = self._calculate_legacy_rent_for_year(verpachtung, year)
if not dry_run:
created, updated = self._update_abrechnung(
verpachtung.land,
year,
rent_amount,
Decimal('0.00'), # No umlage for legacy
f"Legacy-Verpachtung {verpachtung.vertragsnummer}",
force
)
if created:
stats['abrechnungen_created'] += 1
if updated:
stats['abrechnungen_updated'] += 1
stats['total_rent_amount'] += rent_amount
self.stdout.write(
f" 📊 {verpachtung.vertragsnummer} ({year}): {rent_amount:.2f}"
)
# Process New LandVerpachtungen
self.stdout.write('\n🆕 Processing New LandVerpachtungen...')
land_verpachtungen = LandVerpachtung.objects.all()
for verpachtung in land_verpachtungen:
stats['new_contracts'] += 1
years_affected = self._get_affected_years(
verpachtung.pachtbeginn,
verpachtung.pachtende,
target_year
)
for year in years_affected:
stats['years_processed'].add(year)
rent_amount = self._calculate_new_rent_for_year(verpachtung, year)
umlage_amount = Decimal('0.00') # To be calculated later
if not dry_run:
created, updated = self._update_abrechnung(
verpachtung.land,
year,
rent_amount,
umlage_amount,
f"LandVerpachtung {verpachtung.vertragsnummer}",
force
)
if created:
stats['abrechnungen_created'] += 1
if updated:
stats['abrechnungen_updated'] += 1
stats['total_rent_amount'] += rent_amount
self.stdout.write(
f" 📊 {verpachtung.vertragsnummer} ({year}): {rent_amount:.2f}"
)
if dry_run:
# Rollback transaction in dry run
transaction.set_rollback(True)
except Exception as e:
self.stdout.write(
self.style.ERROR(f'❌ Error during synchronization: {str(e)}')
)
raise CommandError(f'Synchronization failed: {str(e)}')
# Print summary
self.stdout.write('\n' + '='*50)
self.stdout.write(self.style.SUCCESS('📈 SYNCHRONIZATION SUMMARY'))
self.stdout.write('='*50)
self.stdout.write(f"Legacy contracts processed: {stats['legacy_contracts']}")
self.stdout.write(f"New contracts processed: {stats['new_contracts']}")
self.stdout.write(f"Years affected: {', '.join(map(str, sorted(stats['years_processed'])))}")
self.stdout.write(f"Abrechnungen created: {stats['abrechnungen_created']}")
self.stdout.write(f"Abrechnungen updated: {stats['abrechnungen_updated']}")
self.stdout.write(f"Total rent amount: {stats['total_rent_amount']:.2f}")
if dry_run:
self.stdout.write(self.style.WARNING('\n📋 This was a DRY RUN - no changes were saved'))
else:
self.stdout.write(self.style.SUCCESS('\n✅ Synchronization completed successfully!'))
def _get_affected_years(self, start_date, end_date, target_year=None):
"""Get all years affected by a contract"""
if not start_date:
return []
years = []
start_year = start_date.year
end_year = end_date.year if end_date else date.today().year
if target_year:
if start_year <= target_year <= end_year:
return [target_year]
else:
return []
for year in range(start_year, end_year + 1):
years.append(year)
return years
def _calculate_legacy_rent_for_year(self, verpachtung, year):
"""Calculate rent for legacy Verpachtung for specific year"""
if not verpachtung.pachtzins_jaehrlich or not verpachtung.pachtbeginn:
return Decimal('0.00')
year_start = date(year, 1, 1)
year_end = date(year, 12, 31)
contract_end_date = verpachtung.verlaengerung if verpachtung.verlaengerung else verpachtung.pachtende
contract_start = max(verpachtung.pachtbeginn, year_start)
contract_end = min(contract_end_date or year_end, year_end)
if contract_start > contract_end:
return Decimal('0.00')
days_in_year = (year_end - year_start).days + 1
days_active = (contract_end - contract_start).days + 1
proportion = Decimal(str(days_active)) / Decimal(str(days_in_year))
return Decimal(str(verpachtung.pachtzins_jaehrlich)) * proportion
def _calculate_new_rent_for_year(self, verpachtung, year):
"""Calculate rent for new LandVerpachtung for specific year"""
if not verpachtung.pachtzins_pauschal or not verpachtung.pachtbeginn:
return Decimal('0.00')
year_start = date(year, 1, 1)
year_end = date(year, 12, 31)
contract_start = max(verpachtung.pachtbeginn, year_start)
contract_end = min(verpachtung.pachtende or year_end, year_end)
if contract_start > contract_end:
return Decimal('0.00')
days_in_year = (year_end - year_start).days + 1
days_active = (contract_end - contract_start).days + 1
proportion = Decimal(str(days_active)) / Decimal(str(days_in_year))
return Decimal(str(verpachtung.pachtzins_pauschal)) * proportion
def _update_abrechnung(self, land, year, rent_amount, umlage_amount, source_note, force):
"""Update or create Abrechnung for specific land and year"""
abrechnung, created = LandAbrechnung.objects.get_or_create(
land=land,
abrechnungsjahr=year,
defaults={
'pacht_vereinnahmt': rent_amount,
'umlagen_vereinnahmt': umlage_amount,
'bemerkungen': f'[{date.today().strftime("%d.%m.%Y")}] Automatisch synchronisiert von {source_note}'
}
)
updated = False
if not created and force:
# Update existing
abrechnung.pacht_vereinnahmt += rent_amount
abrechnung.umlagen_vereinnahmt += umlage_amount
sync_note = f'[{date.today().strftime("%d.%m.%Y")}] Resync: +{rent_amount:.2f}€ von {source_note}'
if abrechnung.bemerkungen:
abrechnung.bemerkungen += f'\n{sync_note}'
else:
abrechnung.bemerkungen = sync_note
abrechnung.save()
updated = True
return created, updated

View File

@@ -0,0 +1,16 @@
# Generated by Django 5.0.6 on 2025-08-31 20:25
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('stiftung', '0022_dokumentlink_land_verpachtung_id_and_more'),
]
operations = [
migrations.DeleteModel(
name='Verpachtung',
),
]

View File

@@ -0,0 +1,18 @@
# Generated by Django 5.0.6 on 2025-08-31 21:22
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('stiftung', '0023_remove_legacy_verpachtung'),
]
operations = [
migrations.AddField(
model_name='dokumentlink',
name='abrechnung_id',
field=models.UUIDField(blank=True, null=True, verbose_name='Abrechnung ID'),
),
]

View File

@@ -0,0 +1,37 @@
# Generated by Django 5.0.6 on 2025-08-31 22:08
import uuid
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('stiftung', '0024_dokumentlink_abrechnung_id'),
]
operations = [
migrations.CreateModel(
name='AppConfiguration',
fields=[
('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)),
('key', models.CharField(max_length=100, unique=True, verbose_name='Setting Key')),
('display_name', models.CharField(max_length=200, verbose_name='Display Name')),
('description', models.TextField(blank=True, null=True, verbose_name='Description')),
('value', models.TextField(verbose_name='Value')),
('default_value', models.TextField(verbose_name='Default Value')),
('setting_type', models.CharField(choices=[('text', 'Text'), ('number', 'Number'), ('boolean', 'Boolean'), ('url', 'URL'), ('tag', 'Tag Name'), ('tag_id', 'Tag ID')], default='text', max_length=20, verbose_name='Type')),
('category', models.CharField(choices=[('paperless', 'Paperless Integration'), ('general', 'General Settings'), ('notifications', 'Notifications'), ('system', 'System Settings')], default='general', max_length=50, verbose_name='Category')),
('is_active', models.BooleanField(default=True, verbose_name='Active')),
('is_system', models.BooleanField(default=False, verbose_name='System Setting (read-only)')),
('order', models.IntegerField(default=0, verbose_name='Display Order')),
('created_at', models.DateTimeField(auto_now_add=True)),
('updated_at', models.DateTimeField(auto_now=True)),
],
options={
'verbose_name': 'App Configuration',
'verbose_name_plural': 'App Configurations',
'ordering': ['category', 'order', 'display_name'],
},
),
]

View File

@@ -0,0 +1,89 @@
# Generated by Django 5.0.6 on 2025-09-01 20:03
import django.db.models.deletion
import uuid
from django.conf import settings
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('stiftung', '0025_appconfiguration'),
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]
operations = [
migrations.AddField(
model_name='destinataerunterstuetzung',
name='ausgezahlt_am',
field=models.DateField(blank=True, null=True, verbose_name='Ausgezahlt am'),
),
migrations.AddField(
model_name='destinataerunterstuetzung',
name='ausgezahlt_von',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to=settings.AUTH_USER_MODEL, verbose_name='Ausgezahlt von'),
),
migrations.AddField(
model_name='destinataerunterstuetzung',
name='empfaenger_iban',
field=models.CharField(blank=True, max_length=34, verbose_name='Empfänger IBAN'),
),
migrations.AddField(
model_name='destinataerunterstuetzung',
name='empfaenger_name',
field=models.CharField(blank=True, max_length=200, verbose_name='Empfänger Name'),
),
migrations.AddField(
model_name='destinataerunterstuetzung',
name='verwendungszweck',
field=models.CharField(blank=True, max_length=140, verbose_name='Verwendungszweck'),
),
migrations.AlterField(
model_name='destinataerunterstuetzung',
name='status',
field=models.CharField(choices=[('geplant', 'Geplant'), ('faellig', 'Fällig'), ('in_bearbeitung', 'In Bearbeitung'), ('ausgezahlt', 'Ausgezahlt'), ('storniert', 'Storniert')], default='geplant', max_length=20, verbose_name='Status'),
),
migrations.CreateModel(
name='UnterstuetzungWiederkehrend',
fields=[
('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)),
('betrag', models.DecimalField(decimal_places=2, max_digits=12, verbose_name='Betrag (€)')),
('intervall', models.CharField(choices=[('monatlich', 'Monatlich'), ('quartalsweise', 'Vierteljährlich'), ('halbjaehrlich', 'Halbjährlich'), ('jaehrlich', 'Jährlich')], max_length=20, verbose_name='Intervall')),
('beschreibung', models.CharField(blank=True, max_length=255, verbose_name='Beschreibung')),
('empfaenger_iban', models.CharField(max_length=34, verbose_name='Empfänger IBAN')),
('empfaenger_name', models.CharField(max_length=200, verbose_name='Empfänger Name')),
('verwendungszweck', models.CharField(blank=True, max_length=140, verbose_name='Verwendungszweck')),
('erste_zahlung_am', models.DateField(verbose_name='Erste Zahlung am')),
('letzte_zahlung_am', models.DateField(blank=True, null=True, verbose_name='Letzte Zahlung am (optional)')),
('naechste_generierung', models.DateField(verbose_name='Nächste Generierung')),
('aktiv', models.BooleanField(default=True, verbose_name='Aktiv')),
('erstellt_am', models.DateTimeField(auto_now_add=True)),
('destinataer', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='wiederkehrende_unterstuetzungen', to='stiftung.destinataer', verbose_name='Destinatär')),
('erstellt_von', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to=settings.AUTH_USER_MODEL, verbose_name='Erstellt von')),
('konto', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to='stiftung.stiftungskonto', verbose_name='Zahlungskonto')),
],
options={
'verbose_name': 'Wiederkehrende Unterstützung',
'verbose_name_plural': 'Wiederkehrende Unterstützungen',
'ordering': ['-erstellt_am'],
},
),
migrations.AddField(
model_name='destinataerunterstuetzung',
name='wiederkehrend_von',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='stiftung.unterstuetzungwiederkehrend', verbose_name='Wiederkehrende Zahlung'),
),
migrations.AddIndex(
model_name='destinataerunterstuetzung',
index=models.Index(fields=['wiederkehrend_von'], name='stiftung_de_wiederk_3d5afc_idx'),
),
migrations.AddIndex(
model_name='unterstuetzungwiederkehrend',
index=models.Index(fields=['aktiv', 'naechste_generierung'], name='stiftung_un_aktiv_b957e5_idx'),
),
migrations.AddIndex(
model_name='unterstuetzungwiederkehrend',
index=models.Index(fields=['destinataer', 'aktiv'], name='stiftung_un_destina_2232fc_idx'),
),
]

View File

@@ -0,0 +1,38 @@
# Generated by Django 5.0.6 on 2025-09-02 19:56
import uuid
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('stiftung', '0026_enhance_unterstuetzung_model'),
]
operations = [
migrations.CreateModel(
name='HelpBox',
fields=[
('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)),
('page_key', models.CharField(choices=[('destinataer_new', 'Neuer Destinatär'), ('unterstuetzung_new', 'Neue Unterstützung'), ('foerderung_new', 'Neue Förderung'), ('paechter_new', 'Neuer Pächter'), ('laenderei_new', 'Neue Länderei'), ('verpachtung_new', 'Neue Verpachtung'), ('person_new', 'Neue Person'), ('konto_new', 'Neues Konto')], max_length=50, unique=True, verbose_name='Seite')),
('title', models.CharField(max_length=200, verbose_name='Titel der Hilfsbox')),
('content', models.TextField(help_text='Sie können Markdown verwenden: **fett**, *kursiv*, `code`, [Link](url), etc.', verbose_name='Inhalt (Markdown unterstützt)')),
('is_active', models.BooleanField(default=True, verbose_name='Aktiv')),
('created_at', models.DateTimeField(auto_now_add=True, verbose_name='Erstellt am')),
('updated_at', models.DateTimeField(auto_now=True, verbose_name='Aktualisiert am')),
('created_by', models.CharField(blank=True, max_length=100, null=True, verbose_name='Erstellt von')),
('updated_by', models.CharField(blank=True, max_length=100, null=True, verbose_name='Aktualisiert von')),
],
options={
'verbose_name': 'Hilfs-Infobox',
'verbose_name_plural': 'Hilfs-Infoboxen',
'ordering': ['page_key'],
},
),
migrations.AlterField(
model_name='appconfiguration',
name='category',
field=models.CharField(choices=[('paperless', 'Paperless Integration'), ('general', 'General Settings'), ('corporate', 'Corporate Identity'), ('notifications', 'Notifications'), ('system', 'System Settings')], default='general', max_length=50, verbose_name='Category'),
),
]

View File

@@ -0,0 +1,18 @@
# Generated by Django 5.0.6 on 2025-09-02 20:53
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('stiftung', '0027_helpbox_alter_appconfiguration_category'),
]
operations = [
migrations.AlterField(
model_name='helpbox',
name='page_key',
field=models.CharField(choices=[('destinataer_new', 'Neuer Destinatär'), ('unterstuetzung_new', 'Neue Unterstützung'), ('foerderung_new', 'Neue Förderung'), ('paechter_new', 'Neuer Pächter'), ('laenderei_new', 'Neue Länderei'), ('verpachtung_new', 'Neue Verpachtung'), ('land_abrechnung_new', 'Neue Landabrechnung'), ('person_new', 'Neue Person'), ('konto_new', 'Neues Konto'), ('verwaltungskosten_new', 'Neue Verwaltungskosten'), ('rentmeister_new', 'Neuer Rentmeister'), ('dokument_new', 'Neues Dokument'), ('user_new', 'Neuer Benutzer'), ('csv_import_new', 'CSV Import'), ('destinataer_notiz_new', 'Destinatär Notiz')], max_length=50, unique=True, verbose_name='Seite'),
),
]

View File

@@ -4,6 +4,8 @@ from io import StringIO
from django.db import models
from django.core.validators import MinValueValidator, MaxValueValidator
from django.utils import timezone
from dateutil.relativedelta import relativedelta
from stiftung.utils.date_utils import ensure_date, get_year_from_date
class CSVImport(models.Model):
"""Track CSV import operations for audit purposes"""
@@ -129,18 +131,18 @@ class Paechter(models.Model):
def get_aktive_verpachtungen(self):
"""Get all active leases for this tenant"""
return self.verpachtung_set.filter(status='aktiv')
return self.neue_verpachtungen.filter(status='aktiv')
def get_gesamt_pachtflaeche(self):
"""Calculate total leased area"""
return self.verpachtung_set.filter(status='aktiv').aggregate(
return self.neue_verpachtungen.filter(status='aktiv').aggregate(
total=models.Sum('verpachtete_flaeche')
)['total'] or 0
def get_gesamt_pachtzins(self):
"""Calculate total annual rent"""
return self.verpachtung_set.filter(status='aktiv').aggregate(
total=models.Sum('pachtzins_jaehrlich')
return self.neue_verpachtungen.filter(status='aktiv').aggregate(
total=models.Sum('pachtzins_pauschal')
)['total'] or 0
class Destinataer(models.Model):
@@ -505,12 +507,8 @@ class Land(models.Model):
if self.verp_flaeche_aktuell and self.verp_flaeche_aktuell > 0:
return self.verp_flaeche_aktuell
# Fallback: Legacy Verpachtungen (für Rückwärtskompatibilität)
legacy_total = self.verpachtung_set.filter(status='aktiv').aggregate(
total=Sum('verpachtete_flaeche')
)['total'] or 0
return legacy_total
# No legacy system - return neue_total (could be 0)
return neue_total
def get_verfuegbare_flaeche(self):
"""Berechnet die noch verfügbare Fläche für neue Verpachtungen"""
@@ -690,17 +688,29 @@ class LandVerpachtung(models.Model):
def is_aktiv(self):
"""Prüft ob der Vertrag noch aktiv ist"""
from datetime import date
heute = date.today()
if self.pachtende:
return self.pachtbeginn <= heute <= self.pachtende
return self.pachtbeginn <= heute # Unbefristet
pachtbeginn_date = ensure_date(self.pachtbeginn)
pachtende_date = ensure_date(self.pachtende)
if not pachtbeginn_date:
return False
if pachtende_date:
return pachtbeginn_date <= heute <= pachtende_date
return pachtbeginn_date <= heute # Unbefristet
def get_restlaufzeit_tage(self):
"""Berechnet die Restlaufzeit in Tagen"""
from datetime import date
heute = date.today()
if self.pachtende and self.pachtende > heute:
return (self.pachtende - heute).days
pachtende_date = ensure_date(self.pachtende)
if pachtende_date and pachtende_date > heute:
return (pachtende_date - heute).days
return None # Unbefristet
@property
@@ -708,10 +718,199 @@ class LandVerpachtung(models.Model):
"""Berechnet die USt auf Pacht (falls optiert)"""
from decimal import Decimal, ROUND_HALF_UP
if self.ust_option and self.pachtzins_pauschal:
ust = self.pachtzins_pauschal * (self.ust_satz / Decimal('100'))
return ust.quantize(Decimal('0.01'), rounding=ROUND_HALF_UP)
ust_betrag = Decimal(str(self.pachtzins_pauschal)) * Decimal(str(self.ust_satz)) / Decimal('100')
return ust_betrag.quantize(Decimal('0.01'), rounding=ROUND_HALF_UP)
return Decimal('0.00')
def save(self, *args, **kwargs):
"""Override save to trigger Abrechnung updates"""
is_new = self.pk is None
old_instance = None
if not is_new:
try:
old_instance = LandVerpachtung.objects.get(pk=self.pk)
except LandVerpachtung.DoesNotExist:
old_instance = None
super().save(*args, **kwargs)
# Update Abrechnungen after save
self._update_abrechnungen(old_instance, is_new)
def _update_abrechnungen(self, old_instance, is_new):
"""Update LandAbrechnung records when Verpachtung changes"""
from datetime import date
# Determine affected years
years_to_update = set()
pachtbeginn_year = get_year_from_date(self.pachtbeginn)
if pachtbeginn_year:
years_to_update.add(pachtbeginn_year)
pachtende_year = get_year_from_date(self.pachtende)
if pachtende_year:
years_to_update.add(pachtende_year)
# If updated, check old dates too
if old_instance:
old_pachtbeginn_year = get_year_from_date(old_instance.pachtbeginn)
if old_pachtbeginn_year:
years_to_update.add(old_pachtbeginn_year)
old_pachtende_year = get_year_from_date(old_instance.pachtende)
if old_pachtende_year:
years_to_update.add(old_pachtende_year)
# Add current year if contract is active
if self.is_aktiv():
years_to_update.add(date.today().year)
# Update each affected year
for year in years_to_update:
self._update_abrechnung_for_year(year, old_instance, is_new)
def _update_abrechnung_for_year(self, year, old_instance, is_new):
"""Update or create LandAbrechnung for specific year"""
from decimal import Decimal
from datetime import date
# Get or create Abrechnung for this year
abrechnung, created = LandAbrechnung.objects.get_or_create(
land=self.land,
abrechnungsjahr=year,
defaults={
'pacht_vereinnahmt': Decimal('0.00'),
'umlagen_vereinnahmt': Decimal('0.00'),
'bemerkungen': f'Automatisch erstellt für {self.vertragsnummer}'
}
)
# Calculate rent for this year
rent_for_year = self._calculate_rent_for_year(year)
umlage_for_year = self._calculate_umlage_for_year(year)
# Update or add to existing amounts
if created or is_new:
# New Abrechnung or new Verpachtung
abrechnung.pacht_vereinnahmt += rent_for_year
abrechnung.umlagen_vereinnahmt += umlage_for_year
change_note = f"Neue Verpachtung {self.vertragsnummer} hinzugefügt"
else:
# Update existing - calculate difference
old_rent = old_instance._calculate_rent_for_year(year) if old_instance else Decimal('0.00')
old_umlage = old_instance._calculate_umlage_for_year(year) if old_instance else Decimal('0.00')
rent_diff = rent_for_year - old_rent
umlage_diff = umlage_for_year - old_umlage
abrechnung.pacht_vereinnahmt += rent_diff
abrechnung.umlagen_vereinnahmt += umlage_diff
if rent_diff != 0 or umlage_diff != 0:
change_note = f"Verpachtung {self.vertragsnummer} geändert: Pacht {rent_diff:+.2f}€, Umlagen {umlage_diff:+.2f}"
else:
change_note = f"Verpachtung {self.vertragsnummer} aktualisiert (keine Betragsänderung)"
# Add change tracking to bemerkungen (if significant change)
if change_note and ('hinzugefügt' in change_note or 'geändert' in change_note):
if abrechnung.bemerkungen:
abrechnung.bemerkungen += f"\n[{date.today().strftime('%d.%m.%Y')}] {change_note}"
else:
abrechnung.bemerkungen = f"[{date.today().strftime('%d.%m.%Y')}] {change_note}"
abrechnung.save()
def _calculate_rent_for_year(self, year):
"""Calculate rent amount for specific year"""
from decimal import Decimal
from datetime import date
from django.utils.dateparse import parse_date
# Helper function to convert date strings to date objects
def ensure_date(date_value):
if not date_value:
return None
if isinstance(date_value, str):
return parse_date(date_value)
return date_value
if not self.pachtzins_pauschal or not self.pachtbeginn:
return Decimal('0.00')
# Check if contract is active in this year
year_start = date(year, 1, 1)
year_end = date(year, 12, 31)
# Convert dates to ensure they are date objects
pachtbeginn_date = ensure_date(self.pachtbeginn)
pachtende_date = ensure_date(self.pachtende)
if not pachtbeginn_date:
return Decimal('0.00')
contract_start = max(pachtbeginn_date, year_start)
contract_end = min(pachtende_date or year_end, year_end)
if contract_start > contract_end:
return Decimal('0.00') # No overlap
# Calculate proportion of year
days_in_year = (year_end - year_start).days + 1
days_active = (contract_end - contract_start).days + 1
proportion = Decimal(str(days_active)) / Decimal(str(days_in_year))
return Decimal(str(self.pachtzins_pauschal)) * proportion
def _calculate_umlage_for_year(self, year):
"""Calculate Umlage amount for specific year based on what can be passed through"""
from decimal import Decimal
# This would need to be calculated based on actual costs and what's umlagefähig
# For now, return 0 - this can be enhanced later with actual cost calculation
return Decimal('0.00')
def delete(self, *args, **kwargs):
"""Override delete to update Abrechnungen when Verpachtung is removed"""
# Calculate what needs to be removed from Abrechnungen
years_to_update = set()
pachtbeginn_year = get_year_from_date(self.pachtbeginn)
if pachtbeginn_year:
years_to_update.add(pachtbeginn_year)
pachtende_year = get_year_from_date(self.pachtende)
if pachtende_year:
years_to_update.add(pachtende_year)
# Remove from Abrechnungen before deleting
for year in years_to_update:
try:
abrechnung = LandAbrechnung.objects.get(land=self.land, abrechnungsjahr=year)
rent_to_remove = self._calculate_rent_for_year(year)
umlage_to_remove = self._calculate_umlage_for_year(year)
abrechnung.pacht_vereinnahmt -= rent_to_remove
abrechnung.umlagen_vereinnahmt -= umlage_to_remove
# Add deletion note
from datetime import date
change_note = f"Verpachtung {self.vertragsnummer} gelöscht: Pacht -{rent_to_remove:.2f}€, Umlagen -{umlage_to_remove:.2f}"
if abrechnung.bemerkungen:
abrechnung.bemerkungen += f"\n[{date.today().strftime('%d.%m.%Y')}] {change_note}"
else:
abrechnung.bemerkungen = f"[{date.today().strftime('%d.%m.%Y')}] {change_note}"
abrechnung.save()
except LandAbrechnung.DoesNotExist:
pass # No Abrechnung to update
super().delete(*args, **kwargs)
class LandAbrechnung(models.Model):
"""Jahresabrechnung für Ländereien"""
@@ -877,99 +1076,6 @@ class LandAbrechnung(models.Model):
return ust.quantize(Decimal('0.01'), rounding=ROUND_HALF_UP)
return Decimal('0.00')
class Verpachtung(models.Model):
"""Verpachtungsverträge für Ländereien"""
STATUS_CHOICES = [
('aktiv', 'Aktiv'),
('beendet', 'Beendet'),
('gekuendigt', 'Gekündigt'),
('verlängert', 'Verlängert'),
]
id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
# Verpachtete Ländereien
land = models.ForeignKey(Land, on_delete=models.CASCADE, verbose_name="Land")
paechter = models.ForeignKey(Paechter, on_delete=models.CASCADE, verbose_name="Pächter")
# Vertragsdaten
vertragsnummer = models.CharField(max_length=50, unique=True, verbose_name="Vertragsnummer")
pachtbeginn = models.DateField(verbose_name="Pachtbeginn")
pachtende = models.DateField(verbose_name="Pachtende")
verlaengerung = models.DateField(null=True, blank=True, verbose_name="Verlängerung bis")
# Finanzielle Bedingungen
pachtzins_pro_qm = models.DecimalField(
max_digits=8,
decimal_places=4,
verbose_name="Pachtzins pro qm (€)"
)
pachtzins_jaehrlich = models.DecimalField(
max_digits=12,
decimal_places=2,
verbose_name="Jährlicher Pachtzins (€)"
)
# Flächenangaben
verpachtete_flaeche = models.DecimalField(
max_digits=12,
decimal_places=2,
verbose_name="Verpachtete Fläche (qm)"
)
# Status
status = models.CharField(max_length=20, choices=STATUS_CHOICES, default='aktiv')
# Dokumentation
verwendungsnachweis = models.ForeignKey('DokumentLink', null=True, blank=True, on_delete=models.SET_NULL, verbose_name="Verwendungsnachweis")
bemerkungen = models.TextField(null=True, blank=True, verbose_name="Ergänzende Kommentare")
# Zeitstempel
erstellt_am = models.DateTimeField(auto_now_add=True)
aktualisiert_am = models.DateTimeField(auto_now=True)
class Meta:
verbose_name = "Verpachtung"
verbose_name_plural = "Verpachtungen"
ordering = ['-pachtbeginn']
def __str__(self):
return f"{self.land} - {self.paechter} ({self.vertragsnummer})"
def get_vertragsdauer_tage(self):
"""Berechnet die Vertragsdauer in Tagen"""
from datetime import date
if self.pachtende:
return (self.pachtende - self.pachtbeginn).days
return 0
def get_restlaufzeit_tage(self):
"""Berechnet die Restlaufzeit in Tagen"""
from datetime import date
heute = date.today()
if self.pachtende and self.pachtende > heute:
return (self.pachtende - heute).days
return 0
def is_aktiv(self):
"""Prüft ob der Vertrag noch aktiv ist"""
from datetime import date
heute = date.today()
return self.pachtbeginn <= heute <= self.pachtende
@property
def verpachtete_flaeche_hektar(self):
"""Berechnet die verpachtete Fläche in Hektar"""
from decimal import Decimal, ROUND_HALF_UP
if self.verpachtete_flaeche and self.verpachtete_flaeche > 0:
# Umrechnung: 1 Hektar = 10.000 qm
hektar = Decimal(str(self.verpachtete_flaeche)) / Decimal('10000')
# Runden auf 2 Nachkommastellen
return hektar.quantize(Decimal('0.01'), rounding=ROUND_HALF_UP)
return Decimal('0.00')
class DokumentLink(models.Model):
KONTEXT_CHOICES = [
('pachtvertrag', 'Pachtvertrag'),
@@ -997,6 +1103,7 @@ class DokumentLink(models.Model):
destinataer_id = models.UUIDField(null=True, blank=True, verbose_name="Destinatär ID")
foerderung_id = models.UUIDField(null=True, blank=True, verbose_name="Förderung ID")
rentmeister_id = models.UUIDField(null=True, blank=True, verbose_name="Rentmeister ID")
abrechnung_id = models.UUIDField(null=True, blank=True, verbose_name="Abrechnung ID")
class Meta:
verbose_name = "Dokument"
@@ -1007,17 +1114,14 @@ class DokumentLink(models.Model):
return f"{self.titel} ({self.get_kontext_display()})"
def get_paperless_url(self):
"""Gibt die URL zum Dokument in Paperless zurück"""
from django.conf import settings
if settings.PAPERLESS_API_URL:
return f"{settings.PAPERLESS_API_URL}/documents/{self.paperless_document_id}/"
return None
"""Gibt die URL zum Dokument in Paperless zurück (über Django Redirect)"""
return f"/api/paperless/documents/{self.paperless_document_id}/"
def get_paperless_thumbnail_url(self):
"""Gibt die URL zum Thumbnail in Paperless zurück"""
from django.conf import settings
if settings.PAPERLESS_API_URL:
return f"{settings.PAPERLESS_API_URL}/api/documents/{self.paperless_document_id}/thumb/"
return f"{settings.PAPERLESS_API_URL}/api/paperless/documents/{self.paperless_document_id}/thumb/"
return None
def get_verpachtung(self):
@@ -1135,6 +1239,7 @@ class DestinataerUnterstuetzung(models.Model):
"""Geplante/ausgeführte Unterstützungszahlungen an Destinatäre"""
STATUS_CHOICES = [
('geplant', 'Geplant'),
('faellig', 'Fällig'),
('in_bearbeitung', 'In Bearbeitung'),
('ausgezahlt', 'Ausgezahlt'),
('storniert', 'Storniert'),
@@ -1147,6 +1252,17 @@ class DestinataerUnterstuetzung(models.Model):
faellig_am = models.DateField(verbose_name='Fällig am')
status = models.CharField(max_length=20, choices=STATUS_CHOICES, default='geplant', verbose_name='Status')
beschreibung = models.CharField(max_length=255, blank=True, verbose_name='Beschreibung')
# Enhanced fields for recurrent payments and IBAN tracking
empfaenger_iban = models.CharField(max_length=34, blank=True, verbose_name='Empfänger IBAN')
empfaenger_name = models.CharField(max_length=200, blank=True, verbose_name='Empfänger Name')
verwendungszweck = models.CharField(max_length=140, blank=True, verbose_name='Verwendungszweck')
ausgezahlt_am = models.DateField(null=True, blank=True, verbose_name='Ausgezahlt am')
ausgezahlt_von = models.ForeignKey('auth.User', on_delete=models.SET_NULL, null=True, blank=True, verbose_name='Ausgezahlt von')
# Link to recurrent payment template if this was auto-generated
wiederkehrend_von = models.ForeignKey('UnterstuetzungWiederkehrend', on_delete=models.SET_NULL, null=True, blank=True, verbose_name='Wiederkehrende Zahlung')
erstellt_am = models.DateTimeField(auto_now_add=True)
aktualisiert_am = models.DateTimeField(auto_now=True)
@@ -1157,11 +1273,107 @@ class DestinataerUnterstuetzung(models.Model):
indexes = [
models.Index(fields=['status', 'faellig_am']),
models.Index(fields=['destinataer', 'status']),
models.Index(fields=['wiederkehrend_von']),
]
def __str__(self):
return f"{self.destinataer.get_full_name()} {self.betrag} am {self.faellig_am} ({self.get_status_display()})"
def is_overdue(self):
"""Check if payment is overdue"""
from django.utils import timezone
return self.faellig_am < timezone.now().date() and self.status in ['geplant', 'faellig']
def can_be_marked_paid(self):
"""Check if payment can be marked as paid"""
return self.status in ['geplant', 'faellig', 'in_bearbeitung']
class UnterstuetzungWiederkehrend(models.Model):
"""Template for recurring support payments"""
INTERVALL_CHOICES = [
('monatlich', 'Monatlich'),
('quartalsweise', 'Vierteljährlich'),
('halbjaehrlich', 'Halbjährlich'),
('jaehrlich', 'Jährlich'),
]
id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
destinataer = models.ForeignKey('Destinataer', on_delete=models.CASCADE, related_name='wiederkehrende_unterstuetzungen', verbose_name='Destinatär')
konto = models.ForeignKey('StiftungsKonto', on_delete=models.PROTECT, verbose_name='Zahlungskonto')
betrag = models.DecimalField(max_digits=12, decimal_places=2, verbose_name='Betrag (€)')
intervall = models.CharField(max_length=20, choices=INTERVALL_CHOICES, verbose_name='Intervall')
beschreibung = models.CharField(max_length=255, blank=True, verbose_name='Beschreibung')
# IBAN and payment details
empfaenger_iban = models.CharField(max_length=34, verbose_name='Empfänger IBAN')
empfaenger_name = models.CharField(max_length=200, verbose_name='Empfänger Name')
verwendungszweck = models.CharField(max_length=140, blank=True, verbose_name='Verwendungszweck')
# Schedule settings
erste_zahlung_am = models.DateField(verbose_name='Erste Zahlung am')
letzte_zahlung_am = models.DateField(null=True, blank=True, verbose_name='Letzte Zahlung am (optional)')
naechste_generierung = models.DateField(verbose_name='Nächste Generierung')
aktiv = models.BooleanField(default=True, verbose_name='Aktiv')
erstellt_am = models.DateTimeField(auto_now_add=True)
erstellt_von = models.ForeignKey('auth.User', on_delete=models.SET_NULL, null=True, blank=True, verbose_name='Erstellt von')
class Meta:
verbose_name = 'Wiederkehrende Unterstützung'
verbose_name_plural = 'Wiederkehrende Unterstützungen'
ordering = ['-erstellt_am']
indexes = [
models.Index(fields=['aktiv', 'naechste_generierung']),
models.Index(fields=['destinataer', 'aktiv']),
]
def __str__(self):
return f"{self.destinataer.get_full_name()} {self.get_intervall_display()}{self.betrag}"
def generiere_naechste_zahlung(self):
"""Generate the next scheduled payment"""
from datetime import timedelta
from dateutil.relativedelta import relativedelta
if not self.aktiv:
return None
heute = timezone.now().date()
if self.naechste_generierung > heute:
return None # Not yet time to generate
# Check if we've reached the end date
if self.letzte_zahlung_am and self.naechste_generierung > self.letzte_zahlung_am:
return None
# Create the next payment
neue_zahlung = DestinataerUnterstuetzung.objects.create(
destinataer=self.destinataer,
konto=self.konto,
betrag=self.betrag,
faellig_am=self.naechste_generierung,
beschreibung=self.beschreibung or f"{self.get_intervall_display()} Unterstützung",
empfaenger_iban=self.empfaenger_iban,
empfaenger_name=self.empfaenger_name,
verwendungszweck=self.verwendungszweck,
wiederkehrend_von=self,
status='geplant'
)
# Calculate next generation date
if self.intervall == 'monatlich':
self.naechste_generierung = self.naechste_generierung + relativedelta(months=1)
elif self.intervall == 'quartalsweise':
self.naechste_generierung = self.naechste_generierung + relativedelta(months=3)
elif self.intervall == 'halbjaehrlich':
self.naechste_generierung = self.naechste_generierung + relativedelta(months=6)
elif self.intervall == 'jaehrlich':
self.naechste_generierung = self.naechste_generierung + relativedelta(years=1)
self.save()
return neue_zahlung
class DestinataerNotiz(models.Model):
"""Zeitgestempelte Notizen/Telefonvermerke zu einem Destinatär, optional mit Datei."""
@@ -1665,3 +1877,153 @@ class BackupJob(models.Model):
return f"{size:.1f} {unit}"
size /= 1024
return f"{size:.1f} TB"
class AppConfiguration(models.Model):
"""Application configuration settings that can be managed through the admin interface"""
SETTING_TYPE_CHOICES = [
('text', 'Text'),
('number', 'Number'),
('boolean', 'Boolean'),
('url', 'URL'),
('tag', 'Tag Name'),
('tag_id', 'Tag ID'),
]
CATEGORY_CHOICES = [
('paperless', 'Paperless Integration'),
('general', 'General Settings'),
('corporate', 'Corporate Identity'),
('notifications', 'Notifications'),
('system', 'System Settings'),
]
id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
key = models.CharField(max_length=100, unique=True, verbose_name="Setting Key")
display_name = models.CharField(max_length=200, verbose_name="Display Name")
description = models.TextField(blank=True, null=True, verbose_name="Description")
value = models.TextField(verbose_name="Value")
default_value = models.TextField(verbose_name="Default Value")
setting_type = models.CharField(max_length=20, choices=SETTING_TYPE_CHOICES, default='text', verbose_name="Type")
category = models.CharField(max_length=50, choices=CATEGORY_CHOICES, default='general', verbose_name="Category")
is_active = models.BooleanField(default=True, verbose_name="Active")
is_system = models.BooleanField(default=False, verbose_name="System Setting (read-only)")
order = models.IntegerField(default=0, verbose_name="Display Order")
# Metadata
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
class Meta:
verbose_name = "App Configuration"
verbose_name_plural = "App Configurations"
ordering = ['category', 'order', 'display_name']
def __str__(self):
return f"{self.display_name} ({self.key})"
def get_typed_value(self):
"""Return the value converted to the appropriate type"""
if self.setting_type == 'boolean':
return self.value.lower() in ('true', '1', 'yes', 'on')
elif self.setting_type == 'number':
try:
if '.' in self.value:
return float(self.value)
return int(self.value)
except (ValueError, TypeError):
return 0
return self.value
@classmethod
def get_setting(cls, key, default=None):
"""Get a setting value by key"""
try:
setting = cls.objects.get(key=key, is_active=True)
return setting.get_typed_value()
except cls.DoesNotExist:
return default
@classmethod
def set_setting(cls, key, value, display_name=None, description=None, setting_type='text', category='general'):
"""Set or update a setting value"""
setting, created = cls.objects.get_or_create(
key=key,
defaults={
'display_name': display_name or key,
'description': description,
'value': str(value),
'default_value': str(value),
'setting_type': setting_type,
'category': category,
}
)
if not created:
setting.value = str(value)
setting.save()
return setting
class HelpBox(models.Model):
"""Editierbare Hilfe-Infoboxen für Formulare"""
PAGE_CHOICES = [
('destinataer_new', 'Neuer Destinatär'),
('unterstuetzung_new', 'Neue Unterstützung'),
('foerderung_new', 'Neue Förderung'),
('paechter_new', 'Neuer Pächter'),
('laenderei_new', 'Neue Länderei'),
('verpachtung_new', 'Neue Verpachtung'),
('land_abrechnung_new', 'Neue Landabrechnung'),
('person_new', 'Neue Person'),
('konto_new', 'Neues Konto'),
('verwaltungskosten_new', 'Neue Verwaltungskosten'),
('rentmeister_new', 'Neuer Rentmeister'),
('dokument_new', 'Neues Dokument'),
('user_new', 'Neuer Benutzer'),
('csv_import_new', 'CSV Import'),
('destinataer_notiz_new', 'Destinatär Notiz'),
]
id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
page_key = models.CharField(
max_length=50,
choices=PAGE_CHOICES,
unique=True,
verbose_name="Seite"
)
title = models.CharField(
max_length=200,
verbose_name="Titel der Hilfsbox"
)
content = models.TextField(
verbose_name="Inhalt (Markdown unterstützt)",
help_text="Sie können Markdown verwenden: **fett**, *kursiv*, `code`, [Link](url), etc."
)
is_active = models.BooleanField(
default=True,
verbose_name="Aktiv"
)
# Metadata
created_at = models.DateTimeField(auto_now_add=True, verbose_name="Erstellt am")
updated_at = models.DateTimeField(auto_now=True, verbose_name="Aktualisiert am")
created_by = models.CharField(max_length=100, null=True, blank=True, verbose_name="Erstellt von")
updated_by = models.CharField(max_length=100, null=True, blank=True, verbose_name="Aktualisiert von")
class Meta:
verbose_name = "Hilfs-Infobox"
verbose_name_plural = "Hilfs-Infoboxen"
ordering = ['page_key']
def __str__(self):
return f"{self.get_page_key_display()}: {self.title}"
@classmethod
def get_help_for_page(cls, page_key):
"""Hole die aktive Hilfs-Infobox für eine bestimmte Seite"""
try:
return cls.objects.get(page_key=page_key, is_active=True)
except cls.DoesNotExist:
return None

View File

View File

@@ -0,0 +1,29 @@
import markdown
from django import template
from django.utils.safestring import mark_safe
from stiftung.models import HelpBox
register = template.Library()
@register.inclusion_tag('stiftung/help_box.html')
def help_box(page_key, user=None):
"""Rendere eine Hilfs-Infobox für eine bestimmte Seite"""
help_obj = HelpBox.get_help_for_page(page_key)
context = {
'help_obj': help_obj,
'page_key': page_key,
'can_edit': user and (user.username == 'root' or user.is_superuser) if user else False
}
if help_obj:
# Konvertiere Markdown zu HTML
md = markdown.Markdown(extensions=['nl2br', 'fenced_code'])
context['content_html'] = mark_safe(md.convert(help_obj.content))
return context
@register.simple_tag
def help_box_exists(page_key):
"""Prüfe, ob eine Hilfs-Infobox für eine Seite existiert"""
return HelpBox.get_help_for_page(page_key) is not None

View File

@@ -0,0 +1,145 @@
"""
PDF-specific template tags and filters
"""
from django import template
from django.utils.safestring import mark_safe
register = template.Library()
@register.filter
def lookup(obj, field_name):
"""
Template filter to dynamically access object attributes or dict keys
Usage: {{ object|lookup:"field_name" }}
"""
if obj is None:
return None
# Handle dict-like objects
if hasattr(obj, '__getitem__') and not isinstance(obj, str):
try:
return obj[field_name]
except (KeyError, TypeError):
pass
# Handle objects with attributes
if hasattr(obj, field_name):
attr = getattr(obj, field_name)
# If it's a callable (method), call it
if callable(attr):
try:
return attr()
except TypeError:
# Method requires arguments, return as is
return attr
return attr
# Try to handle nested field access (e.g., "person.name")
if '.' in field_name:
parts = field_name.split('.')
current = obj
for part in parts:
if current is None:
return None
current = lookup(current, part)
return current
return None
@register.filter
def get_display_value(obj, field_name):
"""
Get the display value for a field, including choices
Usage: {{ object|get_display_value:"field_name" }}
"""
value = lookup(obj, field_name)
# Try to get display value for choice fields
display_method = f'get_{field_name}_display'
if hasattr(obj, display_method):
display_value = getattr(obj, display_method)()
if display_value:
return display_value
return value
@register.filter
def format_currency(value):
"""
Format a number as currency
Usage: {{ value|format_currency }}
"""
if value is None:
return '-'
try:
return f"{float(value):,.2f}".replace(',', ' ')
except (ValueError, TypeError):
return str(value)
@register.filter
def format_status_badge(status):
"""
Format status as HTML badge
Usage: {{ status|format_status_badge }}
"""
if not status:
return '-'
status_lower = str(status).lower()
css_class = f'status-{status_lower}'
return mark_safe(f'<span class="status-badge {css_class}">{status}</span>')
@register.filter
def truncate_field(value, max_length=50):
"""
Truncate field value for display
Usage: {{ value|truncate_field:30 }}
"""
if value is None:
return '-'
str_value = str(value)
if len(str_value) <= max_length:
return str_value
return str_value[:max_length-3] + '...'
@register.simple_tag
def get_field_value(obj, field_config):
"""
Get formatted field value based on field configuration
Usage: {% get_field_value object field_config %}
"""
field_name = field_config.get('field_name')
field_type = field_config.get('field_type', 'text')
value = lookup(obj, field_name)
if value is None:
return '-'
if field_type == 'currency':
return format_currency(value)
elif field_type == 'date':
try:
return value.strftime('%d.%m.%Y')
except (AttributeError, ValueError):
return str(value)
elif field_type == 'datetime':
try:
return value.strftime('%d.%m.%Y %H:%M')
except (AttributeError, ValueError):
return str(value)
elif field_type == 'status':
return format_status_badge(value)
elif field_type == 'boolean':
return 'Ja' if value else 'Nein'
else:
return truncate_field(value)

View File

@@ -53,13 +53,10 @@ urlpatterns = [
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'),
# LandVerpachtung URLs (neue Verpachtungen)
path('laendereien/verpachtungen/<uuid:pk>/', views.land_verpachtung_detail, name='land_verpachtung_detail'),
path('laendereien/verpachtungen/<uuid:pk>/bearbeiten/', views.land_verpachtung_update, name='land_verpachtung_update'),
path('laendereien/verpachtungen/<uuid:pk>/beenden/', views.land_verpachtung_end_direct, name='land_verpachtung_end_direct'),
# Förderung URLs
path('foerderungen/', views.foerderung_list, name='foerderung_list'),
@@ -112,6 +109,7 @@ urlpatterns = [
# Administration URLs
path('administration/', views.administration, name='administration'),
path('administration/settings/', views.app_settings, name='app_settings'),
path('administration/audit-log/', views.audit_log_list, name='audit_log_list'),
path('administration/backup/', views.backup_management, name='backup_management'),
path('administration/backup/<uuid:backup_id>/download/', views.backup_download, name='backup_download'),
@@ -120,6 +118,16 @@ urlpatterns = [
path('administration/unterstuetzungen/<uuid:pk>/bearbeiten/', views.unterstuetzung_edit, name='unterstuetzung_edit'),
path('administration/unterstuetzungen/<uuid:pk>/loeschen/', views.unterstuetzung_delete, name='unterstuetzung_delete'),
# Unterstützungen URLs (direct access from Destinataer)
path('unterstuetzungen/', views.unterstuetzungen_all, name='unterstuetzungen_all'),
path('unterstuetzungen/neu/', views.unterstuetzung_create, name='unterstuetzung_create'),
path('unterstuetzungen/<uuid:pk>/', views.unterstuetzung_detail, name='unterstuetzung_detail'),
path('unterstuetzungen/<uuid:pk>/bezahlt/', views.unterstuetzung_mark_paid, name='unterstuetzung_mark_paid'),
path('unterstuetzungen/wiederkehrend/', views.wiederkehrende_unterstuetzungen, name='wiederkehrende_unterstuetzungen'),
# AJAX endpoints
path('api/destinataer/<uuid:destinataer_id>/info/', views.get_destinataer_info, name='get_destinataer_info'),
# Authentication URLs
path('login/', views.user_login, name='login'),
path('logout/', views.user_logout, name='logout'),
@@ -133,6 +141,10 @@ urlpatterns = [
path('administration/users/<int:pk>/permissions/', views.user_permissions, name='user_permissions'),
path('administration/users/<int:pk>/delete/', views.user_delete, name='user_delete'),
# Hilfsbox URLs
path('help-box/edit/', views.edit_help_box, name='edit_help_box'),
path('help-box/admin/', views.edit_help_box, name='help_boxes_admin'),
# API URLs
path('api/land-stats/', views.land_stats_api, name='land_stats_api'),
path('api/health/', views.health_check, name='health_check'),

View File

@@ -0,0 +1,73 @@
"""
Configuration utilities for the Stiftung application
"""
from django.conf import settings
from stiftung.models import AppConfiguration
def get_config(key, default=None, fallback_to_settings=True):
"""
Get a configuration value from the database or fall back to Django settings
Args:
key: The configuration key
default: Default value if not found
fallback_to_settings: If True, try to get from Django settings using the key in uppercase
Returns:
The configuration value
"""
# Try to get from AppConfiguration first
value = AppConfiguration.get_setting(key, None)
# Fall back to Django settings if value is None or empty string
if not value and fallback_to_settings:
settings_key = key.upper()
return getattr(settings, settings_key, default)
return value if value is not None else default
def get_paperless_config():
"""
Get all Paperless-related configuration values
Returns:
dict: Dictionary containing all Paperless configuration
"""
return {
'api_url': get_config('paperless_api_url', 'http://192.168.178.167:30070'),
'api_token': get_config('paperless_api_token', ''),
'destinataere_tag': get_config('paperless_destinataere_tag', 'Stiftung_Destinatäre'),
'destinataere_tag_id': get_config('paperless_destinataere_tag_id', '210'),
'land_tag': get_config('paperless_land_tag', 'Stiftung_Land_und_Pächter'),
'land_tag_id': get_config('paperless_land_tag_id', '204'),
'admin_tag': get_config('paperless_admin_tag', 'Stiftung_Administration'),
'admin_tag_id': get_config('paperless_admin_tag_id', '216'),
}
def set_config(key, value, **kwargs):
"""
Set a configuration value
Args:
key: The configuration key
value: The value to set
**kwargs: Additional parameters for AppConfiguration.set_setting
Returns:
AppConfiguration: The configuration object
"""
return AppConfiguration.set_setting(key, value, **kwargs)
def is_paperless_configured():
"""
Check if Paperless is properly configured
Returns:
bool: True if API URL and token are configured
"""
config = get_paperless_config()
return bool(config['api_url'] and config['api_token'])

View File

@@ -0,0 +1,34 @@
from __future__ import annotations
from datetime import date as _date
from typing import Optional, Union
try:
from django.utils.dateparse import parse_date as _parse_date
except Exception: # pragma: no cover - django not loaded in some tools
_parse_date = None # type: ignore
DateLike = Union[_date, str, None]
def ensure_date(value: DateLike) -> Optional[_date]:
"""Return a date from a date or ISO string; None stays None.
- Accepts datetime.date, 'YYYY-MM-DD' string, or None.
- Returns None if parsing fails or value falsy.
"""
if not value:
return None
if isinstance(value, _date):
return value
if isinstance(value, str):
if _parse_date is None:
return None
return _parse_date(value)
return None
def get_year_from_date(value: DateLike) -> Optional[int]:
"""Extract year from date or ISO string, else None."""
d = ensure_date(value)
return d.year if d else None

View File

@@ -0,0 +1,420 @@
"""
PDF generation utilities with corporate identity support
"""
import os
import base64
from io import BytesIO
from django.conf import settings
from django.template.loader import render_to_string
from django.http import HttpResponse
from django.utils import timezone
# Try to import WeasyPrint, fall back gracefully if not available
try:
from weasyprint import HTML, CSS
from weasyprint.text.fonts import FontConfiguration
WEASYPRINT_AVAILABLE = True
IMPORT_ERROR = None
except ImportError as e:
# WeasyPrint dependencies not available
HTML = None
CSS = None
FontConfiguration = None
WEASYPRINT_AVAILABLE = False
IMPORT_ERROR = str(e)
except OSError as e:
# System dependencies missing (like pango)
HTML = None
CSS = None
FontConfiguration = None
WEASYPRINT_AVAILABLE = False
IMPORT_ERROR = str(e)
from stiftung.models import AppConfiguration
class PDFGenerator:
"""Corporate identity PDF generator"""
def __init__(self):
if WEASYPRINT_AVAILABLE:
self.font_config = FontConfiguration()
else:
self.font_config = None
def is_available(self):
"""Check if PDF generation is available"""
return WEASYPRINT_AVAILABLE
def get_corporate_settings(self):
"""Get corporate identity settings from configuration"""
return {
'stiftung_name': AppConfiguration.get_setting('corporate_stiftung_name', 'Stiftung'),
'logo_path': AppConfiguration.get_setting('corporate_logo_path', ''),
'primary_color': AppConfiguration.get_setting('corporate_primary_color', '#2c3e50'),
'secondary_color': AppConfiguration.get_setting('corporate_secondary_color', '#3498db'),
'address_line1': AppConfiguration.get_setting('corporate_address_line1', ''),
'address_line2': AppConfiguration.get_setting('corporate_address_line2', ''),
'phone': AppConfiguration.get_setting('corporate_phone', ''),
'email': AppConfiguration.get_setting('corporate_email', ''),
'website': AppConfiguration.get_setting('corporate_website', ''),
'footer_text': AppConfiguration.get_setting('corporate_footer_text', 'Dieser Bericht wurde automatisch generiert.'),
}
def get_logo_base64(self, logo_path):
"""Convert logo to base64 for embedding in PDF"""
if not logo_path:
return None
# Try different possible paths
possible_paths = [
logo_path,
os.path.join(settings.MEDIA_ROOT, logo_path),
os.path.join(settings.STATIC_ROOT or '', logo_path),
os.path.join(settings.BASE_DIR, 'static', logo_path),
]
for path in possible_paths:
if os.path.exists(path):
try:
with open(path, 'rb') as img_file:
img_data = base64.b64encode(img_file.read()).decode('utf-8')
# Determine MIME type
ext = os.path.splitext(path)[1].lower()
if ext in ['.jpg', '.jpeg']:
mime_type = 'image/jpeg'
elif ext == '.png':
mime_type = 'image/png'
elif ext == '.svg':
mime_type = 'image/svg+xml'
else:
mime_type = 'image/png' # default
return f"data:{mime_type};base64,{img_data}"
except Exception:
continue
return None
def get_base_css(self, corporate_settings):
"""Generate base CSS for corporate identity"""
primary_color = corporate_settings.get('primary_color', '#2c3e50')
secondary_color = corporate_settings.get('secondary_color', '#3498db')
return f"""
@page {{
size: A4;
margin: 2cm 1.5cm 2cm 1.5cm;
@bottom-center {{
content: "Seite " counter(page) " von " counter(pages);
font-size: 10pt;
color: #666;
}}
}}
body {{
font-family: 'Segoe UI', 'DejaVu Sans', Arial, sans-serif;
font-size: 10pt;
line-height: 1.4;
color: #333;
margin: 0;
padding: 0;
}}
.header {{
border-bottom: 2px solid {primary_color};
padding-bottom: 15px;
margin-bottom: 25px;
page-break-inside: avoid;
}}
.header-content {{
display: flex;
justify-content: space-between;
align-items: flex-start;
}}
.header-left {{
flex: 1;
}}
.header-right {{
text-align: right;
flex-shrink: 0;
margin-left: 20px;
}}
.logo {{
max-height: 60px;
max-width: 150px;
margin-bottom: 10px;
}}
.stiftung-name {{
font-size: 20pt;
font-weight: bold;
color: {primary_color};
margin: 0;
line-height: 1.2;
}}
.document-title {{
font-size: 16pt;
color: {secondary_color};
margin: 5px 0 0 0;
}}
.header-info {{
font-size: 9pt;
color: #666;
margin-top: 10px;
}}
.contact-info {{
font-size: 8pt;
color: #666;
line-height: 1.3;
}}
h1, h2, h3 {{
color: {primary_color};
page-break-inside: avoid;
page-break-after: avoid;
}}
h1 {{
font-size: 14pt;
margin: 20px 0 15px 0;
border-bottom: 1px solid {secondary_color};
padding-bottom: 5px;
}}
h2 {{
font-size: 12pt;
margin: 15px 0 10px 0;
}}
h3 {{
font-size: 11pt;
margin: 10px 0 8px 0;
}}
table {{
width: 100%;
border-collapse: collapse;
margin: 10px 0;
page-break-inside: avoid;
}}
th, td {{
border: 1px solid #ddd;
padding: 6px 8px;
text-align: left;
vertical-align: top;
}}
th {{
background-color: #f8f9fa;
font-weight: 600;
color: {primary_color};
}}
tr:nth-child(even) {{
background-color: #f9f9f9;
}}
.amount {{
text-align: right;
font-family: 'Courier New', monospace;
font-weight: 500;
}}
.status-badge {{
padding: 2px 6px;
border-radius: 3px;
font-size: 8pt;
font-weight: 500;
}}
.status-beantragt {{ background-color: #fff3cd; color: #856404; }}
.status-genehmigt {{ background-color: #d1ecf1; color: #0c5460; }}
.status-ausgezahlt {{ background-color: #d4edda; color: #155724; }}
.status-abgelehnt {{ background-color: #f8d7da; color: #721c24; }}
.status-storniert {{ background-color: #e2e3e5; color: #383d41; }}
.footer {{
margin-top: 30px;
padding-top: 15px;
border-top: 1px solid #ddd;
font-size: 8pt;
color: #666;
text-align: center;
}}
.stats-grid {{
display: grid;
grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
gap: 15px;
margin: 15px 0;
}}
.stat-card {{
border: 1px solid #ddd;
border-radius: 4px;
padding: 12px;
text-align: center;
background: #f8f9fa;
}}
.stat-value {{
font-size: 14pt;
font-weight: bold;
color: {primary_color};
}}
.stat-label {{
font-size: 8pt;
color: #666;
margin-top: 3px;
}}
.section {{
margin-bottom: 20px;
page-break-inside: avoid;
}}
.no-page-break {{
page-break-inside: avoid;
}}
.page-break-before {{
page-break-before: always;
}}
"""
def generate_pdf_response(self, html_content, filename, css_content=None):
"""Generate PDF response from HTML content"""
if not WEASYPRINT_AVAILABLE:
# Return HTML fallback if WeasyPrint is not available
error_html = f"""
<!DOCTYPE html>
<html>
<head>
<title>PDF Export - {filename}</title>
<style>
body {{ font-family: Arial, sans-serif; padding: 20px; max-width: 1200px; margin: 0 auto; }}
.warning {{ background: #fff3cd; color: #856404; padding: 15px; border: 1px solid #ffeaa7; border-radius: 5px; margin-bottom: 20px; }}
.content {{ border: 1px solid #ddd; padding: 20px; border-radius: 5px; }}
{css_content or ''}
</style>
</head>
<body>
<div class="warning">
<h2>⚠️ PDF Generation Not Available</h2>
<p><strong>WeasyPrint dependencies are missing:</strong> {IMPORT_ERROR}</p>
<p>Showing content as HTML preview instead. You can print this page to PDF using your browser.</p>
</div>
<div class="content">
{html_content}
</div>
</body>
</html>
"""
response = HttpResponse(error_html, content_type='text/html')
response['Content-Disposition'] = f'inline; filename="{filename.replace(".pdf", "_preview.html")}"'
return response
try:
# Create CSS string
if css_content:
css = CSS(string=css_content, font_config=self.font_config)
else:
css = None
# Generate PDF
html_doc = HTML(string=html_content)
pdf_bytes = html_doc.write_pdf(stylesheets=[css] if css else None,
font_config=self.font_config)
# Create response
response = HttpResponse(pdf_bytes, content_type='application/pdf')
response['Content-Disposition'] = f'attachment; filename="{filename}"'
return response
except Exception as e:
# Fallback: return error message as HTML
error_html = f"""
<!DOCTYPE html>
<html>
<head>
<title>PDF Generation Error</title>
<style>
body {{ font-family: Arial, sans-serif; padding: 20px; }}
.error {{ color: #d32f2f; border: 1px solid #d32f2f; padding: 15px; border-radius: 5px; margin-bottom: 20px; }}
.content {{ border: 1px solid #ddd; padding: 20px; border-radius: 5px; }}
</style>
</head>
<body>
<h1>PDF Generation Error</h1>
<div class="error">
<p>An error occurred while generating the PDF:</p>
<p><strong>{str(e)}</strong></p>
<p>Showing content as HTML preview instead.</p>
</div>
<div class="content">
<h2>Original Content</h2>
{html_content}
</div>
</body>
</html>
"""
response = HttpResponse(error_html, content_type='text/html')
response['Content-Disposition'] = f'inline; filename="error_{filename.replace(".pdf", ".html")}"'
return response
def export_data_list_pdf(self, data, fields_config, title, filename_prefix, request_user=None):
"""
Export a list of data as formatted PDF
Args:
data: QuerySet or list of model instances
fields_config: dict with field names as keys and display names as values
title: Document title
filename_prefix: Prefix for the generated filename
request_user: User making the request (for audit purposes)
"""
corporate_settings = self.get_corporate_settings()
logo_base64 = self.get_logo_base64(corporate_settings.get('logo_path', ''))
# Prepare context
context = {
'corporate_settings': corporate_settings,
'logo_base64': logo_base64,
'title': title,
'data': data,
'fields_config': fields_config,
'generation_date': timezone.now(),
'generated_by': (request_user.get_full_name()
if hasattr(request_user, 'get_full_name') and request_user.get_full_name()
else request_user.username
if hasattr(request_user, 'username') and request_user.username
else 'System'),
'total_count': len(data) if hasattr(data, '__len__') else data.count(),
}
# Render HTML
html_content = render_to_string('pdf/data_list.html', context)
# Generate CSS
css_content = self.get_base_css(corporate_settings)
# Generate filename
timestamp = timezone.now().strftime('%Y%m%d_%H%M%S')
filename = f"{filename_prefix}_{timestamp}.pdf"
return self.generate_pdf_response(html_content, filename, css_content)
# Global instance
pdf_generator = PDFGenerator()

File diff suppressed because it is too large Load Diff

View File

@@ -292,10 +292,32 @@
<i class="fas fa-tachometer-alt me-1"></i>Dashboard
</a>
</li>
<li class="nav-item">
<a class="nav-link" href="{% url 'stiftung:destinataer_list' %}">
<li class="nav-item dropdown">
<a class="nav-link dropdown-toggle" href="#" id="destinataereDropdown" role="button" data-bs-toggle="dropdown" aria-expanded="false">
<i class="fas fa-users me-1"></i>Destinatäre
</a>
<ul class="dropdown-menu" aria-labelledby="destinataereDropdown">
<li><a class="dropdown-item" href="{% url 'stiftung:destinataer_list' %}">
<i class="fas fa-list me-2"></i>Alle Destinatäre
</a></li>
<li><a class="dropdown-item" href="{% url 'stiftung:destinataer_create' %}">
<i class="fas fa-plus me-2"></i>Neuer Destinatär
</a></li>
<li><hr class="dropdown-divider"></li>
<li><a class="dropdown-item" href="{% url 'stiftung:foerderung_list' %}">
<i class="fas fa-gift me-2"></i>Alle Förderungen
</a></li>
<li><a class="dropdown-item" href="{% url 'stiftung:foerderung_create' %}">
<i class="fas fa-plus me-2"></i>Neue Förderung
</a></li>
<li><hr class="dropdown-divider"></li>
<li><a class="dropdown-item" href="{% url 'stiftung:unterstuetzungen_all' %}">
<i class="fas fa-hand-holding-usd me-2"></i>Alle Unterstützungen
</a></li>
<li><a class="dropdown-item" href="{% url 'stiftung:unterstuetzung_create' %}">
<i class="fas fa-plus me-2"></i>Neue Unterstützung
</a></li>
</ul>
</li>
<li class="nav-item dropdown">
<a class="nav-link dropdown-toggle" href="#" id="laendereiDropdown" role="button" data-bs-toggle="dropdown" aria-expanded="false">

View File

@@ -0,0 +1,66 @@
<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="utf-8">
<title>{{ title }} - {{ corporate_settings.stiftung_name }}</title>
<style>
{{ css_content|safe }}
</style>
</head>
<body>
<!-- Header Section -->
<div class="header">
<div class="header-content">
<div class="header-left">
{% if logo_base64 %}
<img src="{{ logo_base64 }}" alt="{{ corporate_settings.stiftung_name }} Logo" class="logo">
{% endif %}
<h1 class="stiftung-name">{{ corporate_settings.stiftung_name }}</h1>
<div class="document-title">{{ title }}</div>
<div class="header-info">
Erstellt am: {{ generation_date|date:"d.m.Y H:i" }}
{% if generated_by %} | Erstellt von: {{ generated_by }}{% endif %}
{% if total_count %} | Anzahl Einträge: {{ total_count }}{% endif %}
</div>
</div>
{% if corporate_settings.address_line1 or corporate_settings.phone or corporate_settings.email %}
<div class="header-right">
<div class="contact-info">
{% if corporate_settings.address_line1 %}
{{ corporate_settings.address_line1 }}<br>
{% endif %}
{% if corporate_settings.address_line2 %}
{{ corporate_settings.address_line2 }}<br>
{% endif %}
{% if corporate_settings.phone %}
Tel: {{ corporate_settings.phone }}<br>
{% endif %}
{% if corporate_settings.email %}
E-Mail: {{ corporate_settings.email }}<br>
{% endif %}
{% if corporate_settings.website %}
Web: {{ corporate_settings.website }}
{% endif %}
</div>
</div>
{% endif %}
</div>
</div>
<!-- Content Section -->
<div class="content">
{% block content %}
<!-- Content will be injected here -->
{% endblock %}
</div>
<!-- Footer Section -->
<div class="footer">
{% if corporate_settings.footer_text %}
<p>{{ corporate_settings.footer_text }}</p>
{% endif %}
<p>{{ corporate_settings.stiftung_name }} | Generiert am {{ generation_date|date:"d.m.Y H:i" }}</p>
</div>
</body>
</html>

View File

@@ -0,0 +1,272 @@
{% load pdf_tags %}
<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="utf-8">
<title>{{ title }} - {{ corporate_settings.stiftung_name }}</title>
<style>
@page {
size: A4;
margin: 2cm 1.5cm 2cm 1.5cm;
@bottom-center {
content: "Seite " counter(page) " von " counter(pages);
font-size: 10pt;
color: #666;
}
}
body {
font-family: 'Segoe UI', 'DejaVu Sans', Arial, sans-serif;
font-size: 10pt;
line-height: 1.4;
color: #333;
margin: 0;
padding: 0;
}
.header {
border-bottom: 2px solid {{ corporate_settings.primary_color|default:"#2c3e50" }};
padding-bottom: 15px;
margin-bottom: 25px;
page-break-inside: avoid;
}
.header-content {
display: flex;
justify-content: space-between;
align-items: flex-start;
}
.header-left {
flex: 1;
}
.header-right {
text-align: right;
flex-shrink: 0;
margin-left: 20px;
}
.logo {
max-height: 60px;
max-width: 150px;
margin-bottom: 10px;
}
.stiftung-name {
font-size: 20pt;
font-weight: bold;
color: {{ corporate_settings.primary_color|default:"#2c3e50" }};
margin: 0;
line-height: 1.2;
}
.document-title {
font-size: 16pt;
color: {{ corporate_settings.secondary_color|default:"#3498db" }};
margin: 5px 0 0 0;
}
.header-info {
font-size: 9pt;
color: #666;
margin-top: 10px;
}
.contact-info {
font-size: 8pt;
color: #666;
line-height: 1.3;
}
h1, h2, h3 {
color: {{ corporate_settings.primary_color|default:"#2c3e50" }};
page-break-inside: avoid;
page-break-after: avoid;
}
h1 {
font-size: 14pt;
margin: 20px 0 15px 0;
border-bottom: 1px solid {{ corporate_settings.secondary_color|default:"#3498db" }};
padding-bottom: 5px;
}
h2 {
font-size: 12pt;
margin: 15px 0 10px 0;
}
table {
width: 100%;
border-collapse: collapse;
margin: 10px 0;
page-break-inside: avoid;
font-size: 9pt;
}
th, td {
border: 1px solid #ddd;
padding: 4px 6px;
text-align: left;
vertical-align: top;
}
th {
background-color: #f8f9fa;
font-weight: 600;
color: {{ corporate_settings.primary_color|default:"#2c3e50" }};
font-size: 8pt;
}
tr:nth-child(even) {
background-color: #f9f9f9;
}
.amount {
text-align: right;
font-family: 'Courier New', monospace;
font-weight: 500;
}
.status-badge {
padding: 2px 4px;
border-radius: 3px;
font-size: 7pt;
font-weight: 500;
}
.status-beantragt { background-color: #fff3cd; color: #856404; }
.status-genehmigt { background-color: #d1ecf1; color: #0c5460; }
.status-ausgezahlt { background-color: #d4edda; color: #155724; }
.status-abgelehnt { background-color: #f8d7da; color: #721c24; }
.status-storniert { background-color: #e2e3e5; color: #383d41; }
.footer {
margin-top: 30px;
padding-top: 15px;
border-top: 1px solid #ddd;
font-size: 8pt;
color: #666;
text-align: center;
}
.stats-summary {
background-color: #f8f9fa;
border: 1px solid #ddd;
border-radius: 4px;
padding: 10px;
margin: 15px 0;
}
.section {
margin-bottom: 20px;
page-break-inside: avoid;
}
</style>
</head>
<body>
<!-- Header Section -->
<div class="header">
<div class="header-content">
<div class="header-left">
{% if logo_base64 %}
<img src="{{ logo_base64 }}" alt="{{ corporate_settings.stiftung_name }} Logo" class="logo">
{% endif %}
<h1 class="stiftung-name">{{ corporate_settings.stiftung_name }}</h1>
<div class="document-title">{{ title }}</div>
<div class="header-info">
Erstellt am: {{ generation_date|date:"d.m.Y H:i" }}
{% if generated_by %} | Erstellt von: {{ generated_by }}{% endif %}
{% if total_count %} | Anzahl Einträge: {{ total_count }}{% endif %}
</div>
</div>
{% if corporate_settings.address_line1 or corporate_settings.phone or corporate_settings.email %}
<div class="header-right">
<div class="contact-info">
{% if corporate_settings.address_line1 %}
{{ corporate_settings.address_line1 }}<br>
{% endif %}
{% if corporate_settings.address_line2 %}
{{ corporate_settings.address_line2 }}<br>
{% endif %}
{% if corporate_settings.phone %}
Tel: {{ corporate_settings.phone }}<br>
{% endif %}
{% if corporate_settings.email %}
E-Mail: {{ corporate_settings.email }}<br>
{% endif %}
{% if corporate_settings.website %}
Web: {{ corporate_settings.website }}
{% endif %}
</div>
</div>
{% endif %}
</div>
</div>
<!-- Summary Section -->
{% if total_count %}
<div class="stats-summary">
<strong>Zusammenfassung:</strong> {{ total_count }} Eintrag{{ total_count|pluralize:"e" }} exportiert
</div>
{% endif %}
<!-- Data Table -->
{% if data %}
<div class="section">
<table>
<thead>
<tr>
{% for field_key, field_display in fields_config.items %}
<th>{{ field_display }}</th>
{% endfor %}
</tr>
</thead>
<tbody>
{% for item in data %}
<tr>
{% for field_key, field_display in fields_config.items %}
<td>
{% if field_key == 'betrag' or field_key == 'pachtzins' or 'betrag' in field_key %}
<span class="amount">
{% if item|lookup:field_key %}€{{ item|lookup:field_key|floatformat:2 }}{% else %}-{% endif %}
</span>
{% elif field_key == 'status' %}
{% with status_value=item|lookup:field_key %}
{% if status_value %}
<span class="status-badge status-{{ status_value }}">{{ status_value|capfirst }}</span>
{% else %}-{% endif %}
{% endwith %}
{% elif field_key|slice:"-5:" == '_date' or field_key|slice:"-6:" == '_datum' or 'datum' in field_key or 'date' in field_key %}
{% with date_value=item|lookup:field_key %}
{% if date_value %}{{ date_value|date:"d.m.Y" }}{% else %}-{% endif %}
{% endwith %}
{% else %}
{% with field_value=item|lookup:field_key %}
{{ field_value|default:"-"|truncatechars:50 }}
{% endwith %}
{% endif %}
</td>
{% endfor %}
</tr>
{% endfor %}
</tbody>
</table>
</div>
{% else %}
<div class="section">
<p><em>Keine Daten zum Anzeigen verfügbar.</em></p>
</div>
{% endif %}
<!-- Footer Section -->
<div class="footer">
{% if corporate_settings.footer_text %}
<p>{{ corporate_settings.footer_text }}</p>
{% endif %}
<p>{{ corporate_settings.stiftung_name }} | Generiert am {{ generation_date|date:"d.m.Y H:i" }}</p>
</div>
</body>
</html>

View File

@@ -211,6 +211,12 @@
</div>
<div class="card-body">
<div class="row">
<div class="col-md-3 mb-3">
<a href="{% url 'stiftung:app_settings' %}" class="btn btn-outline-primary w-100">
<i class="fas fa-cogs d-block mb-2 fa-2x"></i>
<span>App Settings</span>
</a>
</div>
<div class="col-md-3 mb-3">
<a href="{% url 'stiftung:audit_log_list' %}" class="btn btn-outline-primary w-100">
<i class="fas fa-history d-block mb-2 fa-2x"></i>

View File

@@ -0,0 +1,148 @@
{% extends 'base.html' %}
{% load static %}
{% block title %}{{ title }} - Stiftung{% endblock %}
{% block content %}
<div class="container-fluid">
<div class="row">
<div class="col-12">
<div class="card">
<div class="card-header d-flex justify-content-between align-items-center">
<h3 class="card-title">
<i class="fas fa-cogs"></i>
{{ title }}
</h3>
<a href="{% url 'stiftung:administration' %}" class="btn btn-secondary">
<i class="fas fa-arrow-left"></i>
Back to Administration
</a>
</div>
<div class="card-body">
<form method="post" class="settings-form">
{% csrf_token %}
{% for category_name, settings in categories.items %}
<div class="card mb-4">
<div class="card-header">
<h4 class="mb-0">
<i class="fas fa-{% if category_name == 'Paperless Integration' %}file-alt{% elif category_name == 'System' %}cog{% elif category_name == 'Database' %}database{% else %}folder{% endif %}"></i>
{{ category_name }}
</h4>
</div>
<div class="card-body">
<div class="row">
{% for setting in settings %}
<div class="col-md-6 mb-3">
<div class="form-group">
<label for="setting_{{ setting.key }}" class="form-label">
<strong>{{ setting.display_name }}</strong>
{% if setting.is_system %}
<span class="badge badge-secondary ml-1">System</span>
{% endif %}
</label>
{% if setting.description %}
<small class="form-text text-muted">{{ setting.description }}</small>
{% endif %}
{% if setting.setting_type == 'boolean' %}
<div class="form-check">
<input type="checkbox"
class="form-check-input"
id="setting_{{ setting.key }}"
name="setting_{{ setting.key }}"
value="True"
{% if setting.get_typed_value %}checked{% endif %}
{% if setting.is_system %}disabled{% endif %}>
<label class="form-check-label" for="setting_{{ setting.key }}">
Enabled
</label>
</div>
{% if not setting.get_typed_value %}
<input type="hidden" name="setting_{{ setting.key }}" value="False">
{% endif %}
{% elif setting.setting_type == 'integer' %}
<input type="number"
class="form-control"
id="setting_{{ setting.key }}"
name="setting_{{ setting.key }}"
value="{{ setting.get_typed_value }}"
{% if setting.is_system %}readonly{% endif %}>
{% elif setting.setting_type == 'text' or setting.setting_type == 'url' %}
<input type="{% if setting.setting_type == 'url' %}url{% else %}text{% endif %}"
class="form-control"
id="setting_{{ setting.key }}"
name="setting_{{ setting.key }}"
value="{{ setting.value }}"
{% if setting.is_system %}readonly{% endif %}>
{% else %}
<textarea class="form-control"
id="setting_{{ setting.key }}"
name="setting_{{ setting.key }}"
rows="3"
{% if setting.is_system %}readonly{% endif %}>{{ setting.value }}</textarea>
{% endif %}
</div>
</div>
{% endfor %}
</div>
</div>
</div>
{% empty %}
<div class="alert alert-info">
<i class="fas fa-info-circle"></i>
No configuration settings are available.
<a href="{% url 'stiftung:administration' %}" class="alert-link">Initialize settings first</a>.
</div>
{% endfor %}
{% if categories %}
<div class="text-right mt-4">
<button type="button" class="btn btn-secondary mr-2" onclick="window.history.back()">
<i class="fas fa-times"></i>
Cancel
</button>
<button type="submit" class="btn btn-primary">
<i class="fas fa-save"></i>
Save Settings
</button>
</div>
{% endif %}
</form>
</div>
</div>
</div>
</div>
</div>
<style>
.settings-form .card {
border-left: 4px solid #007bff;
}
.settings-form .card-header {
background-color: #f8f9fa;
border-bottom: 1px solid #dee2e6;
}
.settings-form .form-group label {
font-weight: 500;
}
.settings-form .badge {
font-size: 0.7em;
}
.settings-form input[readonly],
.settings-form textarea[readonly] {
background-color: #f8f9fa;
opacity: 0.8;
}
.settings-form .form-check {
padding-top: 0.375rem;
}
</style>
{% endblock %}

View File

@@ -15,30 +15,6 @@
<!-- Statistics Cards -->
<div class="row mb-4">
<!-- Person Statistics -->
<div class="col-xl-3 col-md-6 mb-4">
<div class="card border-left-primary shadow h-100 py-2">
<div class="card-body">
<div class="row no-gutters align-items-center">
<div class="col mr-2">
<div class="text-xs font-weight-bold text-primary text-uppercase mb-1">
Personen
</div>
<div class="h5 mb-0 font-weight-bold text-gray-800">
{{ total_persons }}
</div>
<div class="text-xs text-muted">
{{ active_persons }} aktiv
</div>
</div>
<div class="col-auto">
<i class="fas fa-users fa-2x text-gray-300"></i>
</div>
</div>
</div>
</div>
</div>
<!-- Land Statistics -->
<div class="col-xl-3 col-md-6 mb-4">
<div class="card border-left-success shadow h-100 py-2">
@@ -129,12 +105,7 @@
<span>Neue Förderung</span>
</a>
</div>
<div class="col-md-3 mb-3">
<a href="{% url 'stiftung:verpachtung_create' %}" class="btn btn-outline-success w-100 h-100 d-flex flex-column align-items-center justify-content-center p-3">
<i class="fas fa-handshake fa-2x mb-2"></i>
<span>Neue Verpachtung</span>
</a>
</div>
<!-- Removed generic "Neue Verpachtung" - now created via specific Land pages -->
<div class="col-md-3 mb-3">
<a href="{% url 'stiftung:bericht_list' %}" class="btn btn-outline-info w-100 h-100 d-flex flex-column align-items-center justify-content-center p-3">
<i class="fas fa-chart-bar fa-2x mb-2"></i>
@@ -364,15 +335,15 @@
€{{ verpachtung.pachtzins_jaehrlich|floatformat:0 }}/Jahr
</small>
</div>
<a href="{% url 'stiftung:verpachtung_detail' verpachtung.pk %}" class="btn btn-sm btn-outline-primary">
<a href="{% url 'stiftung:land_verpachtung_detail' verpachtung.pk %}" class="btn btn-sm btn-outline-primary">
<i class="fas fa-eye"></i>
</a>
</div>
{% endfor %}
</div>
<div class="text-center mt-3">
<a href="{% url 'stiftung:verpachtung_list' %}" class="btn btn-sm btn-primary">
Alle Verpachtungen anzeigen
<a href="{% url 'stiftung:land_list' %}" class="btn btn-sm btn-primary">
Ländereien verwalten
</a>
</div>
{% else %}

View File

@@ -14,6 +14,9 @@
Destinatär: {{ destinataer.get_full_name }}
</h1>
<div>
<a href="{% url 'stiftung:foerderung_create' %}?destinataer={{ destinataer.pk }}" class="btn btn-success me-2">
<i class="fas fa-gift me-2"></i>Neue Förderung
</a>
<a href="{% url 'stiftung:destinataer_update' pk=destinataer.pk %}" class="btn btn-warning me-2">
<i class="fas fa-edit me-2"></i>Bearbeiten
</a>
@@ -263,6 +266,64 @@
</div>
{% endif %}
<!-- Unterstützungen -->
{% if unterstuetzungen %}
<div class="card shadow mb-4">
<div class="card-header bg-info text-white">
<div class="d-flex justify-content-between align-items-center">
<h5 class="card-title mb-0">
<i class="fas fa-hand-holding-usd me-2"></i>Unterstützungen ({{ unterstuetzungen.count }})
</h5>
<a href="{% url 'stiftung:unterstuetzung_create' %}?destinataer={{ destinataer.pk }}"
class="btn btn-light btn-sm">
<i class="fas fa-plus me-1"></i>Neue Unterstützung
</a>
</div>
</div>
<div class="card-body">
<div class="table-responsive">
<table class="table table-hover">
<thead class="table-light">
<tr>
<th>Fällig am</th>
<th>Betrag</th>
<th>Status</th>
<th>Beschreibung</th>
<th>Aktionen</th>
</tr>
</thead>
<tbody>
{% for unterstuetzung in unterstuetzungen %}
<tr>
<td>{{ unterstuetzung.faellig_am|date:"d.m.Y" }}</td>
<td><span class="text-success fw-bold">€{{ unterstuetzung.betrag|floatformat:2 }}</span></td>
<td>
{% if unterstuetzung.status == 'ausgezahlt' %}
<span class="badge bg-success">Ausgezahlt</span>
{% elif unterstuetzung.status == 'in_bearbeitung' %}
<span class="badge bg-warning">In Bearbeitung</span>
{% elif unterstuetzung.status == 'geplant' %}
<span class="badge bg-secondary">Geplant</span>
{% else %}
<span class="badge bg-danger">{{ unterstuetzung.get_status_display }}</span>
{% endif %}
</td>
<td>{{ unterstuetzung.beschreibung|truncatechars:40 }}</td>
<td>
<a href="{% url 'stiftung:unterstuetzung_detail' pk=unterstuetzung.pk %}"
class="btn btn-sm btn-outline-primary">
<i class="fas fa-eye"></i>
</a>
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
</div>
{% endif %}
<!-- Verknüpfte Dokumente -->
<div class="card shadow mb-4">
<div class="card-header bg-success text-white d-flex justify-content-between align-items-center">

View File

@@ -1,5 +1,6 @@
{% extends 'base.html' %}
{% load static %}
{% load help_tags %}
{% block title %}{{ title }} - Stiftungsverwaltung{% endblock %}
@@ -420,22 +421,9 @@
</div>
</div>
<!-- Sidebar -->
<!-- Sidebar mit Hilfe -->
<div class="col-lg-4">
<div class="card shadow">
<div class="card-header bg-info text-white">
<h6 class="card-title mb-0">
<i class="fas fa-info-circle me-2"></i>Hilfe
</h6>
</div>
<div class="card-body">
<div class="small">
<p><strong>Voraussetzungen für Unterstützung</strong></p>
<p>Die Stiftung kann grundsätzlich laufende Leistungen nur an Abkömmlinge der Geschwister des Stifters Hendrik van Hees und seiner Ehefrau oder im Einzelfall an weitere Personen erbringen, die als Alleinstehende(r) oder Haushaltsvorstand keine höheren Bezüge als 2.245,00 € (5× Regelsatz 563,00 €) haben und deren Vermögen 15.500 € nicht übersteigt (§53 AO). Die Sätze erhöhen sich bei weiteren Haushaltsangehörigen.</p>
<p class="text-muted">Dieser Text ist redaktionell anpassbar (Template).</p>
</div>
</div>
</div>
{% help_box 'destinataer_new' user %}
</div>
</div>
</div>

View File

@@ -78,6 +78,8 @@
<option value="land">Ländereien</option>
<option value="paechter">Pächter</option>
<option value="verpachtung">Verpachtungen</option>
<option value="foerderung">Förderungen</option>
<option value="abrechnung">Abrechnungen</option>
<option value="rentmeister">Rentmeister</option>
</select>
</div>
@@ -253,7 +255,11 @@ function renderDocuments() {
} else if (link.link_type === 'paechter') {
detailUrl = `/paechter/${obj.id}/`;
} else if (link.link_type === 'verpachtung') {
detailUrl = `/verpachtungen/${obj.id}/`;
detailUrl = `/laendereien/verpachtungen/${obj.id}/`;
} else if (link.link_type === 'foerderung') {
detailUrl = `/foerderungen/${obj.id}/`;
} else if (link.link_type === 'abrechnung') {
detailUrl = `/laendereien/abrechnungen/${obj.id}/`;
} else if (link.link_type === 'rentmeister') {
detailUrl = `/geschaeftsfuehrung/rentmeister/${obj.id}/`;
}

View File

@@ -19,7 +19,16 @@
<i class="fas fa-exclamation-triangle me-2"></i>Warnung!
</h5>
<p class="mb-0">
Sind Sie sicher, dass Sie die Förderung für <strong>{{ foerderung.person.get_full_name }}</strong>
Sind Sie sicher, dass Sie die Förderung für
<strong>
{% if foerderung.destinataer %}
{{ foerderung.destinataer.get_full_name }}
{% elif foerderung.person %}
{{ foerderung.person.get_full_name }} (Legacy)
{% else %}
Unbekannter Empfänger
{% endif %}
</strong>
({{ foerderung.jahr }}, €{{ foerderung.betrag|floatformat:2 }}) löschen möchten?
</p>
</div>

View File

@@ -26,9 +26,13 @@
<div class="col-md-6">
<h6 class="text-primary">Person</h6>
<p class="mb-3">
{% if foerderung.destinataer %}
<a href="{% url 'stiftung:destinataer_detail' foerderung.destinataer.pk %}">
{{ foerderung.person.get_full_name }}
{{ foerderung.destinataer.get_full_name }}
</a>
{% else %}
<em class="text-muted">Keine Person zugeordnet</em>
{% endif %}
</p>
<h6 class="text-primary">Jahr</h6>

View File

@@ -1,12 +1,13 @@
{% extends 'base.html' %}
{% load static %}
{% load help_tags %}
{% block title %}{{ title }} - Stiftungsverwaltung{% endblock %}
{% block content %}
<div class="row">
<div class="col-lg-8 mx-auto">
<div class="col-lg-8">
<div class="card shadow">
<div class="card-header">
<h4 class="mb-0">
@@ -19,13 +20,13 @@
<div class="row">
<div class="col-md-6 mb-3">
<label for="{{ form.person.id_for_label }}" class="form-label">
{{ form.person.label }} *
<label for="{{ form.destinataer.id_for_label }}" class="form-label">
{{ form.destinataer.label }} *
</label>
{{ form.person }}
{% if form.person.errors %}
{{ form.destinataer }}
{% if form.destinataer.errors %}
<div class="invalid-feedback d-block">
{{ form.person.errors.0 }}
{{ form.destinataer.errors.0 }}
</div>
{% endif %}
</div>
@@ -121,6 +122,9 @@
{{ form.verwendungsnachweis.errors.0 }}
</div>
{% endif %}
<div class="form-text">
Optionale Verknüpfung zu einem Dokument aus dem Paperless-System
</div>
</div>
</div>
@@ -148,6 +152,11 @@
</div>
</div>
</div>
<!-- Sidebar mit Hilfe -->
<div class="col-lg-4">
{% help_box 'foerderung_new' user %}
</div>
</div>
{% endblock %}

View File

@@ -9,11 +9,173 @@
<h1 class="h3">
<i class="fas fa-gift text-primary me-2"></i>Förderungen
</h1>
<a href="{% url 'stiftung:foerderung_create' %}" class="btn btn-primary">
<i class="fas fa-plus me-2"></i> Neue Förderung
</a>
<div>
<button type="button" class="btn btn-success me-2" data-bs-toggle="modal" data-bs-target="#exportModal">
<i class="fas fa-download me-2"></i> CSV/PDF Export
</button>
<a href="{% url 'stiftung:foerderung_create' %}" class="btn btn-primary">
<i class="fas fa-plus me-2"></i> Neue Förderung
</a>
</div>
</div>
<!-- Export Modal -->
<div class="modal fade" id="exportModal" tabindex="-1">
<div class="modal-dialog modal-lg">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">Export Optionen</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<form id="exportForm" method="post" action="{% url 'stiftung:foerderung_list' %}">
{% csrf_token %}
<div class="modal-body">
<div class="row">
<div class="col-md-6">
<h6 class="fw-bold">Export Format</h6>
<div class="mb-3">
<div class="form-check">
<input class="form-check-input" type="radio" name="format" id="format_csv" value="csv" checked>
<label class="form-check-label" for="format_csv">
<i class="fas fa-file-csv me-2"></i>CSV (Excel-kompatibel)
</label>
</div>
<div class="form-check">
<input class="form-check-input" type="radio" name="format" id="format_pdf" value="pdf">
<label class="form-check-label" for="format_pdf">
<i class="fas fa-file-pdf me-2"></i>PDF (Tabelle)
</label>
</div>
</div>
<h6 class="fw-bold">Export Umfang</h6>
<div class="mb-3">
<div class="form-check">
<input class="form-check-input" type="radio" name="scope" id="scope_all" value="all" checked>
<label class="form-check-label" for="scope_all">
Alle Einträge (<span id="total-count">{{ foerderungen.count }}</span>)
</label>
</div>
<div class="form-check">
<input class="form-check-input" type="radio" name="scope" id="scope_selected" value="selected">
<label class="form-check-label" for="scope_selected">
Nur ausgewählte Einträge (<span id="selected-count">0</span>)
</label>
</div>
</div>
</div>
<div class="col-md-6" id="field-selection" style="max-height: 400px; overflow-y: auto;">
<h6 class="fw-bold">Felder auswählen</h6>
<div class="mb-2">
<button type="button" class="btn btn-sm btn-outline-primary me-2" onclick="selectAllFields()">Alle auswählen</button>
<button type="button" class="btn btn-sm btn-outline-secondary" onclick="selectDefaultFields()">Standard</button>
</div>
<div class="field-groups">
<!-- Kernfelder -->
<div class="mb-3">
<h6 class="text-muted mb-2">Kernfelder</h6>
<div class="form-check">
<input class="form-check-input" type="checkbox" name="fields" value="destinataer_name" id="field_destinataer_name" checked>
<label class="form-check-label" for="field_destinataer_name">Destinatär Name</label>
</div>
<div class="form-check">
<input class="form-check-input" type="checkbox" name="fields" value="jahr" id="field_jahr" checked>
<label class="form-check-label" for="field_jahr">Jahr</label>
</div>
<div class="form-check">
<input class="form-check-input" type="checkbox" name="fields" value="betrag" id="field_betrag" checked>
<label class="form-check-label" for="field_betrag">Betrag (€)</label>
</div>
<div class="form-check">
<input class="form-check-input" type="checkbox" name="fields" value="kategorie" id="field_kategorie" checked>
<label class="form-check-label" for="field_kategorie">Kategorie</label>
</div>
<div class="form-check">
<input class="form-check-input" type="checkbox" name="fields" value="status" id="field_status" checked>
<label class="form-check-label" for="field_status">Status</label>
</div>
<div class="form-check">
<input class="form-check-input" type="checkbox" name="fields" value="antragsdatum" id="field_antragsdatum" checked>
<label class="form-check-label" for="field_antragsdatum">Antragsdatum</label>
</div>
</div>
<!-- Zeitfelder -->
<div class="mb-3">
<h6 class="text-muted mb-2">Datumsfelder</h6>
<div class="form-check">
<input class="form-check-input" type="checkbox" name="fields" value="bewilligungsdatum" id="field_bewilligungsdatum">
<label class="form-check-label" for="field_bewilligungsdatum">Bewilligungsdatum</label>
</div>
<div class="form-check">
<input class="form-check-input" type="checkbox" name="fields" value="auszahlungsdatum" id="field_auszahlungsdatum">
<label class="form-check-label" for="field_auszahlungsdatum">Auszahlungsdatum</label>
</div>
<div class="form-check">
<input class="form-check-input" type="checkbox" name="fields" value="erstellt_am" id="field_erstellt_am">
<label class="form-check-label" for="field_erstellt_am">Erstellt am</label>
</div>
</div>
<!-- Beschreibungsfelder -->
<div class="mb-3">
<h6 class="text-muted mb-2">Details</h6>
<div class="form-check">
<input class="form-check-input" type="checkbox" name="fields" value="beschreibung" id="field_beschreibung" checked>
<label class="form-check-label" for="field_beschreibung">Beschreibung</label>
</div>
<div class="form-check">
<input class="form-check-input" type="checkbox" name="fields" value="begruendung" id="field_begruendung">
<label class="form-check-label" for="field_begruendung">Begründung</label>
</div>
<div class="form-check">
<input class="form-check-input" type="checkbox" name="fields" value="verwendungsnachweis_status" id="field_verwendungsnachweis_status">
<label class="form-check-label" for="field_verwendungsnachweis_status">Verwendungsnachweis Status</label>
</div>
</div>
<!-- Destinataer Details -->
<div class="mb-3">
<h6 class="text-muted mb-2">Destinatär Details</h6>
<div class="form-check">
<input class="form-check-input" type="checkbox" name="fields" value="familienzweig" id="field_familienzweig">
<label class="form-check-label" for="field_familienzweig">Familienzweig</label>
</div>
<div class="form-check">
<input class="form-check-input" type="checkbox" name="fields" value="email" id="field_email">
<label class="form-check-label" for="field_email">E-Mail</label>
</div>
<div class="form-check">
<input class="form-check-input" type="checkbox" name="fields" value="telefon" id="field_telefon">
<label class="form-check-label" for="field_telefon">Telefon</label>
</div>
<div class="form-check">
<input class="form-check-input" type="checkbox" name="fields" value="adresse" id="field_adresse">
<label class="form-check-label" for="field_adresse">Adresse</label>
</div>
<div class="form-check">
<input class="form-check-input" type="checkbox" name="fields" value="berufsgruppe" id="field_berufsgruppe">
<label class="form-check-label" for="field_berufsgruppe">Berufsgruppe</label>
</div>
</div>
</div>
</div>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Abbrechen</button>
<button type="submit" class="btn btn-primary">
<i class="fas fa-download me-2"></i>Export starten
</button>
</div>
</form>
</div>
</div>
</div>
<!-- Statistics Cards -->
<div class="row mb-4">
<div class="col-md-3">
@@ -107,6 +269,9 @@
<table class="table table-hover">
<thead>
<tr>
<th width="50">
<input type="checkbox" class="form-check-input" id="selectAll" onchange="toggleAllCheckboxes(this)">
</th>
<th>Person</th>
<th>Jahr</th>
<th>Betrag</th>
@@ -120,9 +285,16 @@
{% for foerderung in page_obj %}
<tr>
<td>
<input type="checkbox" class="form-check-input entry-checkbox" name="entry" value="{{ foerderung.pk }}" onchange="updateSelectedCount()">
</td>
<td>
{% if foerderung.destinataer and foerderung.destinataer.pk %}
<a href="{% url 'stiftung:destinataer_detail' foerderung.destinataer.pk %}">
{{ foerderung.person.get_full_name }}
{{ foerderung.destinataer.get_full_name }}
</a>
{% else %}
<em class="text-muted">Keine Person zugeordnet</em>
{% endif %}
</td>
<td>{{ foerderung.jahr }}</td>
<td>€{{ foerderung.betrag|floatformat:2 }}</td>
@@ -200,4 +372,80 @@
</div>
</div>
<script>
let selectedEntries = [];
function toggleAllCheckboxes(source) {
checkboxes = document.getElementsByName('entry');
for(let i=0, n=checkboxes.length;i<n;i++) {
checkboxes[i].checked = source.checked;
}
updateSelectedCount();
}
function updateSelectedCount() {
selectedEntries = [];
const checkboxes = document.getElementsByName('entry');
for(let i=0, n=checkboxes.length;i<n;i++) {
if(checkboxes[i].checked) {
selectedEntries.push(checkboxes[i].value);
}
}
const count = selectedEntries.length;
document.getElementById('selectedCount').textContent = count;
document.getElementById('exportButton').disabled = count === 0;
// Update text in buttons
document.getElementById('exportText').textContent =
count === 0 ? 'Export' : `${count} Einträge exportieren`;
}
function selectAllFields() {
document.querySelectorAll('#exportModal input[type="checkbox"]').forEach(cb => cb.checked = true);
}
function selectDefaultFields() {
// First uncheck all
document.querySelectorAll('#exportModal input[type="checkbox"]').forEach(cb => cb.checked = false);
// Then check default fields
const defaultFields = [
'destinataer__person__vorname',
'destinataer__person__nachname',
'jahr',
'betrag',
'kategorie',
'status',
'antragsdatum',
'notizen'
];
defaultFields.forEach(field => {
const checkbox = document.querySelector(`#exportModal input[value="${field}"]`);
if(checkbox) checkbox.checked = true;
});
}
function submitExportForm() {
// Set selected entries
document.getElementById('selectedEntries').value = selectedEntries.join(',');
// Collect selected fields
const selectedFields = [];
document.querySelectorAll('#exportModal input[type="checkbox"]:checked').forEach(cb => {
selectedFields.push(cb.value);
});
document.getElementById('selectedFields').value = selectedFields.join(',');
// Submit form
document.getElementById('exportForm').submit();
}
// Initialize on page load
document.addEventListener('DOMContentLoaded', function() {
updateSelectedCount();
});
</script>
{% endblock %}

View File

@@ -0,0 +1,147 @@
{% load static %}
{% if help_obj %}
<div class="card border-info mb-3" style="max-width: 350px;">
<div class="card-header bg-info text-white d-flex justify-content-between align-items-center">
<h6 class="mb-0">
<i class="fas fa-info-circle me-2"></i>{{ help_obj.title }}
</h6>
{% if can_edit %}
<button class="btn btn-sm btn-outline-light" onclick="editHelpBox('{{ page_key }}')" title="Bearbeiten">
<i class="fas fa-edit"></i>
</button>
{% endif %}
</div>
<div class="card-body">
<div class="help-content">
{{ content_html }}
</div>
{% if can_edit %}
<div class="mt-2">
<small class="text-muted">
<i class="fas fa-user me-1"></i>{{ help_obj.updated_by|default:"System" }}
<i class="fas fa-clock ms-2 me-1"></i>{{ help_obj.updated_at|date:"d.m.Y H:i" }}
</small>
</div>
{% endif %}
</div>
</div>
{% if can_edit %}
<!-- Modal für Bearbeitung -->
<div class="modal fade" id="helpBoxModal" tabindex="-1">
<div class="modal-dialog modal-lg">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">Hilfs-Infobox bearbeiten</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<form id="helpBoxForm" method="post" action="{% url 'stiftung:edit_help_box' %}">
<div class="modal-body">
{% csrf_token %}
<input type="hidden" name="page_key" value="{{ page_key }}">
<div class="mb-3">
<label for="title" class="form-label">Titel</label>
<input type="text" class="form-control" id="title" name="title"
value="{{ help_obj.title }}" required>
</div>
<div class="mb-3">
<label for="content" class="form-label">Inhalt</label>
<textarea class="form-control" id="content" name="content" rows="10" required>{{ help_obj.content }}</textarea>
<div class="form-text">
<strong>Markdown Syntax:</strong><br>
**fett** | *kursiv* | `code` | [Link](url)<br>
- Liste | 1. Nummerierte Liste | &gt; Zitat
</div>
</div>
<div class="mb-3 form-check">
<input type="checkbox" class="form-check-input" id="is_active" name="is_active"
{% if help_obj.is_active %}checked{% endif %}>
<label class="form-check-label" for="is_active">
Aktiv (anzeigen)
</label>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Abbrechen</button>
<button type="submit" class="btn btn-primary">Speichern</button>
</div>
</form>
</div>
</div>
</div>
<script>
function editHelpBox(pageKey) {
var modal = new bootstrap.Modal(document.getElementById('helpBoxModal'));
modal.show();
}
</script>
{% endif %}
{% elif can_edit %}
<!-- Keine Hilfsbox vorhanden - Button zum Erstellen -->
<div class="card border-secondary mb-3" style="max-width: 350px;">
<div class="card-body text-center">
<p class="text-muted mb-2">Keine Hilfe verfügbar</p>
<button class="btn btn-outline-primary btn-sm" onclick="createHelpBox('{{ page_key }}')">
<i class="fas fa-plus me-1"></i>Hilfe erstellen
</button>
</div>
</div>
<!-- Modal für neue Hilfsbox -->
<div class="modal fade" id="createHelpBoxModal" tabindex="-1">
<div class="modal-dialog modal-lg">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">Neue Hilfs-Infobox erstellen</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<form id="createHelpBoxForm" method="post" action="{% url 'stiftung:edit_help_box' %}">
<div class="modal-body">
{% csrf_token %}
<input type="hidden" name="page_key" value="{{ page_key }}">
<div class="mb-3">
<label for="new_title" class="form-label">Titel</label>
<input type="text" class="form-control" id="new_title" name="title"
placeholder="z.B. Hilfe beim Erstellen" required>
</div>
<div class="mb-3">
<label for="new_content" class="form-label">Inhalt</label>
<textarea class="form-control" id="new_content" name="content" rows="10"
placeholder="Geben Sie hier die Hilfsinformationen ein..." required></textarea>
<div class="form-text">
<strong>Markdown Syntax:</strong><br>
**fett** | *kursiv* | `code` | [Link](url)<br>
- Liste | 1. Nummerierte Liste | &gt; Zitat
</div>
</div>
<div class="mb-3 form-check">
<input type="checkbox" class="form-check-input" id="new_is_active" name="is_active" checked>
<label class="form-check-label" for="new_is_active">
Aktiv (anzeigen)
</label>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Abbrechen</button>
<button type="submit" class="btn btn-primary">Erstellen</button>
</div>
</form>
</div>
</div>
</div>
<script>
function createHelpBox(pageKey) {
var modal = new bootstrap.Modal(document.getElementById('createHelpBoxModal'));
modal.show();
}
</script>
{% endif %}

View File

@@ -0,0 +1,166 @@
{% extends 'base.html' %}
{% load static %}
{% block title %}Hilfs-Infoboxen verwalten - Stiftungsverwaltung{% endblock %}
{% block content %}
<div class="row">
<div class="col-12">
<div class="d-flex justify-content-between align-items-center mb-4">
<h1 class="h3">
<i class="fas fa-info-circle text-primary me-2"></i>
Hilfs-Infoboxen verwalten
</h1>
<div>
<a href="{% url 'admin:stiftung_helpbox_add' %}" class="btn btn-primary">
<i class="fas fa-plus me-2"></i>Neue Hilfsbox erstellen
</a>
</div>
</div>
<div class="row">
<div class="col-lg-8">
<div class="card shadow">
<div class="card-header bg-info text-white">
<h5 class="card-title mb-0">
<i class="fas fa-list me-2"></i>Alle Hilfsboxen
</h5>
</div>
<div class="card-body p-0">
{% if help_boxes %}
<div class="table-responsive">
<table class="table table-hover mb-0">
<thead class="table-light">
<tr>
<th>Seite</th>
<th>Titel</th>
<th>Status</th>
<th>Zuletzt geändert</th>
<th>Geändert von</th>
<th>Aktionen</th>
</tr>
</thead>
<tbody>
{% for help_box in help_boxes %}
<tr>
<td>
<span class="badge bg-secondary">{{ help_box.get_page_key_display }}</span>
</td>
<td>
<strong>{{ help_box.title }}</strong>
<br>
<small class="text-muted">{{ help_box.content|truncatechars:80 }}</small>
</td>
<td>
{% if help_box.is_active %}
<span class="badge bg-success">Aktiv</span>
{% else %}
<span class="badge bg-danger">Inaktiv</span>
{% endif %}
</td>
<td>
{{ help_box.updated_at|date:"d.m.Y H:i" }}
</td>
<td>
{{ help_box.updated_by|default:"-" }}
</td>
<td>
<a href="{% url 'admin:stiftung_helpbox_change' help_box.id %}"
class="btn btn-sm btn-outline-primary me-1"
title="Bearbeiten">
<i class="fas fa-edit"></i>
</a>
<a href="{% url 'admin:stiftung_helpbox_delete' help_box.id %}"
class="btn btn-sm btn-outline-danger"
title="Löschen">
<i class="fas fa-trash"></i>
</a>
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
{% else %}
<div class="text-center py-4">
<i class="fas fa-info-circle fa-3x text-muted mb-3"></i>
<h5 class="text-muted">Keine Hilfsboxen gefunden</h5>
<p class="text-muted">Erstellen Sie Ihre erste Hilfsbox!</p>
<a href="{% url 'admin:stiftung_helpbox_add' %}" class="btn btn-primary">
<i class="fas fa-plus me-2"></i>Hilfsbox erstellen
</a>
</div>
{% endif %}
</div>
</div>
</div>
<div class="col-lg-4">
<div class="card shadow">
<div class="card-header bg-light">
<h6 class="card-title mb-0">
<i class="fas fa-chart-pie me-2"></i>Statistiken
</h6>
</div>
<div class="card-body">
<div class="mb-3">
<div class="d-flex justify-content-between">
<span>Gesamt:</span>
<strong>{{ help_boxes|length }}</strong>
</div>
</div>
<div class="mb-3">
<div class="d-flex justify-content-between">
<span>Aktiv:</span>
<strong class="text-success">{{ active_count }}</strong>
</div>
</div>
<div class="mb-3">
<div class="d-flex justify-content-between">
<span>Inaktiv:</span>
<strong class="text-danger">{{ inactive_count }}</strong>
</div>
</div>
<hr>
<h6 class="text-primary">Verfügbare Seiten:</h6>
<div class="small">
{% for key, display in available_pages %}
{% if key not in existing_pages %}
<div class="mb-1">
<span class="badge bg-light text-dark">{{ display }}</span>
<a href="{% url 'admin:stiftung_helpbox_add' %}?page_key={{ key }}"
class="btn btn-sm btn-outline-primary ms-2">
<i class="fas fa-plus"></i>
</a>
</div>
{% endif %}
{% endfor %}
</div>
</div>
</div>
<div class="card shadow mt-3">
<div class="card-header bg-light">
<h6 class="card-title mb-0">
<i class="fas fa-info me-2"></i>Hilfetipps
</h6>
</div>
<div class="card-body">
<div class="small">
<h6>Markdown Syntax:</h6>
<ul class="list-unstyled">
<li><code>**fett**</code><strong>fett</strong></li>
<li><code>*kursiv*</code><em>kursiv</em></li>
<li><code>`code`</code><code>code</code></li>
<li><code>[Link](url)</code><a href="#">Link</a></li>
<li><code>- Liste</code> → Aufzählung</li>
<li><code>1. Nummer</code> → Nummeriert</li>
</ul>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
{% endblock %}

View File

@@ -97,7 +97,7 @@
<i class="fas fa-handshake fa-3x text-secondary mb-3"></i>
<h5 class="card-title">🤝 Verpachtungsverwaltung</h5>
<p class="card-text">Organisieren Sie Pachtverträge und deren Verwaltung effizient.</p>
<a href="{% url 'stiftung:verpachtung_list' %}" class="btn btn-outline-secondary btn-sm mt-2">
<a href="{% url 'stiftung:land_list' %}" class="btn btn-outline-secondary btn-sm mt-2">
<i class="fas fa-external-link-alt me-1"></i>Öffnen
</a>
</div>
@@ -143,7 +143,7 @@
<a href="{% url 'stiftung:paechter_list' %}" class="btn btn-info btn-lg">
<i class="fas fa-user-tie me-2"></i>Pächter
</a>
<a href="{% url 'stiftung:verpachtung_list' %}" class="btn btn-secondary btn-lg">
<a href="{% url 'stiftung:land_list' %}" class="btn btn-secondary btn-lg">
<i class="fas fa-handshake me-2"></i>Verpachtungen
</a>
<a href="{% url 'stiftung:foerderung_list' %}" class="btn btn-warning btn-lg">

View File

@@ -391,6 +391,7 @@
<th>Fläche</th>
<th>Pachtzins</th>
<th>Status</th>
<th>Aktionen</th>
</tr>
</thead>
<tbody>
@@ -433,6 +434,21 @@
<span class="badge bg-info">{{ verpachtung.get_status_display }}</span>
{% endif %}
</td>
<td>
<div class="btn-group btn-group-sm" role="group">
<a href="{% url 'stiftung:land_verpachtung_detail' verpachtung.pk %}" class="btn btn-outline-primary" title="Details anzeigen">
<i class="fas fa-eye"></i>
</a>
<a href="{% url 'stiftung:land_verpachtung_update' verpachtung.pk %}" class="btn btn-outline-warning" title="Bearbeiten">
<i class="fas fa-edit"></i>
</a>
{% if verpachtung.status == 'aktiv' %}
<a href="{% url 'stiftung:land_verpachtung_end_direct' verpachtung.pk %}" class="btn btn-outline-danger" title="Beenden">
<i class="fas fa-stop"></i>
</a>
{% endif %}
</div>
</td>
</tr>
{% endfor %}
</tbody>
@@ -626,11 +642,18 @@
<i class="fas fa-handshake me-2"></i>Verpachtungs-Management
</h6>
<div class="btn-group">
{% if not land.aktueller_paechter %}
{% if land.neue_verpachtungen.exists %}
<!-- New system: Create new LandVerpachtung -->
<a href="{% url 'stiftung:land_verpachtung_create' land_pk=land.pk %}" class="btn btn-sm btn-success">
<i class="fas fa-plus me-2"></i>Neue Verpachtung
</a>
{% elif not land.aktueller_paechter %}
<!-- Old system: No current tenant -->
<a href="{% url 'stiftung:land_verpachtung_create' land_pk=land.pk %}" class="btn btn-sm btn-success">
<i class="fas fa-plus me-2"></i>Verpachtung erstellen
</a>
{% else %}
<!-- Old system: Has current tenant -->
<a href="{% url 'stiftung:land_verpachtung_edit' land_pk=land.pk %}" class="btn btn-sm btn-warning">
<i class="fas fa-edit me-2"></i>Verpachtung bearbeiten
</a>
@@ -641,7 +664,7 @@
</div>
</div>
<div class="card-body">
<!-- Neue Verpachtungen anzeigen -->
<!-- New LandVerpachtung objects -->
{% if land.neue_verpachtungen.all %}
<div class="table-responsive">
<table class="table table-hover">
@@ -696,13 +719,18 @@
{% endif %}
</td>
<td>
<div class="btn-group" role="group">
<a href="#" class="btn btn-sm btn-outline-primary" title="Bearbeiten">
<div class="btn-group btn-group-sm" role="group">
<a href="{% url 'stiftung:land_verpachtung_detail' verpachtung.pk %}" class="btn btn-outline-primary" title="Details anzeigen">
<i class="fas fa-eye"></i>
</a>
<a href="{% url 'stiftung:land_verpachtung_update' verpachtung.pk %}" class="btn btn-outline-warning" title="Bearbeiten">
<i class="fas fa-edit"></i>
</a>
<a href="#" class="btn btn-sm btn-outline-danger" title="Beenden">
{% if verpachtung.status == 'aktiv' %}
<a href="{% url 'stiftung:land_verpachtung_end_direct' verpachtung.pk %}" class="btn btn-outline-danger" title="Beenden">
<i class="fas fa-stop"></i>
</a>
{% endif %}
</div>
</td>
</tr>
@@ -734,6 +762,58 @@
</div>
</div>
</div>
{% elif land.aktueller_paechter %}
<!-- Old system verpachtung display -->
<div class="alert alert-info">
<i class="fas fa-info-circle me-2"></i>
<strong>Legacy Verpachtung:</strong> Diese Länderei verwendet das alte Verpachtungssystem.
</div>
<div class="table-responsive">
<table class="table table-hover">
<thead class="table-light">
<tr>
<th>Pächter</th>
<th>Zeitraum</th>
<th>Fläche</th>
<th>Pachtzins</th>
<th>Status</th>
<th>Aktionen</th>
</tr>
</thead>
<tbody>
<tr>
<td>
<a href="{% url 'stiftung:paechter_detail' land.aktueller_paechter.pk %}">
{{ land.aktueller_paechter.get_full_name }}
</a>
</td>
<td>
{% if land.pachtbeginn %}{{ land.pachtbeginn|date:"d.m.Y" }}{% else %}<span class="text-muted">N/A</span>{% endif %} -
{% if land.pachtende %}{{ land.pachtende|date:"d.m.Y" }}{% else %}<span class="text-muted">Unbefristet</span>{% endif %}
</td>
<td>
{{ land.verp_flaeche_aktuell|default_if_none:land.groesse_qm|floatformat:0 }} qm
</td>
<td>
{% if land.pachtzins_aktuell %}€{{ land.pachtzins_aktuell|floatformat:2 }}/Jahr{% else %}<span class="text-muted">N/A</span>{% endif %}
</td>
<td>
<span class="badge bg-warning">Legacy System</span>
</td>
<td>
<div class="btn-group btn-group-sm" role="group">
<a href="{% url 'stiftung:land_verpachtung_edit' land_pk=land.pk %}" class="btn btn-outline-warning" title="Bearbeiten">
<i class="fas fa-edit"></i>
</a>
<a href="{% url 'stiftung:land_verpachtung_end' land_pk=land.pk %}" class="btn btn-outline-danger" title="Beenden">
<i class="fas fa-stop"></i>
</a>
</div>
</td>
</tr>
</tbody>
</table>
</div>
{% else %}
<div class="text-center py-5">
<i class="fas fa-handshake fa-3x text-muted mb-3"></i>
@@ -759,7 +839,7 @@
</div>
<div class="card-body">
<div class="d-grid gap-2">
<a href="{% url 'stiftung:verpachtung_create' %}?land={{ land.pk }}" class="btn btn-info">
<a href="{% url 'stiftung:land_verpachtung_create' land_pk=land.pk %}" class="btn btn-info">
<i class="fas fa-handshake me-2"></i>Neue Verpachtung
</a>
<a href="{% url 'stiftung:land_update' land.pk %}" class="btn btn-warning">

View File

@@ -1,5 +1,6 @@
{% extends 'base.html' %}
{% load static %}
{% load help_tags %}
{% block title %}{{ title }} - Stiftungsverwaltung{% endblock %}
@@ -19,8 +20,12 @@
</a>
</div>
</div>
</div>
</div>
<!-- Form -->
<!-- Form -->
<div class="row">
<div class="col-lg-8">
<div class="card shadow">
<div class="card-header py-3">
<h6 class="m-0 font-weight-bold text-primary">
@@ -414,6 +419,11 @@
</div>
</div>
</div>
<!-- Sidebar mit Hilfe -->
<div class="col-lg-4">
{% help_box 'laenderei_new' user %}
</div>
</div>
{% endblock %}

View File

@@ -0,0 +1,247 @@
{% extends 'base.html' %}
{% load static %}
{% block title %}Verpachtung {{ landverpachtung.vertragsnummer }} - Stiftung{% endblock %}
{% block content %}
<div class="d-flex justify-content-between align-items-center mb-4">
<h1><i class="fas fa-handshake me-2"></i>Verpachtung {{ landverpachtung.vertragsnummer }}</h1>
<div>
<a href="{% url 'stiftung:land_verpachtung_update' landverpachtung.pk %}" class="btn btn-warning">
<i class="fas fa-edit me-1"></i>Bearbeiten
</a>
{% if landverpachtung.status == 'aktiv' %}
<a href="{% url 'stiftung:land_verpachtung_end_direct' landverpachtung.pk %}" class="btn btn-danger">
<i class="fas fa-stop me-1"></i>Beenden
</a>
{% endif %}
<a href="{% url 'stiftung:land_detail' landverpachtung.land.pk %}" class="btn btn-secondary">
<i class="fas fa-arrow-left me-1"></i>Zur Länderei
</a>
</div>
</div>
<div class="row">
<!-- Grunddaten -->
<div class="col-md-6">
<div class="card mb-4">
<div class="card-header">
<h5 class="mb-0"><i class="fas fa-info-circle me-2"></i>Grunddaten</h5>
</div>
<div class="card-body">
<div class="row mb-2">
<div class="col-sm-4"><strong>Vertragsnummer:</strong></div>
<div class="col-sm-8">{{ landverpachtung.vertragsnummer }}</div>
</div>
<div class="row mb-2">
<div class="col-sm-4"><strong>Status:</strong></div>
<div class="col-sm-8">
{% if landverpachtung.status == 'aktiv' %}
<span class="badge bg-success">{{ landverpachtung.get_status_display }}</span>
{% elif landverpachtung.status == 'beendet' %}
<span class="badge bg-secondary">{{ landverpachtung.get_status_display }}</span>
{% else %}
<span class="badge bg-warning">{{ landverpachtung.get_status_display }}</span>
{% endif %}
</div>
</div>
<div class="row mb-2">
<div class="col-sm-4"><strong>Länderei:</strong></div>
<div class="col-sm-8">
<a href="{% url 'stiftung:land_detail' landverpachtung.land.pk %}">
{{ landverpachtung.land.gemarkung }} - {{ landverpachtung.land.gemeinde }},
Flur {{ landverpachtung.land.flur }}, Flurstück {{ landverpachtung.land.flurstueck|default:"N/A" }}
</a>
</div>
</div>
<div class="row mb-2">
<div class="col-sm-4"><strong>Pächter:</strong></div>
<div class="col-sm-8">
<a href="{% url 'stiftung:paechter_detail' landverpachtung.paechter.pk %}">
{{ landverpachtung.paechter.get_full_name }}
</a>
</div>
</div>
<div class="row mb-2">
<div class="col-sm-4"><strong>Verpachtete Fläche:</strong></div>
<div class="col-sm-8">{{ landverpachtung.verpachtete_flaeche|floatformat:0 }} m² ({{ landverpachtung.verpachtete_flaeche_hektar }} ha)</div>
</div>
</div>
</div>
</div>
<!-- Vertragsdaten -->
<div class="col-md-6">
<div class="card mb-4">
<div class="card-header">
<h5 class="mb-0"><i class="fas fa-calendar-alt me-2"></i>Vertragsdaten</h5>
</div>
<div class="card-body">
<div class="row mb-2">
<div class="col-sm-4"><strong>Pachtbeginn:</strong></div>
<div class="col-sm-8">{{ landverpachtung.pachtbeginn|date:"d.m.Y" }}</div>
</div>
<div class="row mb-2">
<div class="col-sm-4"><strong>Pachtende:</strong></div>
<div class="col-sm-8">{{ landverpachtung.pachtende|date:"d.m.Y"|default:"Unbefristet" }}</div>
</div>
<div class="row mb-2">
<div class="col-sm-4"><strong>Verlängerung:</strong></div>
<div class="col-sm-8">
{% if landverpachtung.verlaengerung_klausel %}
<i class="fas fa-check text-success me-1"></i>Automatische Verlängerung
{% else %}
<i class="fas fa-times text-danger me-1"></i>Keine automatische Verlängerung
{% endif %}
</div>
</div>
</div>
</div>
</div>
</div>
<div class="row">
<!-- Finanzielle Daten -->
<div class="col-md-6">
<div class="card mb-4">
<div class="card-header">
<h5 class="mb-0"><i class="fas fa-euro-sign me-2"></i>Finanzielle Daten</h5>
</div>
<div class="card-body">
<div class="row mb-2">
<div class="col-sm-4"><strong>Pachtzins pauschal:</strong></div>
<div class="col-sm-8">{{ landverpachtung.pachtzins_pauschal|floatformat:2 }} € / Jahr</div>
</div>
{% if landverpachtung.pachtzins_pro_ha %}
<div class="row mb-2">
<div class="col-sm-4"><strong>Pachtzins pro ha:</strong></div>
<div class="col-sm-8">{{ landverpachtung.pachtzins_pro_ha|floatformat:2 }} € / ha</div>
</div>
{% endif %}
<div class="row mb-2">
<div class="col-sm-4"><strong>Zahlungsweise:</strong></div>
<div class="col-sm-8">{{ landverpachtung.get_zahlungsweise_display }}</div>
</div>
<div class="row mb-2">
<div class="col-sm-4"><strong>USt-Option:</strong></div>
<div class="col-sm-8">
{% if landverpachtung.ust_option %}
<i class="fas fa-check text-success me-1"></i>Ja ({{ landverpachtung.ust_satz }}%)
{% else %}
<i class="fas fa-times text-danger me-1"></i>Nein
{% endif %}
</div>
</div>
</div>
</div>
</div>
<!-- Umlagen -->
<div class="col-md-6">
<div class="card mb-4">
<div class="card-header">
<h5 class="mb-0"><i class="fas fa-calculator me-2"></i>Umlagen</h5>
</div>
<div class="card-body">
<div class="row mb-2">
<div class="col-sm-6"><strong>Grundsteuer:</strong></div>
<div class="col-sm-6">
{% if landverpachtung.grundsteuer_umlage %}
<i class="fas fa-check text-success me-1"></i>Umlagefähig
{% else %}
<i class="fas fa-times text-danger me-1"></i>Nicht umlagefähig
{% endif %}
</div>
</div>
<div class="row mb-2">
<div class="col-sm-6"><strong>Versicherungen:</strong></div>
<div class="col-sm-6">
{% if landverpachtung.versicherungen_umlage %}
<i class="fas fa-check text-success me-1"></i>Umlagefähig
{% else %}
<i class="fas fa-times text-danger me-1"></i>Nicht umlagefähig
{% endif %}
</div>
</div>
<div class="row mb-2">
<div class="col-sm-6"><strong>Verbandsbeiträge:</strong></div>
<div class="col-sm-6">
{% if landverpachtung.verbandsbeitraege_umlage %}
<i class="fas fa-check text-success me-1"></i>Umlagefähig
{% else %}
<i class="fas fa-times text-danger me-1"></i>Nicht umlagefähig
{% endif %}
</div>
</div>
<div class="row mb-2">
<div class="col-sm-6"><strong>Jagdpachtanteile:</strong></div>
<div class="col-sm-6">
{% if landverpachtung.jagdpacht_anteil_umlage %}
<i class="fas fa-check text-success me-1"></i>Umlagefähig
{% else %}
<i class="fas fa-times text-danger me-1"></i>Nicht umlagefähig
{% endif %}
</div>
</div>
</div>
</div>
</div>
</div>
<!-- Verknüpfte Dokumente -->
<div class="card mb-4">
<div class="card-header d-flex justify-content-between align-items-center">
<h5 class="mb-0"><i class="fas fa-folder-open me-2"></i>Verknüpfte Dokumente</h5>
<a href="/dokumente/verwaltung/" class="btn btn-sm btn-outline-primary">
<i class="fas fa-link me-1"></i>Dokument verknüpfen
</a>
</div>
<div class="card-body">
{% if verknuepfte_dokumente %}
<div class="table-responsive">
<table class="table table-striped">
<thead>
<tr>
<th>Titel</th>
<th>Kontext</th>
<th>Aktionen</th>
</tr>
</thead>
<tbody>
{% for doc in verknuepfte_dokumente %}
<tr>
<td>
<strong>{{ doc.titel|default:"Ohne Titel" }}</strong>
<br>
<small class="text-muted">Paperless-ID: {{ doc.paperless_document_id }}</small>
</td>
<td>{{ doc.get_kontext_display }}</td>
<td>
<a href="{{ doc.get_paperless_url }}" target="_blank" class="btn btn-sm btn-outline-primary" title="In Paperless öffnen">
<i class="fas fa-external-link-alt"></i>
</a>
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
{% else %}
<p class="text-muted">Keine Dokumente verknüpft.</p>
{% endif %}
</div>
</div>
<!-- Bemerkungen -->
{% if landverpachtung.bemerkungen %}
<div class="card">
<div class="card-header">
<h5 class="mb-0"><i class="fas fa-sticky-note me-2"></i>Bemerkungen</h5>
</div>
<div class="card-body">
<p class="mb-0">{{ landverpachtung.bemerkungen|linebreaks }}</p>
</div>
</div>
{% endif %}
{% endblock %}

View File

@@ -1,37 +1,38 @@
{% extends 'base.html' %}
{% load static %}
{% block title %}Verpachtung löschen - Stiftungsverwaltung{% endblock %}
{% block title %}Verpachtung beenden - Stiftungsverwaltung{% endblock %}
{% block content %}
<div class="row">
<div class="col-lg-6 mx-auto">
<div class="card shadow">
<div class="card-header bg-danger text-white">
<div class="card-header bg-warning text-dark">
<h4 class="mb-0">
<i class="fas fa-exclamation-triangle me-2"></i>Verpachtung löschen
<i class="fas fa-exclamation-triangle me-2"></i>Verpachtung beenden
</h4>
</div>
<div class="card-body">
<div class="alert alert-warning">
<h5 class="alert-heading">
<i class="fas fa-exclamation-triangle me-2"></i>Warnung!
<i class="fas fa-exclamation-triangle me-2"></i>Bestätigung erforderlich!
</h5>
<p class="mb-0">
Sind Sie sicher, dass Sie die Verpachtung <strong>{{ verpachtung.vertragsnummer }}</strong> löschen möchten?
Sind Sie sicher, dass Sie die Verpachtung <strong>{{ verpachtung.vertragsnummer }}</strong> beenden möchten?
</p>
</div>
<div class="card mb-3">
<div class="card-body">
<h6 class="card-title">Verpachtungsdetails:</h6>
<h6 class="card-title">Verpachtungs-Details:</h6>
<p class="card-text">
<strong>Länderei:</strong> {{ verpachtung.land.gemeinde }} - {{ verpachtung.land.gemarkung }}<br>
<strong>Pächter:</strong> {{ verpachtung.paechter.get_full_name }}<br>
<strong>Zeitraum:</strong> {{ verpachtung.pachtbeginn|date:"d.m.Y" }} - {{ verpachtung.pachtende|date:"d.m.Y" }}<br>
<strong>Fläche:</strong> {{ verpachtung.verpachtete_flaeche|floatformat:2 }} qm<br>
<strong>Jährlicher Pachtzins:</strong> {{ verpachtung.pachtzins_jaehrlich|floatformat:2 }}
<strong>Vertragsnummer:</strong> {{ verpachtung.vertragsnummer }}<br>
<strong>Pächter:</strong> {{ verpachtung.paechter }}<br>
<strong>Länderei:</strong> {{ verpachtung.land }}<br>
<strong>Pachtbeginn:</strong> {{ verpachtung.pachtbeginn|date:"d.m.Y" }}<br>
<strong>Verpachtete Fläche:</strong> {{ verpachtung.verpachtete_flaeche_qm|floatformat:2 }} qm<br>
<strong>Pachtzins:</strong> {{ verpachtung.pachtzins_euro_pro_qm|floatformat:4 }} €/qm
</p>
</div>
</div>
@@ -41,18 +42,18 @@
<i class="fas fa-info-circle me-2"></i>Wichtiger Hinweis
</h6>
<p class="mb-0">
Diese Aktion kann nicht rückgängig gemacht werden. Alle zugehörigen Daten werden permanent gelöscht.
Die Verpachtung wird auf "beendet" gesetzt und das Pachtende auf das heutige Datum gesetzt. Diese Aktion kann rückgängig gemacht werden, indem der Status und das Pachtende manuell geändert werden.
</p>
</div>
<form method="post">
{% csrf_token %}
<div class="d-flex justify-content-between">
<a href="{% url 'stiftung:verpachtung_detail' verpachtung.pk %}" class="btn btn-secondary">
<a href="{% url 'stiftung:land_verpachtung_detail' verpachtung.pk %}" class="btn btn-secondary">
<i class="fas fa-arrow-left me-2"></i>Abbrechen
</a>
<button type="submit" class="btn btn-danger">
<i class="fas fa-trash me-2"></i>Endgültig löschen
<button type="submit" class="btn btn-warning">
<i class="fas fa-stop me-2"></i>Verpachtung beenden
</button>
</div>
</form>

View File

@@ -15,12 +15,19 @@
<p class="text-muted">
Länderei: <a href="{% url 'stiftung:land_detail' land.pk %}">{{ land }}</a>
{% if is_edit and land.aktueller_paechter %} | Aktueller Pächter: {{ land.aktueller_paechter.get_full_name }}{% endif %}
{% if is_update %} | Bearbeitung von {{ landverpachtung.vertragsnummer }}{% endif %}
</p>
</div>
<div class="col-md-4 text-end">
{% if is_update %}
<a href="{% url 'stiftung:land_verpachtung_detail' landverpachtung.pk %}" class="btn btn-outline-secondary">
<i class="fas fa-arrow-left me-2"></i>Zurück zur Verpachtung
</a>
{% else %}
<a href="{% url 'stiftung:land_detail' land.pk %}" class="btn btn-outline-secondary">
<i class="fas fa-arrow-left me-2"></i>Zurück zur Länderei
</a>
{% endif %}
</div>
</div>
@@ -42,11 +49,12 @@
<div class="col-md-6">
<div class="mb-3">
<label for="aktueller_paechter" class="form-label">Pächter auswählen *</label>
<select class="form-select" id="aktueller_paechter" name="aktueller_paechter" required>
<select class="form-select" id="aktueller_paechter" name="paechter" required>
<option value="">Bitte wählen...</option>
{% for paechter in paechter_list %}
<option value="{{ paechter.pk }}"
{% if is_edit and land.aktueller_paechter and paechter.pk == land.aktueller_paechter.pk %}selected{% endif %}
{% if is_update and landverpachtung.paechter and paechter.pk == landverpachtung.paechter.pk %}selected
{% elif is_edit and land.aktueller_paechter and paechter.pk == land.aktueller_paechter.pk %}selected{% endif %}
data-name="{{ paechter.get_full_name }}"
data-anschrift="{{ paechter.strasse|default:'' }}&#10;{{ paechter.plz|default:'' }} {{ paechter.ort|default:'' }}">
{{ paechter.get_full_name }}
@@ -60,7 +68,7 @@
<div class="mb-3">
<label for="verpachtete_flaeche" class="form-label">Verpachtete Fläche (qm) *</label>
<input type="number" step="0.01" class="form-control" id="verpachtete_flaeche" name="verpachtete_flaeche"
value="{% if is_edit %}{{ land.verp_flaeche_aktuell|default:verfuegbare_flaeche }}{% else %}{{ verfuegbare_flaeche|default:land.groesse_qm }}{% endif %}"
value="{% if is_update %}{{ landverpachtung.verpachtete_flaeche }}{% elif is_edit %}{{ land.verp_flaeche_aktuell|default:verfuegbare_flaeche }}{% else %}{{ verfuegbare_flaeche|default:land.groesse_qm }}{% endif %}"
max="{{ verfuegbare_flaeche|default:land.groesse_qm }}" required>
<small class="text-muted">Verfügbar: {{ verfuegbare_flaeche|default:land.groesse_qm|floatformat:0 }} qm | Gesamt: {{ land.groesse_qm|floatformat:0 }} qm</small>
</div>
@@ -73,14 +81,14 @@
<div class="mb-3">
<label for="pachtbeginn" class="form-label">Pachtbeginn *</label>
<input type="date" class="form-control" id="pachtbeginn" name="pachtbeginn"
value="{% if is_edit and land.pachtbeginn %}{{ land.pachtbeginn|date:'Y-m-d' }}{% endif %}" required>
value="{% if is_update %}{{ landverpachtung.pachtbeginn|date:'Y-m-d' }}{% elif is_edit and land.pachtbeginn %}{{ land.pachtbeginn|date:'Y-m-d' }}{% endif %}" required>
</div>
</div>
<div class="col-md-4">
<div class="mb-3">
<label for="pachtende" class="form-label">Pachtende</label>
<input type="date" class="form-control" id="pachtende" name="pachtende"
value="{% if is_edit and land.pachtende %}{{ land.pachtende|date:'Y-m-d' }}{% endif %}">
value="{% if is_update and landverpachtung.pachtende %}{{ landverpachtung.pachtende|date:'Y-m-d' }}{% elif is_edit and land.pachtende %}{{ land.pachtende|date:'Y-m-d' }}{% endif %}">
<small class="text-muted">Leer = unbefristet</small>
</div>
</div>
@@ -88,7 +96,7 @@
<div class="mb-3">
<div class="form-check mt-4">
<input class="form-check-input" type="checkbox" id="verlaengerung_klausel" name="verlaengerung_klausel"
{% if is_edit and land.verlaengerung_klausel %}checked{% endif %}>
{% if is_update and landverpachtung.verlaengerung_klausel %}checked{% elif is_edit and land.verlaengerung_klausel %}checked{% endif %}>
<label class="form-check-label" for="verlaengerung_klausel">
Automatische Verlängerung
</label>
@@ -139,9 +147,9 @@
<label for="zahlungsweise" class="form-label">Zahlungsweise</label>
<select class="form-select" id="zahlungsweise" name="zahlungsweise">
<option value="jaehrlich" {% if is_edit and land.zahlungsweise == 'jaehrlich' %}selected{% endif %}>Jährlich</option>
<option value="halbjaehrlich" {% if is_edit and land.zahlungsweise == 'halbjaehrlich' %}selected{% endif %}>Halbjährlich</option>
<option value="vierteljaehrlich" {% if is_edit and land.zahlungsweise == 'vierteljaehrlich' %}selected{% endif %}>Vierteljährlich</option>
<option value="monatlich" {% if is_edit and land.zahlungsweise == 'monatlich' %}selected{% endif %}>Monatlich</option>
<option value="halbjaehrlich" {% if is_update and landverpachtung.zahlungsweise == 'halbjaehrlich' %}selected{% elif is_edit and land.zahlungsweise == 'halbjaehrlich' %}selected{% endif %}>Halbjährlich</option>
<option value="vierteljaehrlich" {% if is_update and landverpachtung.zahlungsweise == 'vierteljaehrlich' %}selected{% elif is_edit and land.zahlungsweise == 'vierteljaehrlich' %}selected{% endif %}>Vierteljährlich</option>
<option value="monatlich" {% if is_update and landverpachtung.zahlungsweise == 'monatlich' %}selected{% elif is_edit and land.zahlungsweise == 'monatlich' %}selected{% endif %}>Monatlich</option>
</select>
</div>
</div>
@@ -153,7 +161,7 @@
<div class="mb-3">
<div class="form-check">
<input class="form-check-input" type="checkbox" id="ust_option" name="ust_option"
{% if is_edit and land.ust_option %}checked{% endif %}>
{% if is_update and landverpachtung.ust_option %}checked{% elif is_edit and land.ust_option %}checked{% endif %}>
<label class="form-check-label" for="ust_option">
<strong>USt-Option</strong> (Pacht mit Umsatzsteuer)
</label>
@@ -164,7 +172,7 @@
<div class="mb-3">
<label for="ust_satz" class="form-label">USt-Satz (%)</label>
<input type="number" step="0.01" class="form-control" id="ust_satz" name="ust_satz"
value="{% if is_edit %}{{ land.ust_satz|default:'19.00' }}{% else %}19.00{% endif %}">
value="{% if is_update %}{{ landverpachtung.ust_satz|default:'19.00' }}{% elif is_edit %}{{ land.ust_satz|default:'19.00' }}{% else %}19.00{% endif %}">
<small class="text-muted">Standard: 19%</small>
</div>
</div>

View File

@@ -1,5 +1,6 @@
{% extends 'base.html' %}
{% load static %}
{% load help_tags %}
{% block title %}{{ title }} - Stiftungsverwaltung{% endblock %}
@@ -18,15 +19,17 @@
</a>
</div>
</div>
</div>
</div>
<div class="row">
<div class="col-lg-8">
<div class="card shadow">
<div class="card-header bg-primary text-white">
<h5 class="card-title mb-0">
<i class="fas fa-edit me-2"></i>Pächter-Daten
</h5>
</div>
<div class="row">
<div class="col-lg-8">
<div class="card shadow">
<div class="card-header bg-primary text-white">
<h5 class="card-title mb-0">
<i class="fas fa-edit me-2"></i>Pächter-Daten
</h5>
</div>
<div class="card-body">
<form method="post" novalidate>
{% csrf_token %}
@@ -90,7 +93,6 @@
{% endif %}
</div>
</div>
</div>
<!-- Kontaktinformationen -->
<div class="row mb-4">
@@ -418,34 +420,10 @@
</div>
</div>
<!-- Sidebar -->
<!-- Sidebar mit Hilfe -->
<div class="col-lg-4">
<div class="card shadow">
<div class="card-header bg-info text-white">
<h6 class="card-title mb-0">
<i class="fas fa-info-circle me-2"></i>Hilfe
</h6>
</div>
<div class="card-body">
<h6>Pflichtfelder:</h6>
<ul class="list-unstyled small">
<li><i class="fas fa-asterisk text-danger me-1"></i>Vorname</li>
<li><i class="fas fa-asterisk text-danger me-1"></i>Nachname</li>
</ul>
<hr>
<h6>Wichtige Hinweise:</h6>
<ul class="list-unstyled small">
<li><i class="fas fa-lightbulb text-warning me-1"></i>Die Pachtnummer dient zur eindeutigen Identifikation</li>
<li><i class="fas fa-lightbulb text-warning me-1"></i>Landwirtschaftliche Ausbildung wird für die Pachtbewertung benötigt</li>
<li><i class="fas fa-lightbulb text-warning me-1"></i>Berufserfahrung und Spezialisierung helfen bei der Flächenzuweisung</li>
</ul>
</div>
</div>
{% help_box 'paechter_new' user %}
</div>
</div>
</div>
</div>
{% endblock %}

View File

@@ -1,15 +1,67 @@
{% extends 'base.html' %}
{% block title %}Unterstützung löschen{% endblock %}
{% load static %}
{% block title %}Unterstützung löschen - Stiftungsverwaltung{% endblock %}
{% block content %}
<div class="card shadow">
<div class="card-body">
<h5>Möchten Sie diese Unterstützung wirklich löschen?</h5>
<p><strong>{{ obj.destinataer.get_full_name }}</strong>, €{{ obj.betrag|floatformat:2 }}, fällig am {{ obj.faellig_am|date:"d.m.Y" }}</p>
<form method="post">{% csrf_token %}
<a href="{% url 'stiftung:unterstuetzungen_list' %}" class="btn btn-outline-secondary">Abbrechen</a>
<button type="submit" class="btn btn-danger">Löschen</button>
</form>
</div>
</div>
<div class="row">
<div class="col-lg-6 mx-auto">
<div class="card shadow">
<div class="card-header bg-danger text-white">
<h4 class="mb-0">
<i class="fas fa-exclamation-triangle me-2"></i>Unterstützung löschen
</h4>
</div>
<div class="card-body">
<div class="alert alert-warning">
<h5 class="alert-heading">
<i class="fas fa-exclamation-triangle me-2"></i>Warnung!
</h5>
<p class="mb-0">
Sind Sie sicher, dass Sie die Unterstützung für <strong>{{ obj.destinataer.get_full_name }}</strong>
(€{{ obj.betrag|floatformat:2 }}, fällig am {{ obj.faellig_am|date:"d.m.Y" }}) löschen möchten?
</p>
</div>
{% if obj.beschreibung %}
<div class="mb-3">
<strong>Beschreibung:</strong> {{ obj.beschreibung }}
</div>
{% endif %}
{% if obj.wiederkehrend_von %}
<div class="alert alert-info">
<i class="fas fa-sync-alt me-2"></i>
<strong>Hinweis:</strong> Diese Unterstützung ist Teil einer wiederkehrenden Zahlung.
{% if will_delete_template %}
<br><strong>Achtung:</strong> Da dies die letzte Zahlung dieser wiederkehrenden Vorlage ist,
wird auch die wiederkehrende Zahlungsvorlage gelöscht.
{% else %}
Das Löschen betrifft nur diese einzelne Zahlung, nicht die wiederkehrende Vorlage.
{% endif %}
</div>
{% endif %}
<p class="text-muted">
Diese Aktion kann nicht rückgängig gemacht werden. Alle zugehörigen Daten werden permanent gelöscht.
</p>
<form method="post">
{% csrf_token %}
<div class="d-flex justify-content-between">
<a href="{% url 'stiftung:unterstuetzungen_all' %}" class="btn btn-secondary">
<i class="fas fa-arrow-left me-2"></i>Abbrechen
</a>
<button type="submit" class="btn btn-danger">
<i class="fas fa-trash me-2"></i>Endgültig löschen
</button>
</div>
</form>
</div>
</div>
</div>
</div>
{% endblock %}

View File

@@ -0,0 +1,183 @@
{% extends 'base.html' %}
{% block title %}{{ title }}{% endblock %}
{% block content %}
<nav aria-label="breadcrumb">
<ol class="breadcrumb">
<li class="breadcrumb-item"><a href="{% url 'stiftung:dashboard' %}">Dashboard</a></li>
<li class="breadcrumb-item"><a href="{% url 'stiftung:unterstuetzungen_all' %}">Unterstützungen</a></li>
<li class="breadcrumb-item active" aria-current="page">{{ title }}</li>
</ol>
</nav>
<div class="row">
<div class="col-lg-10">
<div class="card shadow">
<div class="card-header bg-primary text-white">
<div class="d-flex justify-content-between align-items-center">
<h5 class="mb-0">{{ title }}</h5>
<div>
{% if unterstuetzung.status == 'geplant' %}
<span class="badge bg-light text-dark">{{ unterstuetzung.get_status_display }}</span>
{% elif unterstuetzung.status == 'faellig' %}
<span class="badge bg-warning">{{ unterstuetzung.get_status_display }}</span>
{% elif unterstuetzung.status == 'in_bearbeitung' %}
<span class="badge bg-info">{{ unterstuetzung.get_status_display }}</span>
{% elif unterstuetzung.status == 'ausgezahlt' %}
<span class="badge bg-success">{{ unterstuetzung.get_status_display }}</span>
{% else %}
<span class="badge bg-danger">{{ unterstuetzung.get_status_display }}</span>
{% endif %}
{% if unterstuetzung.is_overdue %}
<span class="badge bg-danger ms-2">Überfällig</span>
{% endif %}
{% if unterstuetzung.wiederkehrend_von %}
<span class="badge bg-secondary ms-2">Wiederkehrend</span>
{% endif %}
</div>
</div>
</div>
<div class="card-body">
<!-- Basic Information -->
<div class="row mb-4">
<div class="col-md-6">
<h6 class="text-muted mb-2"><i class="fas fa-user me-2"></i>Destinatär</h6>
<p class="fw-bold mb-0">
<a href="{% url 'stiftung:destinataer_detail' pk=unterstuetzung.destinataer.pk %}"
class="text-decoration-none">
{{ unterstuetzung.destinataer.get_full_name }}
</a>
</p>
</div>
<div class="col-md-6">
<h6 class="text-muted mb-2"><i class="fas fa-euro-sign me-2"></i>Betrag</h6>
<p class="fw-bold fs-4 text-success mb-0">€{{ unterstuetzung.betrag|floatformat:2 }}</p>
</div>
</div>
<div class="row mb-4">
<div class="col-md-4">
<h6 class="text-muted mb-2"><i class="fas fa-calendar me-2"></i>Fällig am</h6>
<p class="mb-0">{{ unterstuetzung.faellig_am|date:"d.m.Y" }}</p>
</div>
<div class="col-md-4">
<h6 class="text-muted mb-2"><i class="fas fa-university me-2"></i>Zahlungskonto</h6>
<p class="mb-0">{{ unterstuetzung.konto }}</p>
</div>
<div class="col-md-4">
<h6 class="text-muted mb-2"><i class="fas fa-info-circle me-2"></i>Status</h6>
<p class="mb-0">{{ unterstuetzung.get_status_display }}</p>
</div>
</div>
<!-- Bank Transfer Information -->
<div class="row mb-4">
<div class="col-12">
<h6 class="text-muted mb-3"><i class="fas fa-money-bill-transfer me-2"></i>Überweisungsdaten</h6>
</div>
<div class="col-md-6">
<h6 class="text-muted mb-2">Empfänger IBAN</h6>
<p class="mb-0 font-monospace">{{ unterstuetzung.empfaenger_iban|default:"Nicht angegeben" }}</p>
</div>
<div class="col-md-6">
<h6 class="text-muted mb-2">Empfänger Name</h6>
<p class="mb-0">{{ unterstuetzung.empfaenger_name|default:"Nicht angegeben" }}</p>
</div>
</div>
{% if unterstuetzung.verwendungszweck %}
<div class="row mb-4">
<div class="col-12">
<h6 class="text-muted mb-2">Verwendungszweck</h6>
<p class="mb-0">{{ unterstuetzung.verwendungszweck }}</p>
</div>
</div>
{% endif %}
{% if unterstuetzung.beschreibung %}
<div class="row mb-4">
<div class="col-12">
<h6 class="text-muted mb-2">Beschreibung</h6>
<p class="mb-0">{{ unterstuetzung.beschreibung }}</p>
</div>
</div>
{% endif %}
<!-- Payment Information (if paid) -->
{% if unterstuetzung.status == 'ausgezahlt' and unterstuetzung.ausgezahlt_am %}
<div class="alert alert-success mb-4">
<h6 class="alert-heading mb-2"><i class="fas fa-check-circle me-2"></i>Zahlungsinformationen</h6>
<div class="row">
<div class="col-md-6">
<strong>Ausgezahlt am:</strong> {{ unterstuetzung.ausgezahlt_am|date:"d.m.Y" }}
</div>
{% if unterstuetzung.ausgezahlt_von %}
<div class="col-md-6">
<strong>Ausgezahlt von:</strong> {{ unterstuetzung.ausgezahlt_von.get_full_name }}
</div>
{% endif %}
</div>
</div>
{% endif %}
<!-- Recurring Payment Information -->
{% if unterstuetzung.wiederkehrend_von %}
<div class="alert alert-info mb-4">
<h6 class="alert-heading mb-2"><i class="fas fa-sync-alt me-2"></i>Wiederkehrende Zahlung</h6>
<p class="mb-2">Diese Zahlung wurde automatisch aus einer wiederkehrenden Vorlage generiert.</p>
<div class="row">
<div class="col-md-4">
<strong>Intervall:</strong> {{ unterstuetzung.wiederkehrend_von.get_intervall_display }}
</div>
<div class="col-md-4">
<strong>Erste Zahlung:</strong> {{ unterstuetzung.wiederkehrend_von.erste_zahlung_am|date:"d.m.Y" }}
</div>
<div class="col-md-4">
<strong>Nächste Generierung:</strong> {{ unterstuetzung.wiederkehrend_von.naechste_generierung|date:"d.m.Y" }}
</div>
</div>
</div>
{% endif %}
<!-- Meta Information -->
<div class="row mb-4">
<div class="col-md-6">
<h6 class="text-muted mb-2">Erstellt am</h6>
<p class="mb-0">{{ unterstuetzung.erstellt_am|date:"d.m.Y H:i" }}</p>
</div>
<div class="col-md-6">
<h6 class="text-muted mb-2">Zuletzt aktualisiert</h6>
<p class="mb-0">{{ unterstuetzung.aktualisiert_am|date:"d.m.Y H:i" }}</p>
</div>
</div>
<!-- Action Buttons -->
<div class="d-flex justify-content-between mt-4">
<div>
<a href="{% url 'stiftung:unterstuetzungen_all' %}" class="btn btn-secondary">
<i class="fas fa-arrow-left me-2"></i>Zurück zur Liste
</a>
<a href="{% url 'stiftung:destinataer_detail' pk=unterstuetzung.destinataer.pk %}" class="btn btn-outline-primary ms-2">
<i class="fas fa-user me-2"></i>Destinatär anzeigen
</a>
</div>
<div>
<a href="{% url 'stiftung:unterstuetzung_edit' pk=unterstuetzung.pk %}" class="btn btn-warning me-2">
<i class="fas fa-edit me-2"></i>Bearbeiten
</a>
{% if can_mark_paid %}
<a href="{% url 'stiftung:unterstuetzung_mark_paid' pk=unterstuetzung.pk %}" class="btn btn-success me-2">
<i class="fas fa-check me-2"></i>Als bezahlt markieren
</a>
{% endif %}
<a href="{% url 'stiftung:unterstuetzung_delete' pk=unterstuetzung.pk %}" class="btn btn-danger">
<i class="fas fa-trash me-2"></i>Löschen
</a>
</div>
</div>
</div>
</div>
</div>
</div>
{% endblock %}

View File

@@ -1,46 +1,265 @@
{% extends 'base.html' %}
{% load help_tags %}
{% block title %}{{ title }}{% endblock %}
{% block content %}
<nav aria-label="breadcrumb">
<ol class="breadcrumb">
<li class="breadcrumb-item"><a href="{% url 'stiftung:dashboard' %}">Dashboard</a></li>
<li class="breadcrumb-item"><a href="{% url 'stiftung:unterstuetzungen_all' %}">Unterstützungen</a></li>
<li class="breadcrumb-item active" aria-current="page">{{ title }}</li>
</ol>
</nav>
<div class="row">
<div class="col-lg-8">
<div class="card shadow">
<div class="card-header bg-primary text-white"><h5 class="mb-0">{{ title }}</h5></div>
<div class="card-header bg-primary text-white">
<h5 class="mb-0">{{ title }}</h5>
</div>
<div class="card-body">
<form method="post">{% csrf_token %}
<div class="mb-3">
<label class="form-label" for="{{ form.destinataer.id_for_label }}">{{ form.destinataer.label }}</label>
{{ form.destinataer }}
<form method="post">
{% csrf_token %}
<!-- Basic Information -->
<div class="row mb-4">
<div class="col-md-6 mb-3">
<label class="form-label" for="{{ form.destinataer.id_for_label }}">{{ form.destinataer.label }}</label>
{{ form.destinataer }}
{% if form.destinataer.errors %}
<div class="text-danger">{{ form.destinataer.errors }}</div>
{% endif %}
</div>
<div class="col-md-6 mb-3">
<label class="form-label" for="{{ form.konto.id_for_label }}">{{ form.konto.label }}</label>
{{ form.konto }}
{% if form.konto.errors %}
<div class="text-danger">{{ form.konto.errors }}</div>
{% endif %}
</div>
</div>
<div class="mb-3">
<label class="form-label" for="{{ form.konto.id_for_label }}">{{ form.konto.label }}</label>
{{ form.konto }}
</div>
<div class="row">
<div class="col-md-4 mb-3">
<!-- Payment Details -->
<div class="row mb-4">
<div class="col-md-3 mb-3">
<label class="form-label" for="{{ form.betrag.id_for_label }}">{{ form.betrag.label }}</label>
{{ form.betrag }}
{% if form.betrag.errors %}
<div class="text-danger">{{ form.betrag.errors }}</div>
{% endif %}
</div>
<div class="col-md-4 mb-3">
<div class="col-md-3 mb-3">
<label class="form-label" for="{{ form.faellig_am.id_for_label }}">{{ form.faellig_am.label }}</label>
{{ form.faellig_am }}
{% if form.faellig_am.errors %}
<div class="text-danger">{{ form.faellig_am.errors }}</div>
{% endif %}
</div>
<div class="col-md-4 mb-3">
<div class="col-md-3 mb-3">
<label class="form-label" for="{{ form.status.id_for_label }}">{{ form.status.label }}</label>
{{ form.status }}
{% if form.status.errors %}
<div class="text-danger">{{ form.status.errors }}</div>
{% endif %}
</div>
<div class="col-md-3 mb-3">
<div class="form-check mt-4">
{{ form.ist_wiederkehrend }}
<label class="form-check-label" for="{{ form.ist_wiederkehrend.id_for_label }}">
{{ form.ist_wiederkehrend.label }}
</label>
{% if form.ist_wiederkehrend.help_text %}
<div class="form-text">{{ form.ist_wiederkehrend.help_text }}</div>
{% endif %}
</div>
</div>
</div>
<div class="mb-3">
<label class="form-label" for="{{ form.beschreibung.id_for_label }}">{{ form.beschreibung.label }}</label>
{{ form.beschreibung }}
<!-- Recurring Payment Settings (initially hidden) -->
<div id="recurring-fields" style="display: none;" class="mb-4">
<div class="alert alert-info">
<i class="fas fa-info-circle me-2"></i>
<strong>Wiederkehrende Zahlung:</strong> Diese Einstellungen werden nur für automatisch wiederkehrende Zahlungen verwendet.
</div>
<div class="row">
<div class="col-md-6 mb-3">
<label class="form-label" for="{{ form.intervall.id_for_label }}">{{ form.intervall.label }}</label>
{{ form.intervall }}
{% if form.intervall.errors %}
<div class="text-danger">{{ form.intervall.errors }}</div>
{% endif %}
</div>
<div class="col-md-6 mb-3">
<label class="form-label" for="{{ form.letzte_zahlung_am.id_for_label }}">{{ form.letzte_zahlung_am.label }}</label>
{{ form.letzte_zahlung_am }}
{% if form.letzte_zahlung_am.help_text %}
<div class="form-text">{{ form.letzte_zahlung_am.help_text }}</div>
{% endif %}
{% if form.letzte_zahlung_am.errors %}
<div class="text-danger">{{ form.letzte_zahlung_am.errors }}</div>
{% endif %}
</div>
</div>
</div>
<!-- Bank Transfer Information -->
<h6 class="text-muted mb-3"><i class="fas fa-university me-2"></i>Überweisungsdaten</h6>
<div class="row mb-4">
<div class="col-md-6 mb-3">
<label class="form-label" for="{{ form.empfaenger_iban.id_for_label }}">{{ form.empfaenger_iban.label }}</label>
{{ form.empfaenger_iban }}
{% if form.empfaenger_iban.errors %}
<div class="text-danger">{{ form.empfaenger_iban.errors }}</div>
{% endif %}
</div>
<div class="col-md-6 mb-3">
<label class="form-label" for="{{ form.empfaenger_name.id_for_label }}">{{ form.empfaenger_name.label }}</label>
{{ form.empfaenger_name }}
{% if form.empfaenger_name.errors %}
<div class="text-danger">{{ form.empfaenger_name.errors }}</div>
{% endif %}
</div>
</div>
<div class="row mb-4">
<div class="col-md-8 mb-3">
<label class="form-label" for="{{ form.verwendungszweck.id_for_label }}">{{ form.verwendungszweck.label }}</label>
{{ form.verwendungszweck }}
{% if form.verwendungszweck.errors %}
<div class="text-danger">{{ form.verwendungszweck.errors }}</div>
{% endif %}
</div>
<div class="col-md-4 mb-3">
<label class="form-label" for="{{ form.beschreibung.id_for_label }}">{{ form.beschreibung.label }}</label>
{{ form.beschreibung }}
{% if form.beschreibung.errors %}
<div class="text-danger">{{ form.beschreibung.errors }}</div>
{% endif %}
</div>
</div>
<div class="d-flex justify-content-between">
<a href="{% url 'stiftung:unterstuetzungen_list' %}" class="btn btn-outline-secondary">Abbrechen</a>
<button class="btn btn-primary" type="submit">Speichern</button>
<a href="{% url 'stiftung:unterstuetzungen_all' %}" class="btn btn-secondary">
<i class="fas fa-arrow-left me-2"></i>Zurück
</a>
<button type="submit" class="btn btn-primary">
<i class="fas fa-save me-2"></i>Speichern
</button>
</div>
</form>
</div>
</div>
</div>
<!-- Sidebar mit Hilfe -->
<div class="col-lg-4">
{% help_box 'unterstuetzung_new' user %}
</div>
</div>
<script>
function toggleRecurringFields() {
const recurringCheckbox = document.getElementById('{{ form.ist_wiederkehrend.id_for_label }}');
const recurringFields = document.getElementById('recurring-fields');
const intervalField = document.getElementById('{{ form.intervall.id_for_label }}');
console.log('toggleRecurringFields called', {
checkbox: !!recurringCheckbox,
checked: recurringCheckbox ? recurringCheckbox.checked : 'N/A',
fieldsDiv: !!recurringFields,
intervalField: !!intervalField
});
if (recurringCheckbox && recurringFields) {
if (recurringCheckbox.checked) {
recurringFields.style.display = 'block';
if (intervalField) {
intervalField.required = true;
}
console.log('Showing recurring fields');
} else {
recurringFields.style.display = 'none';
if (intervalField) {
intervalField.required = false;
intervalField.value = '';
}
console.log('Hiding recurring fields');
}
} else {
console.error('Could not find required elements for recurring fields toggle');
}
}
function updateDestinataerInfo() {
const destinataerSelect = document.getElementById('{{ form.destinataer.id_for_label }}');
const ibanField = document.getElementById('{{ form.empfaenger_iban.id_for_label }}');
const nameField = document.getElementById('{{ form.empfaenger_name.id_for_label }}');
if (destinataerSelect.value) {
// Fetch IBAN and name information via AJAX
fetch(`/api/destinataer/${destinataerSelect.value}/info/`, {
method: 'GET',
headers: {
'X-Requested-With': 'XMLHttpRequest',
'X-CSRFToken': document.querySelector('[name=csrfmiddlewaretoken]').value
},
})
.then(response => response.json())
.then(data => {
if (data.success) {
// Update IBAN field if it's empty
if (!ibanField.value && data.iban) {
ibanField.value = data.iban;
}
// Update name field if it's empty
if (!nameField.value && data.name) {
nameField.value = data.name;
}
} else {
console.error('Error fetching destinataer info:', data.error);
}
})
.catch(error => {
console.error('Error:', error);
});
} else {
// Clear fields when no destinataer is selected
if (!ibanField.value) ibanField.value = '';
if (!nameField.value) nameField.value = '';
}
}
// Initialize the form
document.addEventListener('DOMContentLoaded', function() {
console.log('Initializing form...');
// Get form elements
const destinataerSelect = document.getElementById('{{ form.destinataer.id_for_label }}');
const recurringCheckbox = document.getElementById('{{ form.ist_wiederkehrend.id_for_label }}');
const recurringFields = document.getElementById('recurring-fields');
const intervalField = document.getElementById('{{ form.intervall.id_for_label }}');
console.log('Form elements found:', {
destinataerSelect: !!destinataerSelect,
recurringCheckbox: !!recurringCheckbox,
recurringFields: !!recurringFields,
intervalField: !!intervalField
});
// Add event listeners
if (destinataerSelect) {
destinataerSelect.addEventListener('change', updateDestinataerInfo);
}
if (recurringCheckbox) {
recurringCheckbox.addEventListener('change', function() {
console.log('Recurring checkbox changed:', this.checked);
toggleRecurringFields();
});
// Initialize state
toggleRecurringFields();
}
});
</script>
{% endblock %}

View File

@@ -0,0 +1,74 @@
{% extends 'base.html' %}
{% block title %}{{ title }}{% endblock %}
{% block content %}
<nav aria-label="breadcrumb">
<ol class="breadcrumb">
<li class="breadcrumb-item"><a href="{% url 'stiftung:dashboard' %}">Dashboard</a></li>
<li class="breadcrumb-item"><a href="{% url 'stiftung:unterstuetzungen_all' %}">Unterstützungen</a></li>
<li class="breadcrumb-item"><a href="{% url 'stiftung:unterstuetzung_detail' pk=unterstuetzung.pk %}">Unterstützung Details</a></li>
<li class="breadcrumb-item active" aria-current="page">Als bezahlt markieren</li>
</ol>
</nav>
<div class="row">
<div class="col-lg-8">
<div class="card shadow">
<div class="card-header bg-success text-white">
<h5 class="mb-0">{{ title }}</h5>
</div>
<div class="card-body">
<!-- Payment Summary -->
<div class="alert alert-info mb-4">
<h6><i class="fas fa-info-circle me-2"></i>Zahlungsdetails</h6>
<div class="row">
<div class="col-md-6">
<strong>Destinatär:</strong> {{ unterstuetzung.destinataer.get_full_name }}<br>
<strong>Betrag:</strong> €{{ unterstuetzung.betrag }}<br>
<strong>Fällig am:</strong> {{ unterstuetzung.faellig_am }}
</div>
<div class="col-md-6">
<strong>IBAN:</strong> {{ unterstuetzung.empfaenger_iban|default:"Nicht angegeben" }}<br>
<strong>Verwendungszweck:</strong> {{ unterstuetzung.verwendungszweck|default:"Nicht angegeben" }}<br>
<strong>Status:</strong> <span class="badge bg-warning">{{ unterstuetzung.get_status_display }}</span>
</div>
</div>
</div>
<form method="post">
{% csrf_token %}
<div class="row">
<div class="col-md-6 mb-3">
<label class="form-label" for="{{ form.ausgezahlt_am.id_for_label }}">{{ form.ausgezahlt_am.label }}</label>
{{ form.ausgezahlt_am }}
{% if form.ausgezahlt_am.errors %}
<div class="text-danger">{{ form.ausgezahlt_am.errors }}</div>
{% endif %}
</div>
</div>
<div class="mb-3">
<label class="form-label" for="{{ form.bemerkung.id_for_label }}">{{ form.bemerkung.label }}</label>
{{ form.bemerkung }}
{% if form.bemerkung.help_text %}
<div class="form-text">{{ form.bemerkung.help_text }}</div>
{% endif %}
{% if form.bemerkung.errors %}
<div class="text-danger">{{ form.bemerkung.errors }}</div>
{% endif %}
</div>
<div class="d-flex justify-content-between">
<a href="{% url 'stiftung:unterstuetzung_detail' pk=unterstuetzung.pk %}" class="btn btn-secondary">
<i class="fas fa-arrow-left me-2"></i>Zurück
</a>
<button type="submit" class="btn btn-success">
<i class="fas fa-check me-2"></i>Als bezahlt markieren
</button>
</div>
</form>
</div>
</div>
</div>
</div>
{% endblock %}

View File

@@ -0,0 +1,492 @@
{% extends 'base.html' %}
{% load static %}
{% block title %}Unterstützungen - Stiftungsverwaltung{% endblock %}
{% block content %}
<div class="d-flex justify-content-between align-items-center mb-4">
<h1 class="h3">
<i class="fas fa-hand-holding-usd text-primary me-2"></i>Unterstützungen
</h1>
<div>
<button type="button" class="btn btn-outline-secondary me-2" data-bs-toggle="modal" data-bs-target="#exportModal">
<i class="fas fa-download me-2"></i>Exportieren
</button>
<a href="{% url 'stiftung:wiederkehrende_unterstuetzungen' %}" class="btn btn-outline-info me-2">
<i class="fas fa-sync-alt me-2"></i> Wiederkehrende
</a>
<a href="{% url 'stiftung:unterstuetzung_create' %}" class="btn btn-primary">
<i class="fas fa-plus me-2"></i> Neue Unterstützung
</a>
</div>
</div>
<!-- Export Modal -->
<div class="modal fade" id="exportModal" tabindex="-1">
<div class="modal-dialog modal-lg">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">Export Optionen</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<form id="exportForm" method="post">
{% csrf_token %}
<div class="modal-body">
<div class="row">
<div class="col-md-6">
<h6 class="fw-bold">Export Format</h6>
<div class="mb-3">
<div class="form-check">
<input class="form-check-input" type="radio" name="format" id="format_csv" value="csv" checked>
<label class="form-check-label" for="format_csv">
<i class="fas fa-file-csv me-2"></i>CSV (Excel-kompatibel)
</label>
</div>
<div class="form-check">
<input class="form-check-input" type="radio" name="format" id="format_pdf" value="pdf">
<label class="form-check-label" for="format_pdf">
<i class="fas fa-file-pdf me-2"></i>PDF (Tabelle)
</label>
</div>
</div>
<h6 class="fw-bold">Export Umfang</h6>
<div class="mb-3">
<div class="form-check">
<input class="form-check-input" type="radio" name="scope" id="scope_all" value="all" checked>
<label class="form-check-label" for="scope_all">
Alle Einträge (<span id="total-count">{{ unterstuetzungen.count }}</span>)
</label>
</div>
<div class="form-check">
<input class="form-check-input" type="radio" name="scope" id="scope_selected" value="selected">
<label class="form-check-label" for="scope_selected">
Nur ausgewählte Einträge (<span id="selected-count">0</span>)
</label>
</div>
</div>
</div>
<div class="col-md-6" id="field-selection" style="max-height: 400px; overflow-y: auto;">
<h6 class="fw-bold">Felder auswählen</h6>
<div class="mb-2">
<button type="button" class="btn btn-sm btn-outline-primary me-2" onclick="selectAllFields()">Alle auswählen</button>
<button type="button" class="btn btn-sm btn-outline-secondary" onclick="selectDefaultFields()">Standard</button>
</div>
<div class="field-groups">
<!-- Core Payment Fields -->
<div class="mb-3">
<h6 class="text-primary mb-2">Zahlungsdaten</h6>
<div class="form-check form-check-sm">
<input class="form-check-input field-checkbox" type="checkbox" name="fields" value="destinataer_name" id="field_destinataer_name" checked>
<label class="form-check-label" for="field_destinataer_name">Destinatär Name</label>
</div>
<div class="form-check form-check-sm">
<input class="form-check-input field-checkbox" type="checkbox" name="fields" value="betrag" id="field_betrag" checked>
<label class="form-check-label" for="field_betrag">Betrag (€)</label>
</div>
<div class="form-check form-check-sm">
<input class="form-check-input field-checkbox" type="checkbox" name="fields" value="faellig_am" id="field_faellig_am" checked>
<label class="form-check-label" for="field_faellig_am">Fällig am</label>
</div>
<div class="form-check form-check-sm">
<input class="form-check-input field-checkbox" type="checkbox" name="fields" value="status" id="field_status" checked>
<label class="form-check-label" for="field_status">Status</label>
</div>
<div class="form-check form-check-sm">
<input class="form-check-input field-checkbox" type="checkbox" name="fields" value="beschreibung" id="field_beschreibung" checked>
<label class="form-check-label" for="field_beschreibung">Beschreibung</label>
</div>
<div class="form-check form-check-sm">
<input class="form-check-input field-checkbox" type="checkbox" name="fields" value="ausgezahlt_am" id="field_ausgezahlt_am">
<label class="form-check-label" for="field_ausgezahlt_am">Ausgezahlt am</label>
</div>
</div>
<!-- Payment Details -->
<div class="mb-3">
<h6 class="text-info mb-2">Überweisungsdetails</h6>
<div class="form-check form-check-sm">
<input class="form-check-input field-checkbox" type="checkbox" name="fields" value="empfaenger_iban" id="field_empfaenger_iban" checked>
<label class="form-check-label" for="field_empfaenger_iban">Empfänger IBAN</label>
</div>
<div class="form-check form-check-sm">
<input class="form-check-input field-checkbox" type="checkbox" name="fields" value="empfaenger_name" id="field_empfaenger_name" checked>
<label class="form-check-label" for="field_empfaenger_name">Empfänger Name</label>
</div>
<div class="form-check form-check-sm">
<input class="form-check-input field-checkbox" type="checkbox" name="fields" value="verwendungszweck" id="field_verwendungszweck">
<label class="form-check-label" for="field_verwendungszweck">Verwendungszweck</label>
</div>
</div>
<!-- Account Information -->
<div class="mb-3">
<h6 class="text-success mb-2">Kontoinformationen</h6>
<div class="form-check form-check-sm">
<input class="form-check-input field-checkbox" type="checkbox" name="fields" value="konto_name" id="field_konto_name">
<label class="form-check-label" for="field_konto_name">Konto</label>
</div>
<div class="form-check form-check-sm">
<input class="form-check-input field-checkbox" type="checkbox" name="fields" value="konto_bank" id="field_konto_bank">
<label class="form-check-label" for="field_konto_bank">Bank</label>
</div>
<div class="form-check form-check-sm">
<input class="form-check-input field-checkbox" type="checkbox" name="fields" value="konto_iban" id="field_konto_iban">
<label class="form-check-label" for="field_konto_iban">Konto IBAN</label>
</div>
</div>
<!-- Destinataer Personal Info -->
<div class="mb-3">
<h6 class="text-warning mb-2">Destinatär Details</h6>
<div class="form-check form-check-sm">
<input class="form-check-input field-checkbox" type="checkbox" name="fields" value="familienzweig" id="field_familienzweig">
<label class="form-check-label" for="field_familienzweig">Familienzweig</label>
</div>
<div class="form-check form-check-sm">
<input class="form-check-input field-checkbox" type="checkbox" name="fields" value="email" id="field_email">
<label class="form-check-label" for="field_email">E-Mail</label>
</div>
<div class="form-check form-check-sm">
<input class="form-check-input field-checkbox" type="checkbox" name="fields" value="telefon" id="field_telefon">
<label class="form-check-label" for="field_telefon">Telefon</label>
</div>
<div class="form-check form-check-sm">
<input class="form-check-input field-checkbox" type="checkbox" name="fields" value="adresse" id="field_adresse">
<label class="form-check-label" for="field_adresse">Adresse</label>
</div>
</div>
<!-- System Fields -->
<div class="mb-3">
<h6 class="text-secondary mb-2">System & Verlauf</h6>
<div class="form-check form-check-sm">
<input class="form-check-input field-checkbox" type="checkbox" name="fields" value="erstellt_am" id="field_erstellt_am">
<label class="form-check-label" for="field_erstellt_am">Erstellt am</label>
</div>
<div class="form-check form-check-sm">
<input class="form-check-input field-checkbox" type="checkbox" name="fields" value="ist_wiederkehrend" id="field_ist_wiederkehrend">
<label class="form-check-label" for="field_ist_wiederkehrend">Wiederkehrend</label>
</div>
</div>
</div>
</div>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Abbrechen</button>
<button type="submit" class="btn btn-primary">
<i class="fas fa-download me-2"></i>Exportieren
</button>
</div>
</form>
</div>
</div>
</div>
<!-- Statistics Cards -->
<div class="row mb-4">
<div class="col-md-3">
<div class="card bg-primary text-white">
<div class="card-body">
<h5 class="card-title">Gesamtbetrag</h5>
<h3 class="card-text">€{{ total_betrag|floatformat:2 }}</h3>
</div>
</div>
</div>
<div class="col-md-3">
<div class="card bg-success text-white">
<div class="card-body">
<h5 class="card-title">Durchschnitt</h5>
<h3 class="card-text">€{{ avg_betrag|floatformat:2 }}</h3>
</div>
</div>
</div>
<div class="col-md-3">
<div class="card bg-info text-white">
<div class="card-body">
<h5 class="card-title">Anzahl</h5>
<h3 class="card-text">{{ unterstuetzungen.count }}</h3>
</div>
</div>
</div>
</div>
<!-- Filters -->
<div class="card mb-4">
<div class="card-header">
<h6 class="mb-0">
<i class="fas fa-filter me-2"></i>Filter
</h6>
</div>
<div class="card-body">
<form method="get" class="row g-3">
<div class="col-md-2">
<label for="status" class="form-label">Status</label>
<select name="status" id="status" class="form-select">
<option value="">Alle Status</option>
{% for status in status_choices %}
<option value="{{ status.0 }}" {% if status_filter == status.0 %}selected{% endif %}>{{ status.1 }}</option>
{% endfor %}
</select>
</div>
<div class="col-md-3">
<label for="destinataer" class="form-label">Destinatär</label>
<select name="destinataer" id="destinataer" class="form-select">
<option value="">Alle Destinatäre</option>
{% for dest in destinataer %}
<option value="{{ dest.id }}" {% if filter_destinataer == dest.id|stringformat:"s" %}selected{% endif %}>{{ dest.get_full_name }}</option>
{% endfor %}
</select>
</div>
<div class="col-md-3 d-flex align-items-end">
<button type="submit" class="btn btn-primary me-2">
<i class="fas fa-search me-2"></i>Filtern
</button>
<a href="{% url 'stiftung:unterstuetzungen_all' %}" class="btn btn-outline-secondary">
<i class="fas fa-times me-2"></i>Zurücksetzen
</a>
</div>
</form>
</div>
</div>
<!-- Unterstützungen Table -->
<div class="card">
<div class="card-header">
<h6 class="mb-0">
<i class="fas fa-list me-2"></i>Unterstützungen
</h6>
</div>
<div class="card-body">
{% if unterstuetzungen %}
<div class="table-responsive">
<table class="table table-hover">
<thead>
<tr>
<th>
<div class="form-check">
<input class="form-check-input" type="checkbox" id="selectAll" onchange="toggleAllCheckboxes()">
</div>
</th>
<th>Destinatär</th>
<th>Betrag</th>
<th>Fällig am</th>
<th>Konto</th>
<th>Status</th>
<th>Beschreibung</th>
<th>Aktionen</th>
</tr>
</thead>
<tbody>
{% for unterstuetzung in unterstuetzungen %}
<tr>
<td>
<div class="form-check">
<input class="form-check-input entry-checkbox" type="checkbox" name="selected_entries" value="{{ unterstuetzung.id }}" onchange="updateSelectedCount()">
</div>
</td>
<td>
<a href="{% url 'stiftung:destinataer_detail' unterstuetzung.destinataer.pk %}">
{{ unterstuetzung.destinataer.get_full_name }}
</a>
</td>
<td>€{{ unterstuetzung.betrag|floatformat:2 }}</td>
<td>{{ unterstuetzung.faellig_am|date:"d.m.Y" }}</td>
<td>
<span class="badge bg-secondary">{{ unterstuetzung.konto }}</span>
</td>
<td>
{% if unterstuetzung.status == 'geplant' %}
<span class="badge bg-secondary">{{ unterstuetzung.get_status_display }}</span>
{% elif unterstuetzung.status == 'in_bearbeitung' %}
<span class="badge bg-warning">{{ unterstuetzung.get_status_display }}</span>
{% elif unterstuetzung.status == 'ausgezahlt' %}
<span class="badge bg-success">{{ unterstuetzung.get_status_display }}</span>
{% else %}
<span class="badge bg-danger">{{ unterstuetzung.get_status_display }}</span>
{% endif %}
</td>
<td>{{ unterstuetzung.beschreibung|truncatechars:40 }}</td>
<td>
<div class="btn-group" role="group">
<a href="{% url 'stiftung:unterstuetzung_detail' unterstuetzung.pk %}" class="btn btn-sm btn-outline-primary">
<i class="fas fa-eye"></i>
</a>
</div>
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
{% else %}
<div class="text-center py-5">
<i class="fas fa-hand-holding-usd fa-3x text-muted mb-3"></i>
<h5 class="text-muted">Keine Unterstützungen gefunden</h5>
<p class="text-muted">Erstellen Sie Ihre erste Unterstützung oder passen Sie die Filter an.</p>
<a href="{% url 'stiftung:unterstuetzung_create' %}" class="btn btn-primary">
<i class="fas fa-plus me-2"></i>Neue Unterstützung erstellen
</a>
</div>
{% endif %}
</div>
</div>
<script>
// Field selection functions
function selectAllFields() {
document.querySelectorAll('.field-checkbox').forEach(checkbox => {
checkbox.checked = true;
});
}
function selectDefaultFields() {
// Uncheck all first
document.querySelectorAll('.field-checkbox').forEach(checkbox => {
checkbox.checked = false;
});
// Check default fields
const defaultFields = [
'destinataer_name', 'betrag', 'faellig_am', 'status',
'empfaenger_iban', 'empfaenger_name', 'beschreibung'
];
defaultFields.forEach(fieldName => {
const checkbox = document.getElementById('field_' + fieldName);
if (checkbox) checkbox.checked = true;
});
}
// Bulk selection functions
function toggleAllCheckboxes() {
const selectAll = document.getElementById('selectAll');
const checkboxes = document.querySelectorAll('.entry-checkbox');
checkboxes.forEach(checkbox => {
checkbox.checked = selectAll.checked;
});
updateSelectedCount();
}
function updateSelectedCount() {
const selectedCheckboxes = document.querySelectorAll('.entry-checkbox:checked');
const count = selectedCheckboxes.length;
document.getElementById('selected-count').textContent = count;
// Update the select all checkbox state
const selectAll = document.getElementById('selectAll');
const allCheckboxes = document.querySelectorAll('.entry-checkbox');
if (count === 0) {
selectAll.indeterminate = false;
selectAll.checked = false;
} else if (count === allCheckboxes.length) {
selectAll.indeterminate = false;
selectAll.checked = true;
} else {
selectAll.indeterminate = true;
}
// Enable/disable the "selected only" radio button
const scopeSelected = document.getElementById('scope_selected');
const scopeSelectedLabel = scopeSelected.parentElement.querySelector('label');
if (count > 0) {
scopeSelected.disabled = false;
scopeSelectedLabel.classList.remove('text-muted');
} else {
scopeSelected.disabled = true;
scopeSelected.checked = false;
document.getElementById('scope_all').checked = true;
scopeSelectedLabel.classList.add('text-muted');
}
}
// Export form handling
document.getElementById('exportForm').addEventListener('submit', function(e) {
e.preventDefault();
const formData = new FormData(this);
const format = formData.get('format');
const scope = formData.get('scope');
// Build query string for GET request with format
const params = new URLSearchParams();
params.append('format', format);
// Add selected fields to the URL
formData.getAll('fields').forEach(field => {
params.append('fields', field);
});
// If scope is "selected", we need to POST the selected IDs
if (scope === 'selected') {
const selectedIds = [];
document.querySelectorAll('.entry-checkbox:checked').forEach(checkbox => {
selectedIds.push(checkbox.value);
});
if (selectedIds.length === 0) {
alert('Bitte wählen Sie mindestens einen Eintrag aus.');
return;
}
// Create a temporary form for POST request
const tempForm = document.createElement('form');
tempForm.method = 'POST';
tempForm.action = window.location.pathname + '?' + params.toString();
tempForm.style.display = 'none';
// Add CSRF token
const csrfToken = document.querySelector('[name=csrfmiddlewaretoken]').value;
const csrfInput = document.createElement('input');
csrfInput.type = 'hidden';
csrfInput.name = 'csrfmiddlewaretoken';
csrfInput.value = csrfToken;
tempForm.appendChild(csrfInput);
// Add selected IDs
selectedIds.forEach(id => {
const input = document.createElement('input');
input.type = 'hidden';
input.name = 'selected_entries';
input.value = id;
tempForm.appendChild(input);
});
// Add fields
formData.getAll('fields').forEach(field => {
const input = document.createElement('input');
input.type = 'hidden';
input.name = 'fields';
input.value = field;
tempForm.appendChild(input);
});
document.body.appendChild(tempForm);
tempForm.submit();
document.body.removeChild(tempForm);
} else {
// For "all" scope, use GET request
window.location.href = window.location.pathname + '?' + params.toString();
}
// Close modal
bootstrap.Modal.getInstance(document.getElementById('exportModal')).hide();
});
// Initialize counts on page load
document.addEventListener('DOMContentLoaded', function() {
updateSelectedCount();
});
</script>
{% endblock %}

View File

@@ -1,19 +1,228 @@
{% extends 'base.html' %}
{% block title %}Destinatärunterstützungen - Administration{% endblock %}
{% block content %}
<div class="d-flex justify-content-between align-items-center mb-4">
<h1 class="h3"><i class="fas fa-hand-holding-usd me-2"></i>Destinatärunterstützungen</h1>
<div class="btn-group">
<a class="btn btn-outline-primary" href="?format=csv"><i class="fas fa-file-csv me-2"></i>CSV exportieren</a>
<a class="btn btn-outline-danger" href="?format=pdf"><i class="fas fa-file-pdf me-2"></i>PDF exportieren</a>
<button type="button" class="btn btn-primary" data-bs-toggle="modal" data-bs-target="#exportModal">
<i class="fas fa-download me-2"></i>Exportieren
</button>
<a class="btn btn-outline-success" href="{% url 'stiftung:unterstuetzung_create' %}">
<i class="fas fa-plus me-2"></i>Neue Unterstützung
</a>
</div>
</div>
<!-- Export Modal -->
<div class="modal fade" id="exportModal" tabindex="-1">
<div class="modal-dialog modal-lg">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">Export Optionen</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<form id="exportForm" method="post" action="{% url 'stiftung:unterstuetzungen_list' %}">
{% csrf_token %}
<div class="modal-body">
<div class="row">
<div class="col-md-6">
<h6 class="fw-bold">Export Format</h6>
<div class="mb-3">
<div class="form-check">
<input class="form-check-input" type="radio" name="format" id="format_csv" value="csv" checked>
<label class="form-check-label" for="format_csv">
<i class="fas fa-file-csv me-2"></i>CSV (Excel-kompatibel)
</label>
</div>
<div class="form-check">
<input class="form-check-input" type="radio" name="format" id="format_pdf" value="pdf">
<label class="form-check-label" for="format_pdf">
<i class="fas fa-file-pdf me-2"></i>PDF (Tabelle)
</label>
</div>
</div>
<h6 class="fw-bold">Export Umfang</h6>
<div class="mb-3">
<div class="form-check">
<input class="form-check-input" type="radio" name="scope" id="scope_all" value="all" checked>
<label class="form-check-label" for="scope_all">
Alle Einträge (<span id="total-count">{{ unterstuetzungen|length }}</span>)
</label>
</div>
<div class="form-check">
<input class="form-check-input" type="radio" name="scope" id="scope_selected" value="selected">
<label class="form-check-label" for="scope_selected">
Nur ausgewählte Einträge (<span id="selected-count">0</span>)
</label>
</div>
</div>
</div>
<div class="col-md-6" id="field-selection" style="max-height: 400px; overflow-y: auto;">
<h6 class="fw-bold">Felder auswählen</h6>
<div class="mb-2">
<button type="button" class="btn btn-sm btn-outline-primary me-2" onclick="selectAllFields()">Alle auswählen</button>
<button type="button" class="btn btn-sm btn-outline-secondary" onclick="selectDefaultFields()">Standard</button>
</div>
<div class="field-groups">
<!-- Core Payment Fields -->
<div class="mb-3">
<h6 class="text-primary mb-2">Zahlungsdaten</h6>
<div class="form-check form-check-sm">
<input class="form-check-input field-checkbox" type="checkbox" name="fields" value="destinataer_name" id="field_destinataer_name" checked>
<label class="form-check-label" for="field_destinataer_name">Destinatär Name</label>
</div>
<div class="form-check form-check-sm">
<input class="form-check-input field-checkbox" type="checkbox" name="fields" value="betrag" id="field_betrag" checked>
<label class="form-check-label" for="field_betrag">Betrag (€)</label>
</div>
<div class="form-check form-check-sm">
<input class="form-check-input field-checkbox" type="checkbox" name="fields" value="faellig_am" id="field_faellig_am" checked>
<label class="form-check-label" for="field_faellig_am">Fällig am</label>
</div>
<div class="form-check form-check-sm">
<input class="form-check-input field-checkbox" type="checkbox" name="fields" value="status" id="field_status" checked>
<label class="form-check-label" for="field_status">Status</label>
</div>
<div class="form-check form-check-sm">
<input class="form-check-input field-checkbox" type="checkbox" name="fields" value="beschreibung" id="field_beschreibung" checked>
<label class="form-check-label" for="field_beschreibung">Beschreibung</label>
</div>
<div class="form-check form-check-sm">
<input class="form-check-input field-checkbox" type="checkbox" name="fields" value="ausgezahlt_am" id="field_ausgezahlt_am">
<label class="form-check-label" for="field_ausgezahlt_am">Ausgezahlt am</label>
</div>
</div>
<!-- Payment Details -->
<div class="mb-3">
<h6 class="text-info mb-2">Überweisungsdetails</h6>
<div class="form-check form-check-sm">
<input class="form-check-input field-checkbox" type="checkbox" name="fields" value="empfaenger_iban" id="field_empfaenger_iban" checked>
<label class="form-check-label" for="field_empfaenger_iban">Empfänger IBAN</label>
</div>
<div class="form-check form-check-sm">
<input class="form-check-input field-checkbox" type="checkbox" name="fields" value="empfaenger_name" id="field_empfaenger_name" checked>
<label class="form-check-label" for="field_empfaenger_name">Empfänger Name</label>
</div>
<div class="form-check form-check-sm">
<input class="form-check-input field-checkbox" type="checkbox" name="fields" value="verwendungszweck" id="field_verwendungszweck">
<label class="form-check-label" for="field_verwendungszweck">Verwendungszweck</label>
</div>
</div>
<!-- Account Information -->
<div class="mb-3">
<h6 class="text-success mb-2">Kontoinformationen</h6>
<div class="form-check form-check-sm">
<input class="form-check-input field-checkbox" type="checkbox" name="fields" value="konto_name" id="field_konto_name">
<label class="form-check-label" for="field_konto_name">Konto</label>
</div>
<div class="form-check form-check-sm">
<input class="form-check-input field-checkbox" type="checkbox" name="fields" value="konto_bank" id="field_konto_bank">
<label class="form-check-label" for="field_konto_bank">Bank</label>
</div>
<div class="form-check form-check-sm">
<input class="form-check-input field-checkbox" type="checkbox" name="fields" value="konto_iban" id="field_konto_iban">
<label class="form-check-label" for="field_konto_iban">Konto IBAN</label>
</div>
</div>
<!-- Destinataer Personal Info -->
<div class="mb-3">
<h6 class="text-warning mb-2">Destinatär Details</h6>
<div class="form-check form-check-sm">
<input class="form-check-input field-checkbox" type="checkbox" name="fields" value="familienzweig" id="field_familienzweig">
<label class="form-check-label" for="field_familienzweig">Familienzweig</label>
</div>
<div class="form-check form-check-sm">
<input class="form-check-input field-checkbox" type="checkbox" name="fields" value="geburtsdatum" id="field_geburtsdatum">
<label class="form-check-label" for="field_geburtsdatum">Geburtsdatum</label>
</div>
<div class="form-check form-check-sm">
<input class="form-check-input field-checkbox" type="checkbox" name="fields" value="email" id="field_email">
<label class="form-check-label" for="field_email">E-Mail</label>
</div>
<div class="form-check form-check-sm">
<input class="form-check-input field-checkbox" type="checkbox" name="fields" value="telefon" id="field_telefon">
<label class="form-check-label" for="field_telefon">Telefon</label>
</div>
<div class="form-check form-check-sm">
<input class="form-check-input field-checkbox" type="checkbox" name="fields" value="adresse" id="field_adresse">
<label class="form-check-label" for="field_adresse">Adresse</label>
</div>
</div>
<!-- Financial Information -->
<div class="mb-3">
<h6 class="text-danger mb-2">Finanzinformationen</h6>
<div class="form-check form-check-sm">
<input class="form-check-input field-checkbox" type="checkbox" name="fields" value="jaehrliches_einkommen" id="field_jaehrliches_einkommen">
<label class="form-check-label" for="field_jaehrliches_einkommen">Jährliches Einkommen</label>
</div>
<div class="form-check form-check-sm">
<input class="form-check-input field-checkbox" type="checkbox" name="fields" value="monatliche_bezuege" id="field_monatliche_bezuege">
<label class="form-check-label" for="field_monatliche_bezuege">Monatliche Bezüge</label>
</div>
<div class="form-check form-check-sm">
<input class="form-check-input field-checkbox" type="checkbox" name="fields" value="haushaltsgroesse" id="field_haushaltsgroesse">
<label class="form-check-label" for="field_haushaltsgroesse">Haushaltsgröße</label>
</div>
<div class="form-check form-check-sm">
<input class="form-check-input field-checkbox" type="checkbox" name="fields" value="vermoegen" id="field_vermoegen">
<label class="form-check-label" for="field_vermoegen">Vermögen</label>
</div>
</div>
<!-- System Fields -->
<div class="mb-3">
<h6 class="text-secondary mb-2">System & Verlauf</h6>
<div class="form-check form-check-sm">
<input class="form-check-input field-checkbox" type="checkbox" name="fields" value="erstellt_am" id="field_erstellt_am">
<label class="form-check-label" for="field_erstellt_am">Erstellt am</label>
</div>
<div class="form-check form-check-sm">
<input class="form-check-input field-checkbox" type="checkbox" name="fields" value="ausgezahlt_von" id="field_ausgezahlt_von">
<label class="form-check-label" for="field_ausgezahlt_von">Ausgezahlt von</label>
</div>
<div class="form-check form-check-sm">
<input class="form-check-input field-checkbox" type="checkbox" name="fields" value="ist_wiederkehrend" id="field_ist_wiederkehrend">
<label class="form-check-label" for="field_ist_wiederkehrend">Wiederkehrend</label>
</div>
<div class="form-check form-check-sm">
<input class="form-check-input field-checkbox" type="checkbox" name="fields" value="id" id="field_id">
<label class="form-check-label" for="field_id">ID</label>
</div>
</div>
</div>
</div>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Abbrechen</button>
<button type="submit" class="btn btn-primary">
<i class="fas fa-download me-2"></i>Exportieren
</button>
</div>
</form>
</div>
</div>
</div>
<div class="card shadow">
<div class="card-body">
<div class="table-responsive">
<table class="table table-hover">
<thead class="table-light">
<tr>
<th>
<div class="form-check">
<input class="form-check-input" type="checkbox" id="selectAll" onchange="toggleAllCheckboxes()">
</div>
</th>
<th>Destinatär</th>
<th>Bank</th>
<th>IBAN</th>
@@ -28,13 +237,27 @@
<tbody>
{% for u in unterstuetzungen %}
<tr>
<td>
<div class="form-check">
<input class="form-check-input entry-checkbox" type="checkbox" name="selected_entries" value="{{ u.id }}" onchange="updateSelectedCount()">
</div>
</td>
<td>{{ u.destinataer.get_full_name }}</td>
<td>{{ u.konto.bank_name }}</td>
<td>{{ u.konto.iban }}</td>
<td>€{{ u.betrag|floatformat:2 }}</td>
<td>{{ u.konto }}</td>
<td>{{ u.faellig_am|date:"d.m.Y" }}</td>
<td><span class="badge bg-secondary">{{ u.get_status_display }}</span></td>
<td>
<span class="badge
{% if u.status == 'ausgezahlt' %}bg-success
{% elif u.status == 'faellig' %}bg-warning
{% elif u.status == 'in_bearbeitung' %}bg-info
{% elif u.status == 'storniert' %}bg-danger
{% else %}bg-secondary{% endif %}">
{{ u.get_status_display }}
</span>
</td>
<td>{{ u.beschreibung }}</td>
<td>
<div class="btn-group btn-group-sm" role="group">
@@ -44,12 +267,165 @@
</td>
</tr>
{% empty %}
<tr><td colspan="6" class="text-center text-muted">Keine Einträge</td></tr>
<tr><td colspan="10" class="text-center text-muted">Keine Einträge</td></tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
</div>
<script>
// Field selection functions
function selectAllFields() {
document.querySelectorAll('.field-checkbox').forEach(checkbox => {
checkbox.checked = true;
});
}
function selectDefaultFields() {
// Uncheck all first
document.querySelectorAll('.field-checkbox').forEach(checkbox => {
checkbox.checked = false;
});
// Check default fields
const defaultFields = [
'destinataer_name', 'betrag', 'faellig_am', 'status',
'empfaenger_iban', 'empfaenger_name', 'beschreibung'
];
defaultFields.forEach(fieldName => {
const checkbox = document.getElementById('field_' + fieldName);
if (checkbox) checkbox.checked = true;
});
}
// Bulk selection functions
function toggleAllCheckboxes() {
const selectAll = document.getElementById('selectAll');
const checkboxes = document.querySelectorAll('.entry-checkbox');
checkboxes.forEach(checkbox => {
checkbox.checked = selectAll.checked;
});
updateSelectedCount();
}
function updateSelectedCount() {
const selectedCheckboxes = document.querySelectorAll('.entry-checkbox:checked');
const count = selectedCheckboxes.length;
document.getElementById('selected-count').textContent = count;
// Update the select all checkbox state
const selectAll = document.getElementById('selectAll');
const allCheckboxes = document.querySelectorAll('.entry-checkbox');
if (count === 0) {
selectAll.indeterminate = false;
selectAll.checked = false;
} else if (count === allCheckboxes.length) {
selectAll.indeterminate = false;
selectAll.checked = true;
} else {
selectAll.indeterminate = true;
}
// Enable/disable the "selected only" radio button
const scopeSelected = document.getElementById('scope_selected');
const scopeSelectedLabel = scopeSelected.parentElement.querySelector('label');
if (count > 0) {
scopeSelected.disabled = false;
scopeSelectedLabel.classList.remove('text-muted');
} else {
scopeSelected.disabled = true;
scopeSelected.checked = false;
document.getElementById('scope_all').checked = true;
scopeSelectedLabel.classList.add('text-muted');
}
}
// Export form handling
document.getElementById('exportForm').addEventListener('submit', function(e) {
e.preventDefault();
const formData = new FormData(this);
const format = formData.get('format');
const scope = formData.get('scope');
// Build query string for GET request with format
const params = new URLSearchParams();
params.append('format', format);
// Add selected fields to the URL
formData.getAll('fields').forEach(field => {
params.append('fields', field);
});
// If scope is "selected", we need to POST the selected IDs
if (scope === 'selected') {
const selectedIds = [];
document.querySelectorAll('.entry-checkbox:checked').forEach(checkbox => {
selectedIds.push(checkbox.value);
});
if (selectedIds.length === 0) {
alert('Bitte wählen Sie mindestens einen Eintrag aus.');
return;
}
// Create a temporary form for POST request
const tempForm = document.createElement('form');
tempForm.method = 'POST';
tempForm.action = window.location.pathname + '?' + params.toString();
tempForm.style.display = 'none';
// Add CSRF token
const csrfToken = document.querySelector('[name=csrfmiddlewaretoken]').value;
const csrfInput = document.createElement('input');
csrfInput.type = 'hidden';
csrfInput.name = 'csrfmiddlewaretoken';
csrfInput.value = csrfToken;
tempForm.appendChild(csrfInput);
// Add selected IDs
selectedIds.forEach(id => {
const input = document.createElement('input');
input.type = 'hidden';
input.name = 'selected_entries';
input.value = id;
tempForm.appendChild(input);
});
// Add fields
formData.getAll('fields').forEach(field => {
const input = document.createElement('input');
input.type = 'hidden';
input.name = 'fields';
input.value = field;
tempForm.appendChild(input);
});
document.body.appendChild(tempForm);
tempForm.submit();
document.body.removeChild(tempForm);
} else {
// For "all" scope, use GET request
window.location.href = window.location.pathname + '?' + params.toString();
}
// Close modal
bootstrap.Modal.getInstance(document.getElementById('exportModal')).hide();
});
// Initialize counts on page load
document.addEventListener('DOMContentLoaded', function() {
updateSelectedCount();
});
</script>
{% endblock %}

View File

@@ -1,304 +0,0 @@
{% extends 'base.html' %}
{% load static %}
{% block title %}{{ verpachtung }} - Verpachtung - Stiftungsverwaltung{% endblock %}
{% block content %}
<div class="row">
<div class="col-12">
<!-- Header -->
<div class="d-flex justify-content-between align-items-center mb-4">
<h1 class="h3">
<i class="fas fa-handshake text-primary me-2"></i>
Verpachtung: {{ verpachtung }}
</h1>
<div>
<a href="{% url 'stiftung:verpachtung_update' pk=verpachtung.pk %}" class="btn btn-warning me-2">
<i class="fas fa-edit me-2"></i>Bearbeiten
</a>
<a href="{% url 'stiftung:verpachtung_export' pk=verpachtung.pk %}" class="btn btn-success me-2">
<i class="fas fa-download me-2"></i>Export
</a>
<a href="{% url 'stiftung:verpachtung_list' %}" class="btn btn-outline-secondary">
<i class="fas fa-arrow-left me-2"></i>Zurück zur Liste
</a>
</div>
</div>
<div class="row">
<!-- Main Content -->
<div class="col-lg-8">
<!-- Verpachtungs-Informationen -->
<div class="card shadow mb-4">
<div class="card-header bg-primary text-white">
<h5 class="card-title mb-0">
<i class="fas fa-info-circle me-2"></i>Verpachtungs-Informationen
</h5>
</div>
<div class="card-body">
<div class="row">
<div class="col-md-6">
<p><strong>Status:</strong>
{% if verpachtung.status == 'aktiv' %}
<span class="badge bg-success">Aktiv</span>
{% elif verpachtung.status == 'beendet' %}
<span class="badge bg-secondary">Beendet</span>
{% elif verpachtung.status == 'gekündigt' %}
<span class="badge bg-danger">Gekündigt</span>
{% else %}
<span class="badge bg-warning">{{ verpachtung.get_status_display }}</span>
{% endif %}
</p>
<p><strong>Pachtbeginn:</strong> {{ verpachtung.pachtbeginn|date:"d.m.Y" }}</p>
<p><strong>Pachtende:</strong>
{% if verpachtung.pachtende %}
{{ verpachtung.pachtende|date:"d.m.Y" }}
{% else %}
<span class="text-muted">Unbefristet</span>
{% endif %}
</p>
</div>
<div class="col-md-6">
<p><strong>Verpachtete Fläche:</strong> {{ verpachtung.verpachtete_flaeche|floatformat:2 }} qm
<small class="text-muted">({{ verpachtung.verpachtete_flaeche_hektar|floatformat:2 }} ha)</small></p>
<p><strong>Pachtzins (jährlich):</strong> €{{ verpachtung.pachtzins_jaehrlich|floatformat:2 }}</p>
<p><strong>Kündigungsfrist:</strong> {{ verpachtung.kuendigungsfrist }} Monate</p>
</div>
</div>
</div>
</div>
<!-- Pächter-Informationen -->
<div class="card shadow mb-4">
<div class="card-header bg-info text-white">
<h5 class="card-title mb-0">
<i class="fas fa-user-tie me-2"></i>Pächter-Informationen
</h5>
</div>
<div class="card-body">
<div class="row">
<div class="col-md-6">
<p><strong>Name:</strong>
<a href="{% url 'stiftung:paechter_detail' pk=verpachtung.paechter.pk %}">
{{ verpachtung.paechter.get_full_name }}
</a>
</p>
<p><strong>Pachtnummer:</strong>
{% if verpachtung.paechter.pachtnummer %}
{{ verpachtung.paechter.pachtnummer }}
{% else %}
<span class="text-muted">Nicht vergeben</span>
{% endif %}
</p>
<p><strong>Landwirtschaftliche Ausbildung:</strong>
{% if verpachtung.paechter.landwirtschaftliche_ausbildung %}
<span class="badge bg-success">Ja</span>
{% else %}
<span class="badge bg-warning">Nein</span>
{% endif %}
</p>
</div>
<div class="col-md-6">
<p><strong>Berufserfahrung:</strong>
{% if verpachtung.paechter.berufserfahrung_jahre %}
{{ verpachtung.paechter.berufserfahrung_jahre }} Jahre
{% else %}
<span class="text-muted">Nicht angegeben</span>
{% endif %}
</p>
<p><strong>Kontakt:</strong>
{% if verpachtung.paechter.telefon %}
<i class="fas fa-phone me-1"></i>{{ verpachtung.paechter.telefon }}<br>
{% endif %}
{% if verpachtung.paechter.email %}
<i class="fas fa-envelope me-1"></i>{{ verpachtung.paechter.email }}
{% endif %}
</p>
</div>
</div>
</div>
</div>
<!-- Verknüpfte Dokumente -->
<div class="card shadow mb-4">
<div class="card-header bg-success text-white d-flex justify-content-between align-items-center">
<h5 class="card-title mb-0">
<i class="fas fa-file-alt me-2"></i>Verknüpfte Dokumente
</h5>
<a href="{% url 'stiftung:dokument_management' %}" class="btn btn-light btn-sm">
<i class="fas fa-plus me-1"></i>Dokument verknüpfen
</a>
</div>
<div class="card-body">
{% if verknuepfte_dokumente %}
<div class="table-responsive">
<table class="table table-hover">
<thead>
<tr>
<th>Dokument</th>
<th>Kontext</th>
<th>Beschreibung</th>
<th>Aktionen</th>
</tr>
</thead>
<tbody>
{% for dokument in verknuepfte_dokumente %}
<tr>
<td>
<strong>{{ dokument.titel }}</strong>
<br>
<small class="text-muted">ID: {{ dokument.paperless_document_id }}</small>
</td>
<td>
<span class="badge bg-secondary">{{ dokument.get_kontext_display }}</span>
</td>
<td>
{% if dokument.beschreibung %}
{{ dokument.beschreibung|truncatewords:10 }}
{% else %}
<span class="text-muted">Keine Beschreibung</span>
{% endif %}
</td>
<td>
<div class="btn-group" role="group">
<a href="{{ dokument.get_paperless_url }}" target="_blank" class="btn btn-sm btn-outline-primary" title="In Paperless öffnen">
<i class="fas fa-external-link-alt"></i>
</a>
<a href="{{ dokument.get_paperless_thumbnail_url }}" target="_blank" class="btn btn-sm btn-outline-info" title="Thumbnail anzeigen">
<i class="fas fa-image"></i>
</a>
<a href="{% url 'stiftung:dokument_update' dokument.pk %}" class="btn btn-sm btn-outline-warning" title="Bearbeiten">
<i class="fas fa-edit"></i>
</a>
<a href="{% url 'stiftung:dokument_delete' dokument.pk %}" class="btn btn-sm btn-outline-danger" title="Verknüpfung löschen">
<i class="fas fa-unlink"></i>
</a>
</div>
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
{% else %}
<div class="text-center py-4">
<i class="fas fa-file-alt fa-3x text-muted mb-3"></i>
<h5 class="text-muted">Keine Dokumente verknüpft</h5>
<p class="text-muted">Verknüpfen Sie Dokumente aus Paperless mit dieser Verpachtung.</p>
<a href="{% url 'stiftung:dokument_management' %}" class="btn btn-success">
<i class="fas fa-plus me-2"></i>Erstes Dokument verknüpfen
</a>
</div>
{% endif %}
</div>
</div>
<!-- Länderei-Informationen -->
<div class="card shadow mb-4">
<div class="card-header bg-success text-white">
<h5 class="card-title mb-0">
<i class="fas fa-map me-2"></i>Länderei-Informationen
</h5>
</div>
<div class="card-body">
<div class="row">
<div class="col-md-6">
<p><strong>Lfd. Nr.:</strong>
<a href="{% url 'stiftung:land_detail' pk=verpachtung.land.pk %}">
{{ verpachtung.land.lfd_nr }}
</a>
</p>
<p><strong>Gemeinde:</strong> {{ verpachtung.land.gemeinde }}</p>
<p><strong>Gemarkung:</strong> {{ verpachtung.land.gemarkung }}</p>
</div>
<div class="col-md-6">
<p><strong>Flur:</strong> {{ verpachtung.land.flur }}</p>
<p><strong>Flurstück:</strong> {{ verpachtung.land.flurstueck }}</p>
<p><strong>Gesamtgröße:</strong> {{ verpachtung.land.groesse_qm|floatformat:2 }} qm</p>
</div>
</div>
</div>
</div>
<!-- Notizen -->
{% if verpachtung.notizen %}
<div class="card shadow mb-4">
<div class="card-header bg-warning text-dark">
<h5 class="card-title mb-0">
<i class="fas fa-sticky-note me-2"></i>Notizen
</h5>
</div>
<div class="card-body">
{{ verpachtung.notizen|linebreaks }}
</div>
</div>
{% endif %}
</div>
<!-- Sidebar -->
<div class="col-lg-4">
<!-- Statistiken -->
<div class="card shadow mb-4">
<div class="card-header bg-primary text-white">
<h6 class="card-title mb-0">
<i class="fas fa-chart-bar me-2"></i>Statistiken
</h6>
</div>
<div class="card-body">
<p><strong>Verpachtungsgrad:</strong>
{% if verpachtung.land.groesse_qm > 0 %}
{% widthratio verpachtung.verpachtete_flaeche verpachtung.land.groesse_qm 100 %}%
{% else %}
0%
{% endif %}
</p>
<p><strong>Flächenanteil:</strong>
{% if verpachtung.land.groesse_qm > 0 %}
{% widthratio verpachtung.verpachtete_flaeche verpachtung.land.groesse_qm 100 %}%
{% else %}
0%
{% endif %}
</p>
</div>
</div>
<!-- Aktionen -->
<div class="card shadow mb-4">
<div class="card-header bg-success text-white">
<h6 class="card-title mb-0">
<i class="fas fa-tools me-2"></i>Aktionen
</h6>
</div>
<div class="card-body">
<a href="{% url 'stiftung:verpachtung_update' pk=verpachtung.pk %}" class="btn btn-warning btn-sm w-100 mb-2">
<i class="fas fa-edit me-2"></i>Bearbeiten
</a>
<a href="{% url 'stiftung:verpachtung_export' pk=verpachtung.pk %}" class="btn btn-success btn-sm w-100 mb-2">
<i class="fas fa-download me-2"></i>Export
</a>
<a href="{% url 'stiftung:verpachtung_delete' pk=verpachtung.pk %}" class="btn btn-danger btn-sm w-100">
<i class="fas fa-trash me-2"></i>Löschen
</a>
</div>
</div>
<!-- Informationen -->
<div class="card shadow">
<div class="card-header bg-info text-white">
<h6 class="card-title mb-0">
<i class="fas fa-info-circle me-2"></i>Informationen
</h6>
</div>
<div class="card-body">
<p><strong>ID:</strong> <small class="text-muted">{{ verpachtung.pk }}</small></p>
<p><strong>Erstellt:</strong> <small class="text-muted">{{ verpachtung.created_at|date:"d.m.Y H:i" }}</small></p>
<p><strong>Zuletzt aktualisiert:</strong> <small class="text-muted">{{ verpachtung.updated_at|date:"d.m.Y H:i" }}</small></p>
</div>
</div>
</div>
</div>
</div>
</div>
{% endblock %}

View File

@@ -1,334 +0,0 @@
{% extends 'base.html' %}
{% load static %}
{% block title %}{{ title }} - Stiftungsverwaltung{% endblock %}
{% block content %}
<div class="row">
<div class="col-12">
<!-- Header -->
<div class="d-flex justify-content-between align-items-center mb-4">
<h1 class="h3">
<i class="fas fa-handshake text-primary me-2"></i>
{{ title }}
</h1>
<div>
<a href="{% url 'stiftung:verpachtung_list' %}" class="btn btn-outline-secondary">
<i class="fas fa-arrow-left me-2"></i>Zurück zur Liste
</a>
</div>
</div>
<!-- Form -->
<div class="card shadow">
<div class="card-header py-3">
<h6 class="m-0 font-weight-bold text-primary">
<i class="fas fa-edit me-2"></i>Verpachtungsdaten eingeben
</h6>
</div>
<div class="card-body">
<form method="post" novalidate>
{% csrf_token %}
<!-- Error Messages -->
{% if form.non_field_errors %}
<div class="alert alert-danger">
<ul class="mb-0">
{% for error in form.non_field_errors %}
<li>{{ error }}</li>
{% endfor %}
</ul>
</div>
{% endif %}
<!-- Vertragsdaten -->
<div class="row mb-4">
<div class="col-12">
<h5 class="text-primary mb-3">
<i class="fas fa-file-contract me-2"></i>Vertragsdaten
</h5>
</div>
<div class="col-md-6">
<div class="mb-3">
<label for="{{ form.vertragsnummer.id_for_label }}" class="form-label">
{{ form.vertragsnummer.label }} *
</label>
{{ form.vertragsnummer }}
{% if form.vertragsnummer.errors %}
<div class="invalid-feedback d-block">
{% for error in form.vertragsnummer.errors %}
{{ error }}
{% endfor %}
</div>
{% endif %}
</div>
</div>
<div class="col-md-6">
<div class="mb-3">
<label for="{{ form.status.id_for_label }}" class="form-label">
{{ form.status.label }} *
</label>
{{ form.status }}
{% if form.status.errors %}
<div class="invalid-feedback d-block">
{% for error in form.status.errors %}
{{ error }}
{% endfor %}
</div>
{% endif %}
</div>
</div>
</div>
<!-- Länderei und Pächter -->
<div class="row mb-4">
<div class="col-12">
<h5 class="text-primary mb-3">
<i class="fas fa-map me-2"></i>Länderei und Pächter
</h5>
</div>
<div class="col-md-6">
<div class="mb-3">
<label for="{{ form.land.id_for_label }}" class="form-label">
{{ form.land.label }} *
</label>
{{ form.land }}
{% if form.land.errors %}
<div class="invalid-feedback d-block">
{% for error in form.land.errors %}
{{ error }}
{% endfor %}
</div>
{% endif %}
</div>
</div>
<div class="col-md-6">
<div class="mb-3">
<label for="{{ form.paechter.id_for_label }}" class="form-label">
{{ form.paechter.label }} *
</label>
{{ form.paechter }}
{% if form.paechter.errors %}
<div class="invalid-feedback d-block">
{% for error in form.paechter.errors %}
{{ error }}
{% endfor %}
</div>
{% endif %}
</div>
</div>
</div>
<!-- Zeitraum -->
<div class="row mb-4">
<div class="col-12">
<h5 class="text-primary mb-3">
<i class="fas fa-calendar me-2"></i>Zeitraum
</h5>
</div>
<div class="col-md-4">
<div class="mb-3">
<label for="{{ form.pachtbeginn.id_for_label }}" class="form-label">
{{ form.pachtbeginn.label }} *
</label>
<input type="date"
name="{{ form.pachtbeginn.name }}"
value="{% if form.pachtbeginn.value %}{{ form.pachtbeginn.value|date:'Y-m-d' }}{% endif %}"
class="form-control"
id="{{ form.pachtbeginn.id_for_label }}">
{% if form.pachtbeginn.errors %}
<div class="invalid-feedback d-block">
{% for error in form.pachtbeginn.errors %}
{{ error }}
{% endfor %}
</div>
{% endif %}
</div>
</div>
<div class="col-md-4">
<div class="mb-3">
<label for="{{ form.pachtende.id_for_label }}" class="form-label">
{{ form.pachtende.label }} *
</label>
<input type="date"
name="{{ form.pachtende.name }}"
value="{% if form.pachtende.value %}{{ form.pachtende.value|date:'Y-m-d' }}{% endif %}"
class="form-control"
id="{{ form.pachtende.id_for_label }}">
{% if form.pachtende.errors %}
<div class="invalid-feedback d-block">
{% for error in form.pachtende.errors %}
{{ error }}
{% endfor %}
</div>
{% endif %}
</div>
</div>
<div class="col-md-4">
<div class="mb-3">
<label for="{{ form.verlaengerung.id_for_label }}" class="form-label">
{{ form.verlaengerung.label }}
</label>
<input type="date"
name="{{ form.verlaengerung.name }}"
value="{% if form.verlaengerung.value %}{{ form.verlaengerung.value|date:'Y-m-d' }}{% endif %}"
class="form-control"
id="{{ form.verlaengerung.id_for_label }}">
{% if form.verlaengerung.errors %}
<div class="invalid-feedback d-block">
{% for error in form.verlaengerung.errors %}
{{ error }}
{% endfor %}
</div>
{% endif %}
</div>
</div>
</div>
<!-- Finanzielle Bedingungen -->
<div class="row mb-4">
<div class="col-12">
<h5 class="text-primary mb-3">
<i class="fas fa-euro-sign me-2"></i>Finanzielle Bedingungen
</h5>
</div>
<div class="col-md-4">
<div class="mb-3">
<label for="{{ form.pachtzins_pro_qm.id_for_label }}" class="form-label">
{{ form.pachtzins_pro_qm.label }} *
</label>
{{ form.pachtzins_pro_qm }}
{% if form.pachtzins_pro_qm.errors %}
<div class="invalid-feedback d-block">
{% for error in form.pachtzins_pro_qm.errors %}
{{ error }}
{% endfor %}
</div>
{% endif %}
</div>
</div>
<div class="col-md-4">
<div class="mb-3">
<label for="{{ form.pachtzins_jaehrlich.id_for_label }}" class="form-label">
{{ form.pachtzins_jaehrlich.label }} *
</label>
{{ form.pachtzins_jaehrlich }}
{% if form.pachtzins_jaehrlich.errors %}
<div class="invalid-feedback d-block">
{% for error in form.pachtzins_jaehrlich.errors %}
{{ error }}
{% endfor %}
</div>
{% endif %}
</div>
</div>
<div class="col-md-4">
<div class="mb-3">
<label for="{{ form.verpachtete_flaeche.id_for_label }}" class="form-label">
{{ form.verpachtete_flaeche.label }} *
</label>
{{ form.verpachtete_flaeche }}
{% if form.verpachtete_flaeche.errors %}
<div class="invalid-feedback d-block">
{% for error in form.verpachtete_flaeche.errors %}
{{ error }}
{% endfor %}
</div>
{% endif %}
</div>
</div>
</div>
<!-- Dokumentation -->
<div class="row mb-4">
<div class="col-12">
<h5 class="text-primary mb-3">
<i class="fas fa-file-alt me-2"></i>Dokumentation
</h5>
</div>
<div class="col-md-6">
<div class="mb-3">
<label for="{{ form.verwendungsnachweis.id_for_label }}" class="form-label">
{{ form.verwendungsnachweis.label }}
</label>
{{ form.verwendungsnachweis }}
{% if form.verwendungsnachweis.errors %}
<div class="invalid-feedback d-block">
{% for error in form.verwendungsnachweis.errors %}
{{ error }}
{% endfor %}
</div>
{% endif %}
</div>
</div>
<div class="col-md-6">
<div class="mb-3">
<label for="{{ form.bemerkungen.id_for_label }}" class="form-label">
{{ form.bemerkungen.label }}
</label>
{{ form.bemerkungen }}
{% if form.bemerkungen.errors %}
<div class="invalid-feedback d-block">
{% for error in form.bemerkungen.errors %}
{{ error }}
{% endfor %}
</div>
{% endif %}
</div>
</div>
</div>
<!-- Submit Buttons -->
<div class="row">
<div class="col-12">
<hr class="my-4">
<div class="d-flex justify-content-between">
<a href="{% url 'stiftung:verpachtung_list' %}" class="btn btn-secondary">
<i class="fas fa-times me-2"></i>Abbrechen
</a>
<button type="submit" class="btn btn-success">
<i class="fas fa-save me-2"></i>
{% if verpachtung %}Aktualisieren{% else %}Erstellen{% endif %}
</button>
</div>
</div>
</div>
</form>
</div>
</div>
</div>
</div>
{% endblock %}
{% block extra_js %}
<script>
// Auto-calculate annual rent from area and per-square-meter rate
function calculateAnnualRent() {
const area = parseFloat(document.getElementById('{{ form.verpachtete_flaeche.id_for_label }}').value) || 0;
const ratePerSqm = parseFloat(document.getElementById('{{ form.pachtzins_pro_qm.id_for_label }}').value) || 0;
if (area > 0 && ratePerSqm > 0) {
const annualRent = area * ratePerSqm;
document.getElementById('{{ form.pachtzins_jaehrlich.id_for_label }}').value = annualRent.toFixed(2);
}
}
// Auto-calculate per-square-meter rate from annual rent and area
function calculateRatePerSqm() {
const area = parseFloat(document.getElementById('{{ form.verpachtete_flaeche.id_for_label }}').value) || 0;
const annualRent = parseFloat(document.getElementById('{{ form.pachtzins_jaehrlich.id_for_label }}').value) || 0;
if (area > 0 && annualRent > 0) {
const ratePerSqm = annualRent / area;
document.getElementById('{{ form.pachtzins_pro_qm.id_for_label }}').value = ratePerSqm.toFixed(4);
}
}
// Add event listeners
document.getElementById('{{ form.verpachtete_flaeche.id_for_label }}').addEventListener('input', calculateAnnualRent);
document.getElementById('{{ form.pachtzins_pro_qm.id_for_label }}').addEventListener('input', calculateAnnualRent);
document.getElementById('{{ form.pachtzins_jaehrlich.id_for_label }}').addEventListener('input', calculateRatePerSqm);
</script>
{% endblock %}

View File

@@ -1,238 +0,0 @@
{% extends 'base.html' %}
{% load static %}
{% block title %}Verpachtungen - Stiftungsverwaltung{% endblock %}
{% block content %}
<div class="row">
<div class="col-12">
<!-- Header -->
<div class="d-flex justify-content-between align-items-center mb-4">
<h1 class="h3">
<i class="fas fa-handshake text-primary me-2"></i>Verpachtungen
</h1>
<a href="{% url 'stiftung:verpachtung_create' %}" class="btn btn-primary">
<i class="fas fa-plus me-2"></i>Neue Verpachtung
</a>
</div>
<!-- Statistics Cards -->
<div class="row mb-4">
<div class="col-md-3">
<div class="card bg-primary text-white">
<div class="card-body">
<h5 class="card-title">Aktive Verpachtungen</h5>
<h3 class="card-text">{{ aktive_verpachtungen|default:"0" }}</h3>
</div>
</div>
</div>
<div class="col-md-3">
<div class="card bg-success text-white">
<div class="card-body">
<h5 class="card-title">Gesamtfläche</h5>
<h3 class="card-text">{{ gesamt_flaeche|default:"0"|floatformat:0 }} qm</h3>
</div>
</div>
</div>
<div class="col-md-3">
<div class="card bg-info text-white">
<div class="card-body">
<h5 class="card-title">Jährlicher Pachtzins</h5>
<h3 class="card-text">€{{ jaehrlicher_pachtzins|default:"0"|floatformat:0 }}</h3>
</div>
</div>
</div>
<div class="col-md-3">
<div class="card bg-warning text-white">
<div class="card-body">
<h5 class="card-title">Anzahl</h5>
<h3 class="card-text">{{ page_obj.paginator.count|default:"0" }}</h3>
</div>
</div>
</div>
</div>
<!-- Filters -->
<div class="card mb-4">
<div class="card-header">
<h6 class="mb-0">
<i class="fas fa-filter me-2"></i>Filter
</h6>
</div>
<div class="card-body">
<form method="get" class="row g-3">
<div class="col-md-3">
<label for="search" class="form-label">Suche</label>
<input type="text" name="search" id="search" class="form-control" value="{{ search_query }}" placeholder="Vertragsnummer, Gemeinde, Pächter">
</div>
<div class="col-md-2">
<label for="status" class="form-label">Status</label>
<select name="status" id="status" class="form-select">
<option value="">Alle Status</option>
{% for status in status_choices %}
<option value="{{ status.0 }}" {% if status_filter == status.0 %}selected{% endif %}>{{ status.1 }}</option>
{% endfor %}
</select>
</div>
<div class="col-md-3">
<label for="gemeinde" class="form-label">Gemeinde</label>
<select name="gemeinde" id="gemeinde" class="form-select">
<option value="">Alle Gemeinden</option>
{% for gemeinde in gemeinden %}
<option value="{{ gemeinde }}" {% if gemeinde_filter == gemeinde %}selected{% endif %}>{{ gemeinde }}</option>
{% endfor %}
</select>
</div>
<div class="col-md-4 d-flex align-items-end">
<button type="submit" class="btn btn-primary me-2">
<i class="fas fa-search me-2"></i>Filtern
</button>
<a href="{% url 'stiftung:verpachtung_list' %}" class="btn btn-outline-secondary">
<i class="fas fa-times me-2"></i>Zurücksetzen
</a>
</div>
</form>
</div>
</div>
<!-- Verpachtungen Table -->
<div class="card">
<div class="card-header">
<h6 class="mb-0">
<i class="fas fa-list me-2"></i>Verpachtungen
</h6>
</div>
<div class="card-body">
{% if page_obj %}
<div class="table-responsive">
<table class="table table-hover">
<thead>
<tr>
<th>
<a href="?sort=vertragsnummer&dir={% if sort == 'vertragsnummer' and dir != 'desc' %}desc{% else %}asc{% endif %}{% if search_query %}&search={{ search_query }}{% endif %}{% if status_filter %}&status={{ status_filter }}{% endif %}{% if gemeinde_filter %}&gemeinde={{ gemeinde_filter }}{% endif %}">Vertragsnummer</a>
</th>
<th>
<a href="?sort=land&dir={% if sort == 'land' and dir != 'desc' %}desc{% else %}asc{% endif %}{% if search_query %}&search={{ search_query }}{% endif %}{% if status_filter %}&status={{ status_filter }}{% endif %}{% if gemeinde_filter %}&gemeinde={{ gemeinde_filter }}{% endif %}">Länderei</a>
</th>
<th>
<a href="?sort=paechter&dir={% if sort == 'paechter' and dir != 'desc' %}desc{% else %}asc{% endif %}{% if search_query %}&search={{ search_query }}{% endif %}{% if status_filter %}&status={{ status_filter }}{% endif %}{% if gemeinde_filter %}&gemeinde={{ gemeinde_filter }}{% endif %}">Pächter</a>
</th>
<th>
<a href="?sort=beginn&dir={% if sort == 'beginn' and dir != 'desc' %}desc{% else %}asc{% endif %}{% if search_query %}&search={{ search_query }}{% endif %}{% if status_filter %}&status={{ status_filter }}{% endif %}{% if gemeinde_filter %}&gemeinde={{ gemeinde_filter }}{% endif %}">Beginn</a>/<a href="?sort=ende&dir={% if sort == 'ende' and dir != 'desc' %}desc{% else %}asc{% endif %}{% if search_query %}&search={{ search_query }}{% endif %}{% if status_filter %}&status={{ status_filter }}{% endif %}{% if gemeinde_filter %}&gemeinde={{ gemeinde_filter }}{% endif %}">Ende</a>
</th>
<th>
<a href="?sort=flaeche&dir={% if sort == 'flaeche' and dir != 'desc' %}desc{% else %}asc{% endif %}{% if search_query %}&search={{ search_query }}{% endif %}{% if status_filter %}&status={{ status_filter }}{% endif %}{% if gemeinde_filter %}&gemeinde={{ gemeinde_filter }}{% endif %}">Fläche</a>
</th>
<th>
<a href="?sort=pachtzins&dir={% if sort == 'pachtzins' and dir != 'desc' %}desc{% else %}asc{% endif %}{% if search_query %}&search={{ search_query }}{% endif %}{% if status_filter %}&status={{ status_filter }}{% endif %}{% if gemeinde_filter %}&gemeinde={{ gemeinde_filter }}{% endif %}">Pachtzins</a>
</th>
<th>
<a href="?sort=status&dir={% if sort == 'status' and dir != 'desc' %}desc{% else %}asc{% endif %}{% if search_query %}&search={{ search_query }}{% endif %}{% if status_filter %}&status={{ status_filter }}{% endif %}{% if gemeinde_filter %}&gemeinde={{ gemeinde_filter }}{% endif %}">Status</a>
</th>
<th>Aktionen</th>
</tr>
</thead>
<tbody>
{% for verpachtung in page_obj %}
<tr>
<td>
<strong>{{ verpachtung.vertragsnummer }}</strong>
</td>
<td>
<a href="{% url 'stiftung:land_detail' verpachtung.land.pk %}">
{{ verpachtung.land.gemeinde }}
</a>
</td>
<td>
<a href="{% url 'stiftung:paechter_detail' verpachtung.paechter.pk %}">
{{ verpachtung.paechter.get_full_name }}
</a>
</td>
<td>
{{ verpachtung.pachtbeginn|date:"d.m.Y" }} - {{ verpachtung.pachtende|date:"d.m.Y" }}
{% if verpachtung.verlaengerung %}
<br><small class="text-muted">Verlängert bis: {{ verpachtung.verlaengerung|date:"d.m.Y" }}</small>
{% endif %}
</td>
<td>
{{ verpachtung.verpachtete_flaeche|floatformat:2 }} qm
<br>
<small class="text-muted">({{ verpachtung.verpachtete_flaeche_hektar|floatformat:2 }} ha)</small>
</td>
<td>€{{ verpachtung.pachtzins_jaehrlich|floatformat:2 }}/Jahr</td>
<td>
<span class="badge bg-{% if verpachtung.status == 'aktiv' %}success{% elif verpachtung.status == 'beendet' %}secondary{% elif verpachtung.status == 'gekuendigt' %}danger{% else %}warning{% endif %}">
{{ verpachtung.get_status_display }}
</span>
</td>
<td>
<div class="btn-group" role="group">
<a href="{% url 'stiftung:verpachtung_detail' verpachtung.pk %}" class="btn btn-sm btn-outline-primary">
<i class="fas fa-eye"></i>
</a>
<a href="{% url 'stiftung:verpachtung_update' verpachtung.pk %}" class="btn btn-sm btn-outline-warning">
<i class="fas fa-edit"></i>
</a>
<a href="{% url 'stiftung:verpachtung_delete' verpachtung.pk %}" class="btn btn-sm btn-outline-danger">
<i class="fas fa-trash"></i>
</a>
</div>
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
<!-- Pagination -->
{% if page_obj.has_other_pages %}
<nav aria-label="Verpachtungen Pagination">
<ul class="pagination justify-content-center">
{% if page_obj.has_previous %}
<li class="page-item">
<a class="page-link" href="?page={{ page_obj.previous_page_number }}{% if search_query %}&search={{ search_query }}{% endif %}{% if status_filter %}&status={{ status_filter }}{% endif %}{% if gemeinde_filter %}&gemeinde={{ gemeinde_filter }}{% endif %}{% if sort %}&sort={{ sort }}&dir={{ dir }}{% endif %}">
<i class="fas fa-chevron-left"></i>
</a>
</li>
{% endif %}
{% for num in page_obj.paginator.page_range %}
{% if page_obj.number == num %}
<li class="page-item active">
<span class="page-link">{{ num }}</span>
</li>
{% elif num > page_obj.number|add:'-3' and num < page_obj.number|add:'3' %}
<li class="page-item">
<a class="page-link" href="?page={{ num }}{% if search_query %}&search={{ search_query }}{% endif %}{% if status_filter %}&status={{ status_filter }}{% endif %}{% if gemeinde_filter %}&gemeinde={{ gemeinde_filter }}{% endif %}{% if sort %}&sort={{ sort }}&dir={{ dir }}{% endif %}">{{ num }}</a>
</li>
{% endif %}
{% endfor %}
{% if page_obj.has_next %}
<li class="page-item">
<a class="page-link" href="?page={{ page_obj.next_page_number }}{% if search_query %}&search={{ search_query }}{% endif %}{% if status_filter %}&status={{ status_filter }}{% endif %}{% if gemeinde_filter %}&gemeinde={{ gemeinde_filter }}{% endif %}{% if sort %}&sort={{ sort }}&dir={{ dir }}{% endif %}">
<i class="fas fa-chevron-right"></i>
</a>
</li>
{% endif %}
</ul>
</nav>
{% endif %}
{% else %}
<div class="text-center py-5">
<i class="fas fa-handshake fa-3x text-muted mb-3"></i>
<h5 class="text-muted">Keine Verpachtungen gefunden</h5>
<p class="text-muted">Erstellen Sie Ihre erste Verpachtung oder passen Sie die Filter an.</p>
<a href="{% url 'stiftung:verpachtung_create' %}" class="btn btn-primary">
<i class="fas fa-plus me-2"></i>Neue Verpachtung erstellen
</a>
</div>
{% endif %}
</div>
</div>
</div>
</div>
{% endblock %}

View File

@@ -0,0 +1,126 @@
{% extends 'base.html' %}
{% block title %}{{ title }}{% endblock %}
{% block content %}
<nav aria-label="breadcrumb">
<ol class="breadcrumb">
<li class="breadcrumb-item"><a href="{% url 'stiftung:dashboard' %}">Dashboard</a></li>
<li class="breadcrumb-item"><a href="{% url 'stiftung:unterstuetzungen_all' %}">Unterstützungen</a></li>
<li class="breadcrumb-item active" aria-current="page">{{ title }}</li>
</ol>
</nav>
<div class="d-flex justify-content-between align-items-center mb-4">
<h2>{{ title }}</h2>
<div class="btn-group">
<a href="{% url 'stiftung:unterstuetzungen_all' %}" class="btn btn-outline-primary">
<i class="fas fa-list me-2"></i>Alle Unterstützungen
</a>
<a href="?cleanup=1" class="btn btn-outline-warning"
onclick="return confirm('Möchten Sie wirklich alle verwaisten Zahlungsvorlagen löschen? Dies können Vorlagen sein, deren zugehörige Unterstützungen bereits gelöscht wurden.')">
<i class="fas fa-broom me-2"></i>Verwaiste bereinigen
</a>
</div>
</div>
<div class="row">
<div class="col-12">
<div class="card shadow">
<div class="card-header bg-info text-white">
<h5 class="mb-0">
<i class="fas fa-sync-alt me-2"></i>Wiederkehrende Zahlungsvorlagen
</h5>
</div>
<div class="card-body">
{% if templates %}
<div class="table-responsive">
<table class="table table-hover">
<thead>
<tr>
<th>Destinatär</th>
<th>Betrag</th>
<th>Intervall</th>
<th>Nächste Generierung</th>
<th>Status</th>
<th>Zahlungen</th>
<th>Aktionen</th>
</tr>
</thead>
<tbody>
{% for template in templates %}
<tr>
<td>
<strong>{{ template.destinataer.get_full_name }}</strong>
{% if template.beschreibung %}
<br><small class="text-muted">{{ template.beschreibung }}</small>
{% endif %}
</td>
<td class="text-success fw-bold">€{{ template.betrag|floatformat:2 }}</td>
<td>
<span class="badge bg-secondary">{{ template.get_intervall_display }}</span>
</td>
<td>{{ template.naechste_generierung|date:"d.m.Y" }}</td>
<td>
{% if template.aktiv %}
<span class="badge bg-success">Aktiv</span>
{% else %}
<span class="badge bg-danger">Inaktiv</span>
{% endif %}
</td>
<td>
{% if template.aktive_zahlungen > 0 %}
<span class="badge bg-info">{{ template.aktive_zahlungen }}</span>
{% else %}
<span class="badge bg-warning text-dark">0</span>
{% endif %}
</td>
<td>
<div class="btn-group btn-group-sm">
<a href="{% url 'stiftung:destinataer_detail' pk=template.destinataer.pk %}"
class="btn btn-outline-primary" title="Destinatär anzeigen">
<i class="fas fa-user"></i>
</a>
{% if template.empfaenger_iban %}
<button class="btn btn-outline-info" title="IBAN kopieren"
onclick="navigator.clipboard.writeText('{{ template.empfaenger_iban }}'); alert('IBAN kopiert!');">
<i class="fas fa-copy"></i>
</button>
{% endif %}
</div>
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
{% else %}
<div class="text-center py-5">
<i class="fas fa-sync-alt fa-3x text-muted mb-3"></i>
<h5 class="text-muted">Keine wiederkehrenden Zahlungen vorhanden</h5>
<p class="text-muted">
Erstellen Sie eine neue Unterstützung mit der Option "Wiederkehrende Zahlung"
um automatische Zahlungen zu generieren.
</p>
<a href="{% url 'stiftung:unterstuetzung_create' %}" class="btn btn-primary">
<i class="fas fa-plus me-2"></i>Neue Unterstützung erstellen
</a>
</div>
{% endif %}
</div>
</div>
</div>
</div>
{% if templates %}
<div class="mt-4">
<div class="alert alert-info">
<h6><i class="fas fa-info-circle me-2"></i>Hinweise zu wiederkehrenden Zahlungen:</h6>
<ul class="mb-0">
<li>Wiederkehrende Zahlungen werden automatisch basierend auf dem eingestellten Intervall generiert</li>
<li>Die nächste Generierung erfolgt am angegebenen Datum</li>
<li>Deaktivierte Vorlagen generieren keine neuen Zahlungen</li>
<li>Jede generierte Zahlung kann individual bearbeitet werden</li>
</ul>
</div>
</div>
{% endif %}
{% endblock %}

View File

@@ -0,0 +1,32 @@
#!/usr/bin/env python
import os
import django
# Setup Django environment
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'core.settings')
django.setup()
from stiftung.models import UnterstuetzungWiederkehrend, DestinataerUnterstuetzung
# Find orphaned templates
orphaned = []
for template in UnterstuetzungWiederkehrend.objects.all():
count = DestinataerUnterstuetzung.objects.filter(wiederkehrend_von=template).count()
if count == 0:
orphaned.append((template, count))
print(f'Verwaiste Vorlagen gefunden: {len(orphaned)}')
if orphaned:
print('Details:')
for template, count in orphaned[:10]: # Show first 10
print(f'- ID {template.id}: {template.destinataer} - {template.beschreibung} ({template.betrag}€)')
else:
print('Keine verwaisten Vorlagen!')
# Also show all templates with their payment counts
print('\n--- Alle wiederkehrende Vorlagen ---')
all_templates = UnterstuetzungWiederkehrend.objects.all()
for template in all_templates[:10]: # Show first 10
count = DestinataerUnterstuetzung.objects.filter(wiederkehrend_von=template).count()
status = "VERWAIST" if count == 0 else f"{count} Zahlungen"
print(f'ID {template.id}: {template.destinataer} - {template.beschreibung} ({template.betrag}€) - {status}')

View File

@@ -0,0 +1,130 @@
# Legacy Verpachtung Cleanup - Complete Removal
**Date:** August 31, 2025
**Status:** ✅ COMPLETED - Legacy system completely removed
## Overview
This document summarizes the complete removal of the legacy Verpachtung system from the Django Stiftung application. The legacy system has been fully replaced by the new LandVerpachtung model and all references have been cleaned up.
## Actions Completed
### 1. Data Migration ✅
- ✅ Deleted all 3 legacy Verpachtung records (user confirmed no valid data)
- ✅ New LandVerpachtung system contains 8 active records
- ✅ LandAbrechnung system validated with 55 accounting records
### 2. Model Cleanup ✅
- ✅ Removed `Verpachtung` class from `stiftung/models.py`
- ✅ Created and applied Django migration `0023_remove_legacy_verpachtung.py`
- ✅ Database table `stiftung_verpachtung` successfully dropped
- ✅ Removed imports of `Verpachtung` from models
### 3. Views Cleanup ✅
- ✅ Removed all legacy verpachtung views:
- `verpachtung_list`
- `verpachtung_detail`
- `verpachtung_create`
- `verpachtung_update`
- `verpachtung_delete`
- `verpachtung_export`
- ✅ Removed `Verpachtung` and `VerpachtungForm` imports from views
- ✅ Cleaned up view imports in `stiftung/views.py`
### 4. URL Cleanup ✅
- ✅ Removed all legacy verpachtung URL patterns from `stiftung/urls.py`:
- `verpachtungen/` (list)
- `verpachtungen/<uuid:pk>/` (detail)
- `verpachtungen/neu/` (create)
- `verpachtungen/<uuid:pk>/bearbeiten/` (update)
- `verpachtungen/<uuid:pk>/loeschen/` (delete)
- `verpachtungen/<uuid:pk>/export/` (export)
### 5. Forms Cleanup ✅
- ✅ Removed `VerpachtungForm` class from `stiftung/forms.py`
- ✅ Removed `Verpachtung` import from forms
### 6. Admin Cleanup ✅
- ✅ Removed `VerpachtungAdmin` class from `stiftung/admin.py`
- ✅ Removed `@admin.register(Verpachtung)` decorator
- ✅ Removed `Verpachtung` import from admin
### 7. Template Cleanup ✅
- ✅ Deleted all legacy verpachtung templates:
- `verpachtung_list.html`
- `verpachtung_detail.html`
- `verpachtung_form.html`
- `verpachtung_confirm_delete.html`
### 8. Command Cleanup ✅
- ✅ Removed `convert_legacy_verpachtungen.py` management command (no longer needed)
### 9. System Validation ✅
- ✅ Application starts successfully after cleanup
- ✅ New LandVerpachtung model functions correctly (8 records)
- ✅ LandAbrechnung accounting system intact (55 records)
- ✅ No legacy references remain in codebase
## Current State
### Active Systems
- **LandVerpachtung**: Modern lease management with 8 active leases
- **LandAbrechnung**: Accounting system with 55 records
- **Navigation**: Clean UI with only new lease workflows
- **Database**: Legacy table `stiftung_verpachtung` removed
### Removed Systems
- **Legacy Verpachtung Model**: Completely removed
- **Legacy URLs**: All verpachtung URLs removed
- **Legacy Views**: All verpachtung views removed
- **Legacy Templates**: All verpachtung templates removed
- **Legacy Forms**: VerpachtungForm removed
- **Legacy Admin**: VerpachtungAdmin removed
- **Legacy Commands**: Conversion command removed
## Benefits Achieved
1. **Simplified Codebase**: Removed ~500 lines of legacy code
2. **Clean User Interface**: No confusing dual lease systems
3. **Better Architecture**: Single lease model with proper accounting integration
4. **Reduced Maintenance**: No legacy code to maintain
5. **Database Cleanup**: Removed unused table and relationships
## Next Steps (Recommended)
1. **Monitor System**: Verify new lease workflows work as expected
2. **User Training**: Update any documentation to reflect single lease system
3. **Backup Validation**: Ensure backup system works with cleaned schema
## Files Modified
### Removed Files
- `templates/stiftung/verpachtung_*.html` (4 files)
- `stiftung/management/commands/convert_legacy_verpachtungen.py`
### Modified Files
- `stiftung/models.py` - Removed Verpachtung class
- `stiftung/views.py` - Removed verpachtung views and imports
- `stiftung/urls.py` - Removed verpachtung URL patterns
- `stiftung/forms.py` - Removed VerpachtungForm
- `stiftung/admin.py` - Removed VerpachtungAdmin
- `stiftung/migrations/0023_remove_legacy_verpachtung.py` - New migration
### Unchanged Files
- All LandVerpachtung related files remain intact
- All LandAbrechnung accounting files remain intact
- Navigation templates already cleaned up previously
## Technical Notes
- Django migration `0023_remove_legacy_verpachtung.py` successfully applied
- No foreign key constraints violated (references already cleaned up)
- Application restart successful
- All imports resolved correctly
- No template resolution errors
---
**Migration Status:** Complete
**System Status:** Fully operational with new lease system only
**Legacy Status:** Completely removed

View File

@@ -0,0 +1,79 @@
# Verpachtung Legacy → LandVerpachtung Migration Plan
## Status (2025-08-31) - ✅ COMPLETED
- **Phase 1 Complete**: Legacy create/edit blocked via feature flags
- **Phase 2 Complete**: Conversion command created and executed (3 legacy → 8 new)
- **Phase 3 Complete**: Legacy system completely removed from codebase
## Final State
- Legacy count: 0 Verpachtung records (deleted - no valid data)
- New count: 8 LandVerpachtung records (active)
- Legacy model: Completely removed from system
- Financial sync: Both models working (11,120.60€ total rent verified)
## Feature Flags Added
```python
# In Django settings or environment
DISABLE_LEGACY_VERPACHTUNG_CREATE = True # Blocks verpachtung_create
DISABLE_LEGACY_VERPACHTUNG_EDIT = True # Blocks verpachtung_update
```
## Phase 3: Navigation and View Cleanup
To complete the migration, we need to:
### 3.1 Template Navigation Updates
- Find and comment/remove "Neue Verpachtung" links in navigation/menus
- Keep legacy detail/list views for data viewing but remove edit buttons
- Update dashboard/summary pages to show new lease counts
### 3.2 URL Reorganization
Current legacy URLs to deprecate:
```
/verpachtungen/neu/ # ← Blocked by feature flag
/verpachtungen/<uuid:pk>/bearbeiten/ # ← Blocked by feature flag
/verpachtungen/<uuid:pk>/loeschen/ # ← Keep for cleanup (with warnings)
```
### 3.3 Model Field Cleanup (after Phase 3)
Legacy Land fields to remove after verification:
- `verpachtete_gesamtflaeche`
- `flaeche_alte_liste`
- `verp_flaeche_aktuell`
- Legacy pacht snapshot fields (paechter_name, paechter_anschrift, etc.)
### 3.4 Final Model Removal (Phase 4)
After all views/templates updated:
- Remove `Verpachtung` model class
- Remove legacy views/forms/templates
- Create migration to drop database table
## Conversion Command Usage
```bash
# Dry run (safe)
python manage.py convert_legacy_verpachtungen
# Convert with limit
python manage.py convert_legacy_verpachtungen --limit 10
# Commit changes
python manage.py convert_legacy_verpachtungen --commit
```
## Verification Steps
1. Check counts: `Legacy: 3, New: 8`
2. Financial sync: `11,120.60€ total`
3. UI testing: Access new lease flows via Land detail pages
4. Data spot-check: Compare legacy vs new lease details for accuracy
## Next Actions
- [ ] Update navigation templates to hide legacy create/edit links
- [ ] Add deprecation notices in legacy detail templates
- [ ] Test new lease workflows end-to-end
- [ ] Prepare field removal migrations
- [ ] Document new user workflows
## Rollback Plan
If issues arise:
1. Set feature flags to `False` to re-enable legacy flows
2. Delete problematic new LandVerpachtung records if needed
3. Revert to legacy workflow until issues resolved

View File

@@ -0,0 +1,134 @@
# Stiftung Starter: Schema, Usage, and Cleanup Plan (2025-08-31)
This document consolidates the current domain model, relationships, active usage across the codebase, and targeted cleanup/migration proposals.
## Summary
- Server and date-handling fixes completed earlier; stack is healthy.
- New vs. legacy lease models both in use; annual accounting (LandAbrechnung) is the hub.
- Orphans: a few likely candidates (LandAbrechnung.zahlungen, file fields; ApplicationPermission stub). Several legacy fields still referenced; plan deprecation after migration.
## Entities and Relationships
- CSVImport
- Tracks CSV imports; fields: import_type, filename, file_size, status, totals, error_log, created_by, started_at, completed_at
- Paechter (Tenant)
- Fields: vorname, nachname, geburtsdatum, email, telefon, iban, strasse, plz, ort, personentyp, pachtnummer, pachtbeginn_erste, pachtende_letzte, pachtzins_aktuell, landwirtschaftliche_ausbildung, berufserfahrung_jahre, spezialisierung, notizen, aktiv
- Relations: Verpachtung (legacy) M:1; LandVerpachtung (new) M:1; reverse from Land.aktueller_paechter
- Helpers: get_full_name, get_aktive_verpachtungen, get_gesamt_pachtflaeche, get_gesamt_pachtzins
- Destinataer (Beneficiary)
- Fields: familienzweig, vorname, nachname, geburtsdatum, email, telefon, iban, strasse, plz, ort, berufsgruppe, ausbildungsstand, institution, projekt_beschreibung, jaehrliches_einkommen, finanzielle_notlage, notizen, aktiv, ist_abkoemmling, haushaltsgroesse, monatliche_bezuege, vermoegen, unterstuetzung_bestaetigt, standard_konto(FK StiftungsKonto), vierteljaehrlicher_betrag, studiennachweis_erforderlich, letzter_studiennachweis
- Relations: Foerderung 1:M (via destinataer), DestinataerUnterstuetzung 1:M, DestinataerNotiz 1:M
- Helpers: get_full_name, totals, erfuellt_voraussetzungen, naechste_studiennachweis_termine
- Person (Legacy)
- Legacy stand-in for beneficiaries; still referenced in views/templates; used by Foerderung.person
- Land (Parcel)
- Identity: lfd_nr(unique), ew_nummer, grundbuchblatt, amtsgericht, gemeinde, gemarkung, flur, flurstueck, adresse
- Area: groesse_qm, gruenland_qm, acker_qm, wald_qm, sonstiges_qm
- Legacy pacht: verpachtete_gesamtflaeche, flaeche_alte_liste, verp_flaeche_aktuell
- Current pacht snapshot: aktueller_paechter(FK), paechter_name, paechter_anschrift, pachtbeginn, pachtende, verlaengerung_klausel, zahlungsweise, pachtzins_pro_ha, pachtzins_pauschal
- USt/Tax: ust_option, ust_satz, grundsteuer_umlage, versicherungen_umlage, verbandsbeitraege_umlage, jagdpacht_anteil_umlage, anteil_grundsteuer, anteil_lwk
- Status/meta: aktiv, notizen, erstellt_am, aktualisiert_am
- Relations: Verpachtung (legacy) 1:M; LandVerpachtung (new) 1:M; LandAbrechnung 1:M
- Helpers: area conversions; pachtfläche calculators
- LandVerpachtung (New Lease)
- land(FK), paechter(FK), vertragsnummer(unique), pachtbeginn, pachtende?, verlaengerung_klausel
- verpachtete_flaeche, pachtzins_pauschal, pachtzins_pro_ha?, zahlungsweise
- USt/umlagen flags; status, bemerkungen; timestamps
- Behavior: save/delete sync LandAbrechnung; rent proration per year; umlage calc currently 0
- LandAbrechnung (Annual Accounting)
- land(FK), abrechnungsjahr(unique per land)
- Einnahmen: pacht_vereinnahmt, umlagen_vereinnahmt, sonstige_einnahmen, zahlungen(JSON?)
- Ausgaben buckets; vorsteuer_aus_umlagen; offene_posten; bemerkungen
- File fields for uploads; timestamps; derived totals incl. ust_pacht_betrag
- Verpachtung (Legacy Lease)
- land(FK), paechter(FK), vertragsnummer(unique), pachtbeginn, pachtende, verlaengerung?
- pachtzins_pro_qm, pachtzins_jaehrlich, verpachtete_flaeche; status; verwendungsnachweis; bemerkungen; timestamps
- Behavior: save/delete sync LandAbrechnung; rent proration with verlängerung
- DokumentLink (Paperless linking)
- paperless_document_id, kontext, titel, beschreibung
- Flexible UUID links: verpachtung_id, land_verpachtung_id, land_id, paechter_id, destinataer_id, foerderung_id, rentmeister_id
- Foerderung (Grant)
- person(FK legacy, nullable), destinataer(FK, nullable), jahr, betrag, kategorie, status, verwendungsnachweis(FK), bemerkungen, antragsdatum, entscheidungsdatum
- DestinataerUnterstuetzung (Support)
- destinataer(FK), konto(FK StiftungsKonto), betrag, faellig_am, status, beschreibung, timestamps; indexes
- DestinataerNotiz
- destinataer(FK), titel, text, datei, erstellt_von(FK auth.User), erstellt_am
- Rentmeister
- person/address/bank fields; seit_datum, bis_datum?, aktiv, monatliche_verguetung, km_pauschale; used by Verwaltungskosten
- StiftungsKonto
- kontoname, bank_name, iban, bic, konto_typ, saldo, saldo_datum, zinssatz, laufzeit_bis, aktiv, notizen, timestamps
- BankTransaction
- konto(FK), datum, valuta?, betrag, waehrung, verwendungszweck, empfaenger_zahlungspflichtiger, iban_gegenpartei, bic_gegenpartei, referenz, transaction_type, status, kommentare, verwaltungskosten(FK?), import_datei, importiert_am, saldo_nach_buchung
- unique_together: (konto, datum, betrag, referenz)
- Verwaltungskosten
- bezeichnung, kategorie, betrag, datum, lieferant_firma, rechnungsnummer, status, rentmeister(FK), zahlungskonto(FK), quellkonto(FK), konto(FK legacy), km_anzahl, km_satz, von_ort, nach_ort, zweck, beschreibung, notizen, timestamps
- ApplicationPermission (managed=False)
- Declares custom permission codenames; no table created
- AuditLog
- user(FK), username, timestamp, action, entity_type, entity_id, entity_name, description, changes(JSON), ip_address, user_agent, session_key; indexes
- BackupJob
- backup_type, status, created_by(FK), created_at/started_at/completed_at, backup_filename, backup_size, error_message, database_size, files_count; helpers
## Usage observations (workspace-wide scans)
- Legacy vs. new lease models are both heavily referenced across views/templates/forms.
- LandAbrechnung core fields are used in forms and detail pages; pacht_vereinnahmt updated by save/delete hooks and a sync command.
- DokumentLink UUID references are widely used for cross-entity navigation.
- Destinataer support fields power quarterly payout scheduling.
- Person (legacy) still appears in Foerderung flows and person pages.
## Likely orphans / cleanup candidates
- LandAbrechnung.zahlungen (JSON): no UI/logic usage detected.
- LandAbrechnung file fields (pachtvertrag_datei, grundsteuer_bescheid_datei, versicherungsnachweis_datei): unused in UI; Paperless appears primary doc source.
- ApplicationPermission: only a managed=False stub with permissions; keep if used for seeding, otherwise move to data migration and remove the class.
- LandVerpachtung._calculate_umlage_for_year: returns 0; umlage flags are gathered but not used to compute revenue.
- Legacy Land fields: still used (imports, displays). Plan deprecation after migration to new lease model.
- Foerderung.person vs destinataer: both used; plan single-source migration and drop legacy.
## Recommendations and plan
1) Consolidate to LandVerpachtung
- Stop creating new legacy Verpachtung in UI; a feature flag now blocks legacy create (`DISABLE_LEGACY_VERPACHTUNG_CREATE`, default True) and guides users to the new flow.
- A conversion command `convert_legacy_verpachtungen` is scaffolded (dry-run by default; use `--commit` to persist) to create `LandVerpachtung` rows from `Verpachtung`.
- After migration and verification: remove legacy model, views, and Lands legacy pacht snapshot fields.
2) DRY date handling
- A shared helper exists: `stiftung/utils/date_utils.py` with `ensure_date` and `get_year_from_date`. Models now import and use it.
3) Implement Umlage allocation to Abrechnung
- For each LandAbrechnung year, distribute expense buckets (grundsteuer_betrag, versicherungen_betrag, verbandsbeitraege_betrag, ggf. jagdpacht) across active leases using area share; increase umlagen_vereinnahmt accordingly.
- Extend sync_abrechnungen to recompute Umlagen similar to Pacht.
4) Prune unused LandAbrechnung fields
- If Paperless is canonical: drop the three file fields.
- If no plan for zahlungen JSON: remove or replace with a Payment model.
5) Indexing
- Add selective DB indexes on frequent filters (year, status, land, konto+datum).
6) Permissions
- If using custom permissions, define them in a data migration or settings; remove the managed=False model class.
7) Template consolidation
- Extract shared partials for lease display to reduce duplication between legacy and new renders.
## Notes
- Date: 2025-08-31
- Compose services OK; app checks pass (docker-compose exec web python manage.py check).
- To try conversion: `python manage.py convert_legacy_verpachtungen` (dry-run), add `--commit` to apply changes; optionally `--limit N`.

View File

@@ -0,0 +1,32 @@
#!/usr/bin/env python3
"""
Script to replace all remaining legacy Verpachtung references with LandVerpachtung
"""
import re
def replace_verpachtung_references():
with open('app/stiftung/views.py', 'r', encoding='utf-8') as f:
content = f.read()
# Replace Verpachtung.objects with LandVerpachtung.objects
content = re.sub(r'\bVerpachtung\.objects\b', 'LandVerpachtung.objects', content)
# Replace legacy field names with new ones
content = re.sub(r'pachtzins_jaehrlich', 'pachtzins_pauschal', content)
# Replace legacy field references in aggregates
content = re.sub(r"Sum\('verpachtung__pachtzins_jaehrlich'\)", "Sum('landverpachtung__pachtzins_pauschal')", content)
content = re.sub(r"Sum\('verpachtung__verpachtete_flaeche'\)", "Sum('landverpachtung__verpachtete_flaeche')", content)
# Update related name references
content = re.sub(r"verpachtung_set", "landverpachtung_set", content)
content = re.sub(r"verpachtung_id", "land_verpachtung_id", content)
with open('app/stiftung/views.py', 'w', encoding='utf-8') as f:
f.write(content)
print("Updated all legacy Verpachtung references to use LandVerpachtung")
if __name__ == "__main__":
replace_verpachtung_references()

31
remove_legacy_views.py Normal file
View File

@@ -0,0 +1,31 @@
#!/usr/bin/env python3
"""
Script to remove legacy Verpachtung views from views.py
"""
import re
import sys
def remove_legacy_verpachtung_views():
with open('app/stiftung/views.py', 'r', encoding='utf-8') as f:
content = f.read()
# Remove verpachtung_export function
export_pattern = r'@login_required\s+def verpachtung_export\([^)]+\):[^@]*?pass\s*\n\n'
content = re.sub(export_pattern, '', content, flags=re.DOTALL)
# More aggressive pattern for verpachtung_export
export_pattern2 = r'@login_required\s+def verpachtung_export.*?(?=@login_required|def \w+.*?\(.*?\):|\Z)'
content = re.sub(export_pattern2, '', content, flags=re.DOTALL)
# Remove comment and all verpachtung views until next comment
verpachtung_section_pattern = r'# Verpachtung Views.*?(?=# [A-Z][^#\n]*Views|\Z)'
content = re.sub(verpachtung_section_pattern, '', content, flags=re.DOTALL)
with open('app/stiftung/views.py', 'w', encoding='utf-8') as f:
f.write(content)
print("Legacy Verpachtung views removed from views.py")
if __name__ == "__main__":
remove_legacy_verpachtung_views()