Format code with Black and isort for CI/CD compliance

- Apply Black formatting to all Python files in core and stiftung modules
- Fix import statement ordering with isort
- Ensure all code meets automated quality standards
- Resolve CI/CD pipeline formatting failures
- Maintain consistent code style across the entire codebase
This commit is contained in:
Stiftung Development
2025-09-06 21:04:07 +02:00
parent c7c790ee09
commit e0c7d0e351
54 changed files with 11004 additions and 6423 deletions

View File

@@ -1,4 +1,3 @@
from .celery import app as celery from .celery import app as celery
__all__ = ("celery",) __all__ = ("celery",)

View File

@@ -1,4 +1,6 @@
import os import os
from django.core.asgi import get_asgi_application from django.core.asgi import get_asgi_application
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'core.settings')
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "core.settings")
application = get_asgi_application() application = get_asgi_application()

View File

@@ -1,4 +1,5 @@
import os import os
from celery import Celery from celery import Celery
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "core.settings") os.environ.setdefault("DJANGO_SETTINGS_MODULE", "core.settings")
@@ -6,5 +7,3 @@ os.environ.setdefault("DJANGO_SETTINGS_MODULE", "core.settings")
app = Celery("core") app = Celery("core")
app.config_from_object("django.conf:settings", namespace="CELERY") app.config_from_object("django.conf:settings", namespace="CELERY")
app.autodiscover_tasks() app.autodiscover_tasks()

View File

@@ -1,5 +1,6 @@
import os import os
from pathlib import Path from pathlib import Path
from dotenv import load_dotenv from dotenv import load_dotenv
# Load environment variables from .env file # Load environment variables from .env file
@@ -13,59 +14,59 @@ ALLOWED_HOSTS = os.getenv("DJANGO_ALLOWED_HOSTS", "*").split(",")
# CSRF settings for localhost development # CSRF settings for localhost development
CSRF_TRUSTED_ORIGINS = [ CSRF_TRUSTED_ORIGINS = [
'http://localhost:8081', "http://localhost:8081",
'http://127.0.0.1:8081', "http://127.0.0.1:8081",
] ]
INSTALLED_APPS = [ INSTALLED_APPS = [
'django.contrib.admin', "django.contrib.admin",
'django.contrib.auth', "django.contrib.auth",
'django.contrib.contenttypes', "django.contrib.contenttypes",
'django.contrib.sessions', "django.contrib.sessions",
'django.contrib.messages', "django.contrib.messages",
'django.contrib.staticfiles', "django.contrib.staticfiles",
'django.contrib.humanize', "django.contrib.humanize",
'rest_framework', "rest_framework",
'stiftung', "stiftung",
] ]
MIDDLEWARE = [ MIDDLEWARE = [
'django.middleware.security.SecurityMiddleware', "django.middleware.security.SecurityMiddleware",
'django.contrib.sessions.middleware.SessionMiddleware', "django.contrib.sessions.middleware.SessionMiddleware",
'django.middleware.common.CommonMiddleware', "django.middleware.common.CommonMiddleware",
'django.middleware.csrf.CsrfViewMiddleware', "django.middleware.csrf.CsrfViewMiddleware",
'django.contrib.auth.middleware.AuthenticationMiddleware', "django.contrib.auth.middleware.AuthenticationMiddleware",
'django.contrib.messages.middleware.MessageMiddleware', "django.contrib.messages.middleware.MessageMiddleware",
'django.middleware.clickjacking.XFrameOptionsMiddleware', "django.middleware.clickjacking.XFrameOptionsMiddleware",
'stiftung.middleware.AuditMiddleware', # Audit logging middleware "stiftung.middleware.AuditMiddleware", # Audit logging middleware
] ]
ROOT_URLCONF = 'core.urls' ROOT_URLCONF = "core.urls"
TEMPLATES = [ TEMPLATES = [
{ {
'BACKEND': 'django.template.backends.django.DjangoTemplates', "BACKEND": "django.template.backends.django.DjangoTemplates",
'DIRS': [BASE_DIR / 'templates'], "DIRS": [BASE_DIR / "templates"],
'APP_DIRS': True, "APP_DIRS": True,
'OPTIONS': { "OPTIONS": {
'context_processors': [ "context_processors": [
'django.template.context_processors.debug', "django.template.context_processors.debug",
'django.template.context_processors.request', "django.template.context_processors.request",
'django.contrib.auth.context_processors.auth', "django.contrib.auth.context_processors.auth",
'django.contrib.messages.context_processors.messages', "django.contrib.messages.context_processors.messages",
], ],
}, },
}, },
] ]
WSGI_APPLICATION = 'core.wsgi.application' WSGI_APPLICATION = "core.wsgi.application"
DATABASES = { DATABASES = {
'default': { "default": {
'ENGINE': 'django.db.backends.postgresql', "ENGINE": "django.db.backends.postgresql",
'NAME': os.getenv('POSTGRES_DB', 'stiftung'), "NAME": os.getenv("POSTGRES_DB", "stiftung"),
'USER': os.getenv('POSTGRES_USER', 'stiftung'), "USER": os.getenv("POSTGRES_USER", "stiftung"),
'PASSWORD': os.getenv('POSTGRES_PASSWORD', 'stiftungpass'), "PASSWORD": os.getenv("POSTGRES_PASSWORD", "stiftungpass"),
'HOST': os.getenv('DB_HOST', 'db'), "HOST": os.getenv("DB_HOST", "db"),
'PORT': os.getenv('DB_PORT', '5432'), "PORT": os.getenv("DB_PORT", "5432"),
} }
} }
@@ -74,17 +75,17 @@ TIME_ZONE = os.getenv("TIME_ZONE", "Europe/Berlin")
USE_I18N = True USE_I18N = True
USE_TZ = True USE_TZ = True
STATIC_URL = 'static/' STATIC_URL = "static/"
STATIC_ROOT = BASE_DIR / 'staticfiles' STATIC_ROOT = BASE_DIR / "staticfiles"
# Additional locations of static files # Additional locations of static files
STATICFILES_DIRS = [ STATICFILES_DIRS = [
BASE_DIR / "static", BASE_DIR / "static",
] ]
DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField' DEFAULT_AUTO_FIELD = "django.db.models.BigAutoField"
MEDIA_URL = '/media/' MEDIA_URL = "/media/"
MEDIA_ROOT = BASE_DIR / 'media' MEDIA_ROOT = BASE_DIR / "media"
# Celery # Celery
CELERY_BROKER_URL = os.getenv("REDIS_URL", "redis://redis:6379/0") CELERY_BROKER_URL = os.getenv("REDIS_URL", "redis://redis:6379/0")
@@ -101,13 +102,13 @@ PAPERLESS_LAND_TAG_ID = os.getenv("PAPERLESS_LAND_TAG_ID", "204")
PAPERLESS_ADMIN_TAG_ID = os.getenv("PAPERLESS_ADMIN_TAG_ID", "216") PAPERLESS_ADMIN_TAG_ID = os.getenv("PAPERLESS_ADMIN_TAG_ID", "216")
# Authentication # Authentication
LOGIN_URL = '/login/' LOGIN_URL = "/login/"
LOGIN_REDIRECT_URL = '/' LOGIN_REDIRECT_URL = "/"
LOGOUT_REDIRECT_URL = '/login/' LOGOUT_REDIRECT_URL = "/login/"
# Gramps integration # Gramps integration
GRAMPS_URL = os.environ.get('GRAMPS_URL', 'http://grampsweb:80') GRAMPS_URL = os.environ.get("GRAMPS_URL", "http://grampsweb:80")
GRAMPS_API_TOKEN = os.environ.get('GRAMPS_API_TOKEN', '') GRAMPS_API_TOKEN = os.environ.get("GRAMPS_API_TOKEN", "")
GRAMPS_STIFTER_IDS = os.environ.get('GRAMPS_STIFTER_IDS', '') # comma-separated GRAMPS_STIFTER_IDS = os.environ.get("GRAMPS_STIFTER_IDS", "") # comma-separated
GRAMPS_USERNAME = os.environ.get('GRAMPS_USERNAME', '') GRAMPS_USERNAME = os.environ.get("GRAMPS_USERNAME", "")
GRAMPS_PASSWORD = os.environ.get('GRAMPS_PASSWORD', '') GRAMPS_PASSWORD = os.environ.get("GRAMPS_PASSWORD", "")

View File

@@ -1,17 +1,21 @@
from django.contrib import admin
from django.urls import path, include
from django.conf import settings from django.conf import settings
from django.conf.urls.static import static from django.conf.urls.static import static
from django.contrib import admin
from django.contrib.auth import views as auth_views from django.contrib.auth import views as auth_views
from django.urls import include, path
from stiftung.views import home from stiftung.views import home
urlpatterns = [ urlpatterns = [
path('', include('stiftung.urls')), path("", include("stiftung.urls")),
path('admin/', admin.site.urls), path("admin/", admin.site.urls),
# Authentication URLs # Authentication URLs
path('login/', auth_views.LoginView.as_view(template_name='registration/login.html'), name='login'), path(
path('logout/', auth_views.LogoutView.as_view(), name='logout'), "login/",
auth_views.LoginView.as_view(template_name="registration/login.html"),
name="login",
),
path("logout/", auth_views.LogoutView.as_view(), name="logout"),
] ]
if settings.DEBUG: if settings.DEBUG:

View File

@@ -1,4 +1,6 @@
import os import os
from django.core.wsgi import get_wsgi_application from django.core.wsgi import get_wsgi_application
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'core.settings')
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "core.settings")
application = get_wsgi_application() application = get_wsgi_application()

File diff suppressed because it is too large Load Diff

View File

@@ -1,5 +1,6 @@
from django.apps import AppConfig from django.apps import AppConfig
class StiftungConfig(AppConfig): class StiftungConfig(AppConfig):
default_auto_field = 'django.db.models.BigAutoField' default_auto_field = "django.db.models.BigAutoField"
name = 'stiftung' name = "stiftung"

View File

@@ -4,8 +4,10 @@ Provides functions to log user actions throughout the application
""" """
import json import json
from django.utils import timezone
from django.contrib.auth import get_user_model from django.contrib.auth import get_user_model
from django.utils import timezone
from stiftung.models import AuditLog from stiftung.models import AuditLog
User = get_user_model() User = get_user_model()
@@ -13,15 +15,17 @@ User = get_user_model()
def get_client_ip(request): def get_client_ip(request):
"""Extract the client IP address from the request""" """Extract the client IP address from the request"""
x_forwarded_for = request.META.get('HTTP_X_FORWARDED_FOR') x_forwarded_for = request.META.get("HTTP_X_FORWARDED_FOR")
if x_forwarded_for: if x_forwarded_for:
ip = x_forwarded_for.split(',')[0] ip = x_forwarded_for.split(",")[0]
else: else:
ip = request.META.get('REMOTE_ADDR') ip = request.META.get("REMOTE_ADDR")
return ip return ip
def log_action(request, action, entity_type, entity_id, entity_name, description, changes=None): def log_action(
request, action, entity_type, entity_id, entity_name, description, changes=None
):
""" """
Log a user action to the audit log Log a user action to the audit log
@@ -35,12 +39,12 @@ def log_action(request, action, entity_type, entity_id, entity_name, description
changes: Dictionary of field changes (optional) changes: Dictionary of field changes (optional)
""" """
user = request.user if request.user.is_authenticated else None user = request.user if request.user.is_authenticated else None
username = user.username if user else 'Anonymous' username = user.username if user else "Anonymous"
# Get request metadata # Get request metadata
ip_address = get_client_ip(request) ip_address = get_client_ip(request)
user_agent = request.META.get('HTTP_USER_AGENT', '') user_agent = request.META.get("HTTP_USER_AGENT", "")
session_key = request.session.session_key if hasattr(request, 'session') else '' session_key = request.session.session_key if hasattr(request, "session") else ""
# Create audit log entry # Create audit log entry
audit_entry = AuditLog.objects.create( audit_entry = AuditLog.objects.create(
@@ -48,13 +52,13 @@ def log_action(request, action, entity_type, entity_id, entity_name, description
username=username, username=username,
action=action, action=action,
entity_type=entity_type, entity_type=entity_type,
entity_id=str(entity_id) if entity_id else '', entity_id=str(entity_id) if entity_id else "",
entity_name=entity_name, entity_name=entity_name,
description=description, description=description,
changes=changes, changes=changes,
ip_address=ip_address, ip_address=ip_address,
user_agent=user_agent[:500], # Truncate to avoid very long user agents user_agent=user_agent[:500], # Truncate to avoid very long user agents
session_key=session_key session_key=session_key,
) )
return audit_entry return audit_entry
@@ -67,11 +71,11 @@ def log_create(request, entity_type, entity_id, entity_name, description=None):
return log_action( return log_action(
request=request, request=request,
action='create', action="create",
entity_type=entity_type, entity_type=entity_type,
entity_id=entity_id, entity_id=entity_id,
entity_name=entity_name, entity_name=entity_name,
description=description description=description,
) )
@@ -84,57 +88,75 @@ def log_update(request, entity_type, entity_id, entity_name, changes, descriptio
return log_action( return log_action(
request=request, request=request,
action='update', action="update",
entity_type=entity_type, entity_type=entity_type,
entity_id=entity_id, entity_id=entity_id,
entity_name=entity_name, entity_name=entity_name,
description=description, description=description,
changes=changes changes=changes,
) )
def log_delete(request, entity_type, entity_id, entity_name, description=None): def log_delete(request, entity_type, entity_id, entity_name, description=None):
"""Log entity deletion""" """Log entity deletion"""
if not description: if not description:
description = f"{entity_type.replace('_', ' ').title()} '{entity_name}' wurde gelöscht" description = (
f"{entity_type.replace('_', ' ').title()} '{entity_name}' wurde gelöscht"
)
return log_action( return log_action(
request=request, request=request,
action='delete', action="delete",
entity_type=entity_type, entity_type=entity_type,
entity_id=entity_id, entity_id=entity_id,
entity_name=entity_name, entity_name=entity_name,
description=description description=description,
) )
def log_link(request, entity_type, entity_id, entity_name, target_type, target_name, description=None): def log_link(
request,
entity_type,
entity_id,
entity_name,
target_type,
target_name,
description=None,
):
"""Log entity linking""" """Log entity linking"""
if not description: if not description:
description = f"{entity_type.replace('_', ' ').title()} '{entity_name}' wurde mit {target_type.replace('_', ' ')} '{target_name}' verknüpft" description = f"{entity_type.replace('_', ' ').title()} '{entity_name}' wurde mit {target_type.replace('_', ' ')} '{target_name}' verknüpft"
return log_action( return log_action(
request=request, request=request,
action='link', action="link",
entity_type=entity_type, entity_type=entity_type,
entity_id=entity_id, entity_id=entity_id,
entity_name=entity_name, entity_name=entity_name,
description=description description=description,
) )
def log_unlink(request, entity_type, entity_id, entity_name, target_type, target_name, description=None): def log_unlink(
request,
entity_type,
entity_id,
entity_name,
target_type,
target_name,
description=None,
):
"""Log entity unlinking""" """Log entity unlinking"""
if not description: if not description:
description = f"Verknüpfung zwischen {entity_type.replace('_', ' ').title()} '{entity_name}' und {target_type.replace('_', ' ')} '{target_name}' wurde entfernt" description = f"Verknüpfung zwischen {entity_type.replace('_', ' ').title()} '{entity_name}' und {target_type.replace('_', ' ')} '{target_name}' wurde entfernt"
return log_action( return log_action(
request=request, request=request,
action='unlink', action="unlink",
entity_type=entity_type, entity_type=entity_type,
entity_id=entity_id, entity_id=entity_id,
entity_name=entity_name, entity_name=entity_name,
description=description description=description,
) )
@@ -143,11 +165,11 @@ def log_system_action(request, action, description, details=None):
return log_action( return log_action(
request=request, request=request,
action=action, action=action,
entity_type='system', entity_type="system",
entity_id='', entity_id="",
entity_name='System', entity_name="System",
description=description, description=description,
changes=details changes=details,
) )
@@ -164,7 +186,13 @@ def track_model_changes(old_instance, new_instance, exclude_fields=None):
Dictionary of changes in format {field: {'old': old_value, 'new': new_value}} Dictionary of changes in format {field: {'old': old_value, 'new': new_value}}
""" """
if exclude_fields is None: if exclude_fields is None:
exclude_fields = ['id', 'erstellt_am', 'aktualisiert_am', 'created_at', 'updated_at'] exclude_fields = [
"id",
"erstellt_am",
"aktualisiert_am",
"created_at",
"updated_at",
]
changes = {} changes = {}
@@ -183,10 +211,7 @@ def track_model_changes(old_instance, new_instance, exclude_fields=None):
new_str = str(new_value) if new_value is not None else None new_str = str(new_value) if new_value is not None else None
if old_str != new_str: if old_str != new_str:
changes[field_name] = { changes[field_name] = {"old": old_str, "new": new_str}
'old': old_str,
'new': new_str
}
return changes return changes
@@ -195,8 +220,9 @@ class AuditLogMixin:
""" """
Mixin for views that provides audit logging functionality Mixin for views that provides audit logging functionality
""" """
audit_entity_type = None audit_entity_type = None
audit_entity_name_field = 'name' audit_entity_name_field = "name"
def get_audit_entity_type(self): def get_audit_entity_type(self):
"""Get the entity type for audit logging""" """Get the entity type for audit logging"""
@@ -204,16 +230,16 @@ class AuditLogMixin:
return self.audit_entity_type return self.audit_entity_type
# Try to derive from model name # Try to derive from model name
if hasattr(self, 'model') and self.model: if hasattr(self, "model") and self.model:
return self.model.__name__.lower() return self.model.__name__.lower()
return 'unknown' return "unknown"
def get_audit_entity_name(self, instance): def get_audit_entity_name(self, instance):
"""Get the entity name for audit logging""" """Get the entity name for audit logging"""
if hasattr(instance, self.audit_entity_name_field): if hasattr(instance, self.audit_entity_name_field):
return str(getattr(instance, self.audit_entity_name_field)) return str(getattr(instance, self.audit_entity_name_field))
elif hasattr(instance, '__str__'): elif hasattr(instance, "__str__"):
return str(instance) return str(instance)
else: else:
return f"{self.get_audit_entity_type()} #{instance.pk}" return f"{self.get_audit_entity_type()} #{instance.pk}"
@@ -224,7 +250,7 @@ class AuditLogMixin:
request=self.request, request=self.request,
entity_type=self.get_audit_entity_type(), entity_type=self.get_audit_entity_type(),
entity_id=instance.pk, entity_id=instance.pk,
entity_name=self.get_audit_entity_name(instance) entity_name=self.get_audit_entity_name(instance),
) )
def log_update_action(self, old_instance, new_instance): def log_update_action(self, old_instance, new_instance):
@@ -236,7 +262,7 @@ class AuditLogMixin:
entity_type=self.get_audit_entity_type(), entity_type=self.get_audit_entity_type(),
entity_id=new_instance.pk, entity_id=new_instance.pk,
entity_name=self.get_audit_entity_name(new_instance), entity_name=self.get_audit_entity_name(new_instance),
changes=changes changes=changes,
) )
def log_delete_action(self, instance): def log_delete_action(self, instance):
@@ -245,7 +271,7 @@ class AuditLogMixin:
request=self.request, request=self.request,
entity_type=self.get_audit_entity_type(), entity_type=self.get_audit_entity_type(),
entity_id=instance.pk, entity_id=instance.pk,
entity_name=self.get_audit_entity_name(instance) entity_name=self.get_audit_entity_name(instance),
) )
@@ -255,11 +281,11 @@ def log_login(request, user):
try: try:
return log_action( return log_action(
request=request, request=request,
action='login', action="login",
entity_type='user', entity_type="user",
entity_id=user.pk, entity_id=user.pk,
entity_name=user.get_username(), entity_name=user.get_username(),
description=f"User '{user.get_username()}' logged in" description=f"User '{user.get_username()}' logged in",
) )
except Exception: except Exception:
return None return None
@@ -268,14 +294,14 @@ def log_login(request, user):
def log_logout(request, user): def log_logout(request, user):
"""Log a successful user logout.""" """Log a successful user logout."""
try: try:
username = user.get_username() if user else 'Unknown' username = user.get_username() if user else "Unknown"
return log_action( return log_action(
request=request, request=request,
action='logout', action="logout",
entity_type='user', entity_type="user",
entity_id=getattr(user, 'pk', ''), entity_id=getattr(user, "pk", ""),
entity_name=username, entity_name=username,
description=f"User '{username}' logged out" description=f"User '{username}' logged out",
) )
except Exception: except Exception:
return None return None

View File

@@ -6,17 +6,19 @@ Handles creation and restoration of complete system backups
import os import os
import shutil import shutil
import subprocess import subprocess
import tempfile
import tarfile import tarfile
import tempfile
from datetime import datetime from datetime import datetime
from django.conf import settings from django.conf import settings
from django.utils import timezone from django.utils import timezone
from stiftung.models import BackupJob from stiftung.models import BackupJob
def get_backup_directory(): def get_backup_directory():
"""Get or create the backup directory""" """Get or create the backup directory"""
backup_dir = '/app/backups' backup_dir = "/app/backups"
os.makedirs(backup_dir, exist_ok=True) os.makedirs(backup_dir, exist_ok=True)
return backup_dir return backup_dir
@@ -28,28 +30,28 @@ def run_backup(backup_job_id):
""" """
try: try:
backup_job = BackupJob.objects.get(id=backup_job_id) backup_job = BackupJob.objects.get(id=backup_job_id)
backup_job.status = 'running' backup_job.status = "running"
backup_job.started_at = timezone.now() backup_job.started_at = timezone.now()
backup_job.save() backup_job.save()
backup_dir = get_backup_directory() backup_dir = get_backup_directory()
timestamp = datetime.now().strftime('%Y%m%d_%H%M%S') timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
backup_filename = f"stiftung_backup_{timestamp}.tar.gz" backup_filename = f"stiftung_backup_{timestamp}.tar.gz"
backup_path = os.path.join(backup_dir, backup_filename) backup_path = os.path.join(backup_dir, backup_filename)
# Create temporary directory for backup staging # Create temporary directory for backup staging
with tempfile.TemporaryDirectory() as temp_dir: with tempfile.TemporaryDirectory() as temp_dir:
staging_dir = os.path.join(temp_dir, 'backup_staging') staging_dir = os.path.join(temp_dir, "backup_staging")
os.makedirs(staging_dir) os.makedirs(staging_dir)
# 1. Database backup # 1. Database backup
if backup_job.backup_type in ['full', 'database']: if backup_job.backup_type in ["full", "database"]:
db_backup_path = create_database_backup(staging_dir) db_backup_path = create_database_backup(staging_dir)
if not db_backup_path: if not db_backup_path:
raise Exception("Database backup failed") raise Exception("Database backup failed")
# 2. Files backup # 2. Files backup
if backup_job.backup_type in ['full', 'files']: if backup_job.backup_type in ["full", "files"]:
files_backup_path = create_files_backup(staging_dir) files_backup_path = create_files_backup(staging_dir)
if not files_backup_path: if not files_backup_path:
raise Exception("Files backup failed") raise Exception("Files backup failed")
@@ -62,14 +64,14 @@ def run_backup(backup_job_id):
# 5. Update job status # 5. Update job status
backup_size = os.path.getsize(backup_path) backup_size = os.path.getsize(backup_path)
backup_job.status = 'completed' backup_job.status = "completed"
backup_job.completed_at = timezone.now() backup_job.completed_at = timezone.now()
backup_job.backup_filename = backup_filename backup_job.backup_filename = backup_filename
backup_job.backup_size = backup_size backup_job.backup_size = backup_size
backup_job.save() backup_job.save()
except Exception as e: except Exception as e:
backup_job.status = 'failed' backup_job.status = "failed"
backup_job.error_message = str(e) backup_job.error_message = str(e)
backup_job.completed_at = timezone.now() backup_job.completed_at = timezone.now()
backup_job.save() backup_job.save()
@@ -78,28 +80,33 @@ def run_backup(backup_job_id):
def create_database_backup(staging_dir): def create_database_backup(staging_dir):
"""Create a database backup using pg_dump""" """Create a database backup using pg_dump"""
try: try:
db_backup_file = os.path.join(staging_dir, 'database.sql') db_backup_file = os.path.join(staging_dir, "database.sql")
# Get database settings # Get database settings
db_settings = settings.DATABASES['default'] db_settings = settings.DATABASES["default"]
# Build pg_dump command # Build pg_dump command
cmd = [ cmd = [
'pg_dump', "pg_dump",
'--host', db_settings.get('HOST', 'localhost'), "--host",
'--port', str(db_settings.get('PORT', 5432)), db_settings.get("HOST", "localhost"),
'--username', db_settings.get('USER', 'postgres'), "--port",
'--format', 'custom', str(db_settings.get("PORT", 5432)),
'--no-owner', # portability across environments "--username",
'--no-privileges', # skip GRANT/REVOKE db_settings.get("USER", "postgres"),
'--no-password', "--format",
'--file', db_backup_file, "custom",
db_settings.get('NAME', 'stiftung') "--no-owner", # portability across environments
"--no-privileges", # skip GRANT/REVOKE
"--no-password",
"--file",
db_backup_file,
db_settings.get("NAME", "stiftung"),
] ]
# Set environment variables for authentication # Set environment variables for authentication
env = os.environ.copy() env = os.environ.copy()
env['PGPASSWORD'] = db_settings.get('PASSWORD', '') env["PGPASSWORD"] = db_settings.get("PASSWORD", "")
# Run pg_dump # Run pg_dump
result = subprocess.run(cmd, env=env, capture_output=True, text=True) result = subprocess.run(cmd, env=env, capture_output=True, text=True)
@@ -117,14 +124,14 @@ def create_database_backup(staging_dir):
def create_files_backup(staging_dir): def create_files_backup(staging_dir):
"""Create backup of application files""" """Create backup of application files"""
try: try:
files_dir = os.path.join(staging_dir, 'files') files_dir = os.path.join(staging_dir, "files")
os.makedirs(files_dir) os.makedirs(files_dir)
# Files to backup # Files to backup
backup_paths = [ backup_paths = [
'/app/media', # User uploads "/app/media", # User uploads
'/app/static', # Static files "/app/static", # Static files
'/app/.env', # Environment configuration "/app/.env", # Environment configuration
] ]
for source_path in backup_paths: for source_path in backup_paths:
@@ -149,24 +156,26 @@ def create_backup_metadata(staging_dir, backup_job):
import json import json
metadata = { metadata = {
'backup_id': str(backup_job.id), "backup_id": str(backup_job.id),
'backup_type': backup_job.backup_type, "backup_type": backup_job.backup_type,
'created_at': backup_job.created_at.isoformat(), "created_at": backup_job.created_at.isoformat(),
'created_by': backup_job.created_by.username if backup_job.created_by else 'system', "created_by": (
'django_version': '5.0.6', backup_job.created_by.username if backup_job.created_by else "system"
'app_version': '1.0.0', ),
'python_version': '3.12', "django_version": "5.0.6",
"app_version": "1.0.0",
"python_version": "3.12",
} }
metadata_file = os.path.join(staging_dir, 'backup_metadata.json') metadata_file = os.path.join(staging_dir, "backup_metadata.json")
with open(metadata_file, 'w') as f: with open(metadata_file, "w") as f:
json.dump(metadata, f, indent=2) json.dump(metadata, f, indent=2)
def create_compressed_backup(staging_dir, backup_path): def create_compressed_backup(staging_dir, backup_path):
"""Create compressed tar.gz archive""" """Create compressed tar.gz archive"""
with tarfile.open(backup_path, 'w:gz') as tar: with tarfile.open(backup_path, "w:gz") as tar:
tar.add(staging_dir, arcname='.') tar.add(staging_dir, arcname=".")
def run_restore(restore_job_id, backup_file_path): def run_restore(restore_job_id, backup_file_path):
@@ -176,46 +185,47 @@ def run_restore(restore_job_id, backup_file_path):
""" """
try: try:
restore_job = BackupJob.objects.get(id=restore_job_id) restore_job = BackupJob.objects.get(id=restore_job_id)
restore_job.status = 'running' restore_job.status = "running"
restore_job.started_at = timezone.now() restore_job.started_at = timezone.now()
restore_job.save() restore_job.save()
# Extract backup # Extract backup
with tempfile.TemporaryDirectory() as temp_dir: with tempfile.TemporaryDirectory() as temp_dir:
extract_dir = os.path.join(temp_dir, 'restore') extract_dir = os.path.join(temp_dir, "restore")
os.makedirs(extract_dir) os.makedirs(extract_dir)
# Extract tar.gz # Extract tar.gz
with tarfile.open(backup_file_path, 'r:gz') as tar: with tarfile.open(backup_file_path, "r:gz") as tar:
tar.extractall(extract_dir) tar.extractall(extract_dir)
# Validate backup # Validate backup
metadata_file = os.path.join(extract_dir, 'backup_metadata.json') metadata_file = os.path.join(extract_dir, "backup_metadata.json")
if not os.path.exists(metadata_file): if not os.path.exists(metadata_file):
raise Exception("Invalid backup: missing metadata") raise Exception("Invalid backup: missing metadata")
# Read metadata # Read metadata
import json import json
with open(metadata_file, 'r') as f:
with open(metadata_file, "r") as f:
metadata = json.load(f) metadata = json.load(f)
# Restore database # Restore database
db_backup_file = os.path.join(extract_dir, 'database.sql') db_backup_file = os.path.join(extract_dir, "database.sql")
if os.path.exists(db_backup_file): if os.path.exists(db_backup_file):
restore_database(db_backup_file) restore_database(db_backup_file)
# Restore files # Restore files
files_dir = os.path.join(extract_dir, 'files') files_dir = os.path.join(extract_dir, "files")
if os.path.exists(files_dir): if os.path.exists(files_dir):
restore_files(files_dir) restore_files(files_dir)
# Update job status # Update job status
restore_job.status = 'completed' restore_job.status = "completed"
restore_job.completed_at = timezone.now() restore_job.completed_at = timezone.now()
restore_job.save() restore_job.save()
except Exception as e: except Exception as e:
restore_job.status = 'failed' restore_job.status = "failed"
restore_job.error_message = str(e) restore_job.error_message = str(e)
restore_job.completed_at = timezone.now() restore_job.completed_at = timezone.now()
restore_job.save() restore_job.save()
@@ -225,38 +235,43 @@ def restore_database(db_backup_file):
"""Restore database from backup""" """Restore database from backup"""
try: try:
# Get database settings # Get database settings
db_settings = settings.DATABASES['default'] db_settings = settings.DATABASES["default"]
# Build pg_restore command # Build pg_restore command
cmd = [ cmd = [
'pg_restore', "pg_restore",
'--host', db_settings.get('HOST', 'localhost'), "--host",
'--port', str(db_settings.get('PORT', 5432)), db_settings.get("HOST", "localhost"),
'--username', db_settings.get('USER', 'postgres'), "--port",
'--dbname', db_settings.get('NAME', 'stiftung'), str(db_settings.get("PORT", 5432)),
'--clean', # Drop existing objects first "--username",
'--if-exists', # Don't error if objects don't exist db_settings.get("USER", "postgres"),
'--no-owner', # don't attempt to set original owners "--dbname",
'--role', db_settings.get('USER', 'postgres'), # set target owner db_settings.get("NAME", "stiftung"),
'--single-transaction', # restore atomically when possible "--clean", # Drop existing objects first
'--disable-triggers', # avoid FK issues during data load "--if-exists", # Don't error if objects don't exist
'--no-password', "--no-owner", # don't attempt to set original owners
'--verbose', "--role",
db_backup_file db_settings.get("USER", "postgres"), # set target owner
"--single-transaction", # restore atomically when possible
"--disable-triggers", # avoid FK issues during data load
"--no-password",
"--verbose",
db_backup_file,
] ]
# Set environment variables for authentication # Set environment variables for authentication
env = os.environ.copy() env = os.environ.copy()
env['PGPASSWORD'] = db_settings.get('PASSWORD', '') env["PGPASSWORD"] = db_settings.get("PASSWORD", "")
# Run pg_restore # Run pg_restore
result = subprocess.run(cmd, env=env, capture_output=True, text=True) result = subprocess.run(cmd, env=env, capture_output=True, text=True)
# Fail if there are real errors # Fail if there are real errors
if result.returncode != 0: if result.returncode != 0:
stderr = result.stderr or '' stderr = result.stderr or ""
# escalate only if we see ERROR # escalate only if we see ERROR
if 'ERROR' in stderr.upper(): if "ERROR" in stderr.upper():
raise Exception(f"pg_restore failed: {stderr}") raise Exception(f"pg_restore failed: {stderr}")
else: else:
print(f"pg_restore completed with warnings: {stderr}") print(f"pg_restore completed with warnings: {stderr}")
@@ -270,9 +285,9 @@ def restore_files(files_dir):
try: try:
# Restore paths # Restore paths
restore_mappings = { restore_mappings = {
'media': '/app/media', "media": "/app/media",
'static': '/app/static', "static": "/app/static",
'.env': '/app/.env', ".env": "/app/.env",
} }
for source_name, dest_path in restore_mappings.items(): for source_name, dest_path in restore_mappings.items():
@@ -281,7 +296,9 @@ def restore_files(files_dir):
if os.path.exists(source_path): if os.path.exists(source_path):
# Backup existing files first # Backup existing files first
if os.path.exists(dest_path): if os.path.exists(dest_path):
backup_path = f"{dest_path}.backup.{datetime.now().strftime('%Y%m%d_%H%M%S')}" backup_path = (
f"{dest_path}.backup.{datetime.now().strftime('%Y%m%d_%H%M%S')}"
)
if os.path.isdir(dest_path): if os.path.isdir(dest_path):
shutil.move(dest_path, backup_path) shutil.move(dest_path, backup_path)
else: else:
@@ -304,7 +321,7 @@ def cleanup_old_backups(keep_count=10):
backup_files = [] backup_files = []
for filename in os.listdir(backup_dir): for filename in os.listdir(backup_dir):
if filename.startswith('stiftung_backup_') and filename.endswith('.tar.gz'): if filename.startswith("stiftung_backup_") and filename.endswith(".tar.gz"):
filepath = os.path.join(backup_dir, filename) filepath = os.path.join(backup_dir, filename)
backup_files.append((filepath, os.path.getmtime(filepath))) backup_files.append((filepath, os.path.getmtime(filepath)))

File diff suppressed because it is too large Load Diff

View File

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

View File

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

View File

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

View File

@@ -1,30 +1,37 @@
import logging
from django.core.management.base import BaseCommand from django.core.management.base import BaseCommand
from django.db import transaction from django.db import transaction
from stiftung.models import Land, Verpachtung, Paechter
import logging from stiftung.models import Land, Paechter, Verpachtung
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
class Command(BaseCommand): class Command(BaseCommand):
help = 'Migriert bestehende Verpachtungen in die neue Land-Struktur' help = "Migriert bestehende Verpachtungen in die neue Land-Struktur"
def add_arguments(self, parser): def add_arguments(self, parser):
parser.add_argument( parser.add_argument(
'--dry-run', "--dry-run",
action='store_true', action="store_true",
help='Zeigt nur an, was gemacht würde, ohne Änderungen zu speichern', help="Zeigt nur an, was gemacht würde, ohne Änderungen zu speichern",
) )
def handle(self, *args, **options): def handle(self, *args, **options):
dry_run = options['dry_run'] dry_run = options["dry_run"]
if dry_run: if dry_run:
self.stdout.write(self.style.WARNING('DRY RUN - Keine Änderungen werden gespeichert!')) self.stdout.write(
self.style.WARNING("DRY RUN - Keine Änderungen werden gespeichert!")
)
# Alle aktiven Verpachtungen finden # Alle aktiven Verpachtungen finden
aktive_verpachtungen = Verpachtung.objects.filter(status='aktiv') aktive_verpachtungen = Verpachtung.objects.filter(status="aktiv")
self.stdout.write(f'Gefunden: {aktive_verpachtungen.count()} aktive Verpachtungen') self.stdout.write(
f"Gefunden: {aktive_verpachtungen.count()} aktive Verpachtungen"
)
migrated_count = 0 migrated_count = 0
skipped_count = 0 skipped_count = 0
@@ -37,20 +44,22 @@ class Command(BaseCommand):
if land.aktueller_paechter is not None: if land.aktueller_paechter is not None:
self.stdout.write( self.stdout.write(
self.style.WARNING( self.style.WARNING(
f'Übersprungen: {land} hat bereits einen aktuellen Pächter' f"Übersprungen: {land} hat bereits einen aktuellen Pächter"
) )
) )
skipped_count += 1 skipped_count += 1
continue continue
# Migration durchführen # Migration durchführen
self.stdout.write(f'Migriere: {land} -> {verpachtung.paechter}') self.stdout.write(f"Migriere: {land} -> {verpachtung.paechter}")
if not dry_run: if not dry_run:
# Pächter-Daten ins Land übertragen # Pächter-Daten ins Land übertragen
land.aktueller_paechter = verpachtung.paechter land.aktueller_paechter = verpachtung.paechter
land.paechter_name = verpachtung.paechter.get_full_name() land.paechter_name = verpachtung.paechter.get_full_name()
land.paechter_anschrift = self._get_paechter_anschrift(verpachtung.paechter) land.paechter_anschrift = self._get_paechter_anschrift(
verpachtung.paechter
)
land.pachtbeginn = verpachtung.pachtbeginn land.pachtbeginn = verpachtung.pachtbeginn
land.pachtende = verpachtung.pachtende land.pachtende = verpachtung.pachtende
land.verlaengerung_klausel = bool(verpachtung.verlaengerung) land.verlaengerung_klausel = bool(verpachtung.verlaengerung)
@@ -69,13 +78,13 @@ class Command(BaseCommand):
if dry_run: if dry_run:
self.stdout.write( self.stdout.write(
self.style.SUCCESS( self.style.SUCCESS(
f'DRY RUN abgeschlossen: {migrated_count} Verpachtungen würden migriert, {skipped_count} übersprungen' f"DRY RUN abgeschlossen: {migrated_count} Verpachtungen würden migriert, {skipped_count} übersprungen"
) )
) )
else: else:
self.stdout.write( self.stdout.write(
self.style.SUCCESS( self.style.SUCCESS(
f'Migration abgeschlossen: {migrated_count} Verpachtungen migriert, {skipped_count} übersprungen' f"Migration abgeschlossen: {migrated_count} Verpachtungen migriert, {skipped_count} übersprungen"
) )
) )
@@ -89,4 +98,4 @@ class Command(BaseCommand):
elif paechter.ort: elif paechter.ort:
parts.append(paechter.ort) parts.append(paechter.ort)
return '\n'.join(parts) if parts else '' return "\n".join(parts) if parts else ""

View File

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

View File

@@ -1,36 +1,43 @@
import logging
from datetime import datetime
from django.core.management.base import BaseCommand from django.core.management.base import BaseCommand
from django.db import transaction from django.db import transaction
from stiftung.models import Land, Verpachtung, Paechter, LandAbrechnung
from datetime import datetime from stiftung.models import Land, LandAbrechnung, Paechter, Verpachtung
import logging
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
class Command(BaseCommand): class Command(BaseCommand):
help = 'Vereinheitlicht Verpachtungen, Land und Abrechnungen zu einem konsistenten System' help = "Vereinheitlicht Verpachtungen, Land und Abrechnungen zu einem konsistenten System"
def add_arguments(self, parser): def add_arguments(self, parser):
parser.add_argument( parser.add_argument(
'--dry-run', "--dry-run",
action='store_true', action="store_true",
help='Zeigt nur an, was gemacht würde, ohne Änderungen zu speichern', help="Zeigt nur an, was gemacht würde, ohne Änderungen zu speichern",
) )
parser.add_argument( parser.add_argument(
'--create-abrechnungen', "--create-abrechnungen",
action='store_true', action="store_true",
help='Erstellt automatisch Abrechnungen aus Verpachtungsdaten', help="Erstellt automatisch Abrechnungen aus Verpachtungsdaten",
) )
def handle(self, *args, **options): def handle(self, *args, **options):
dry_run = options['dry_run'] dry_run = options["dry_run"]
create_abrechnungen = options['create_abrechnungen'] create_abrechnungen = options["create_abrechnungen"]
if dry_run: if dry_run:
self.stdout.write(self.style.WARNING('DRY RUN - Keine Änderungen werden gespeichert!')) self.stdout.write(
self.style.WARNING("DRY RUN - Keine Änderungen werden gespeichert!")
)
# Schritt 1: Alle Verpachtungen analysieren # Schritt 1: Alle Verpachtungen analysieren
alle_verpachtungen = Verpachtung.objects.all().order_by('land', '-pachtbeginn') alle_verpachtungen = Verpachtung.objects.all().order_by("land", "-pachtbeginn")
self.stdout.write(f'Gefunden: {alle_verpachtungen.count()} Verpachtungen insgesamt') self.stdout.write(
f"Gefunden: {alle_verpachtungen.count()} Verpachtungen insgesamt"
)
land_updates = 0 land_updates = 0
abrechnungen_created = 0 abrechnungen_created = 0
@@ -46,14 +53,18 @@ class Command(BaseCommand):
current_land = land current_land = land
# Prüfen ob dies die neueste aktive Verpachtung ist # Prüfen ob dies die neueste aktive Verpachtung ist
if verpachtung.status == 'aktiv' and not land.aktueller_paechter: if verpachtung.status == "aktiv" and not land.aktueller_paechter:
self.stdout.write(f'Setze aktuelle Verpachtung: {land} -> {verpachtung.paechter}') self.stdout.write(
f"Setze aktuelle Verpachtung: {land} -> {verpachtung.paechter}"
)
if not dry_run: if not dry_run:
# Land-Felder aktualisieren # Land-Felder aktualisieren
land.aktueller_paechter = verpachtung.paechter land.aktueller_paechter = verpachtung.paechter
land.paechter_name = verpachtung.paechter.get_full_name() land.paechter_name = verpachtung.paechter.get_full_name()
land.paechter_anschrift = self._get_paechter_anschrift(verpachtung.paechter) land.paechter_anschrift = self._get_paechter_anschrift(
verpachtung.paechter
)
land.pachtbeginn = verpachtung.pachtbeginn land.pachtbeginn = verpachtung.pachtbeginn
land.pachtende = verpachtung.pachtende land.pachtende = verpachtung.pachtende
land.verlaengerung_klausel = bool(verpachtung.verlaengerung) land.verlaengerung_klausel = bool(verpachtung.verlaengerung)
@@ -66,45 +77,50 @@ class Command(BaseCommand):
land_updates += 1 land_updates += 1
# Schritt 2: Abrechnungen aus Verpachtungen erstellen (optional) # Schritt 2: Abrechnungen aus Verpachtungen erstellen (optional)
if create_abrechnungen and verpachtung.status == 'aktiv': if create_abrechnungen and verpachtung.status == "aktiv":
# Erstelle Abrechnungen für die letzten 3 Jahre # Erstelle Abrechnungen für die letzten 3 Jahre
current_year = datetime.now().year current_year = datetime.now().year
for jahr in range(current_year - 2, current_year + 1): for jahr in range(current_year - 2, current_year + 1):
# Prüfen ob Abrechnung bereits existiert # Prüfen ob Abrechnung bereits existiert
existing = LandAbrechnung.objects.filter( existing = LandAbrechnung.objects.filter(
land=land, land=land, abrechnungsjahr=jahr
abrechnungsjahr=jahr
).first() ).first()
if not existing: if not existing:
self.stdout.write(f'Erstelle Abrechnung: {land} - {jahr}') self.stdout.write(f"Erstelle Abrechnung: {land} - {jahr}")
if not dry_run: if not dry_run:
abrechnung = LandAbrechnung.objects.create( abrechnung = LandAbrechnung.objects.create(
land=land, land=land,
abrechnungsjahr=jahr, abrechnungsjahr=jahr,
pacht_vereinnahmt=verpachtung.pachtzins_jaehrlich, pacht_vereinnahmt=verpachtung.pachtzins_jaehrlich,
bemerkungen=f'Automatisch erstellt aus Verpachtung {verpachtung.vertragsnummer}' bemerkungen=f"Automatisch erstellt aus Verpachtung {verpachtung.vertragsnummer}",
) )
abrechnungen_created += 1 abrechnungen_created += 1
# Zusammenfassung # Zusammenfassung
self.stdout.write(self.style.SUCCESS('\n=== MIGRATION ABGESCHLOSSEN ===')) self.stdout.write(self.style.SUCCESS("\n=== MIGRATION ABGESCHLOSSEN ==="))
if dry_run: if dry_run:
self.stdout.write(f'DRY RUN: {land_updates} Länder würden aktualisiert') self.stdout.write(f"DRY RUN: {land_updates} Länder würden aktualisiert")
if create_abrechnungen: if create_abrechnungen:
self.stdout.write(f'DRY RUN: {abrechnungen_created} Abrechnungen würden erstellt') self.stdout.write(
f"DRY RUN: {abrechnungen_created} Abrechnungen würden erstellt"
)
else: else:
self.stdout.write(f'{land_updates} Länder aktualisiert') self.stdout.write(f"{land_updates} Länder aktualisiert")
if create_abrechnungen: if create_abrechnungen:
self.stdout.write(f'{abrechnungen_created} Abrechnungen erstellt') self.stdout.write(f"{abrechnungen_created} Abrechnungen erstellt")
# Empfehlungen # Empfehlungen
self.stdout.write(self.style.WARNING('\n=== NÄCHSTE SCHRITTE ===')) self.stdout.write(self.style.WARNING("\n=== NÄCHSTE SCHRITTE ==="))
self.stdout.write('1. Prüfen Sie die migrierten Daten in der Weboberfläche') self.stdout.write("1. Prüfen Sie die migrierten Daten in der Weboberfläche")
self.stdout.write('2. Alte Verpachtungs-Views können als "Legacy" markiert werden') self.stdout.write(
self.stdout.write('3. Neue Verpachtungen sollten direkt im Land-Model erstellt werden') '2. Alte Verpachtungs-Views können als "Legacy" markiert werden'
)
self.stdout.write(
"3. Neue Verpachtungen sollten direkt im Land-Model erstellt werden"
)
def _get_paechter_anschrift(self, paechter): def _get_paechter_anschrift(self, paechter):
"""Erstellt eine Anschrift aus den Pächter-Daten""" """Erstellt eine Anschrift aus den Pächter-Daten"""
@@ -116,4 +132,4 @@ class Command(BaseCommand):
elif paechter.ort: elif paechter.ort:
parts.append(paechter.ort) parts.append(paechter.ort)
return '\n'.join(parts) if parts else '' return "\n".join(parts) if parts else ""

View File

@@ -4,11 +4,13 @@ Automatically tracks all model changes throughout the application
""" """
import threading import threading
from django.utils.deprecation import MiddlewareMixin
from django.contrib.auth.signals import user_logged_in, user_logged_out from django.contrib.auth.signals import user_logged_in, user_logged_out
from django.db.models.signals import post_save, post_delete, pre_save from django.db.models.signals import post_delete, post_save, pre_save
from django.dispatch import receiver from django.dispatch import receiver
from stiftung.audit import log_action, track_model_changes, get_client_ip from django.utils.deprecation import MiddlewareMixin
from stiftung.audit import get_client_ip, log_action, track_model_changes
# Thread-local storage for request context # Thread-local storage for request context
_local = threading.local() _local = threading.local()
@@ -27,16 +29,16 @@ class AuditMiddleware(MiddlewareMixin):
def process_response(self, request, response): def process_response(self, request, response):
"""Clean up thread-local storage""" """Clean up thread-local storage"""
if hasattr(_local, 'request'): if hasattr(_local, "request"):
delattr(_local, 'request') delattr(_local, "request")
if hasattr(_local, 'user_changes'): if hasattr(_local, "user_changes"):
delattr(_local, 'user_changes') delattr(_local, "user_changes")
return response return response
def get_current_request(): def get_current_request():
"""Get the current request from thread-local storage""" """Get the current request from thread-local storage"""
return getattr(_local, 'request', None) return getattr(_local, "request", None)
def get_entity_type_from_model(model): def get_entity_type_from_model(model):
@@ -44,28 +46,28 @@ def get_entity_type_from_model(model):
model_name = model.__name__.lower() model_name = model.__name__.lower()
mapping = { mapping = {
'destinataer': 'destinataer', "destinataer": "destinataer",
'land': 'land', "land": "land",
'paechter': 'paechter', "paechter": "paechter",
'verpachtung': 'verpachtung', "verpachtung": "verpachtung",
'foerderung': 'foerderung', "foerderung": "foerderung",
'rentmeister': 'rentmeister', "rentmeister": "rentmeister",
'stiftungskonto': 'stiftungskonto', "stiftungskonto": "stiftungskonto",
'verwaltungskosten': 'verwaltungskosten', "verwaltungskosten": "verwaltungskosten",
'banktransaction': 'banktransaction', "banktransaction": "banktransaction",
'dokumentlink': 'dokumentlink', "dokumentlink": "dokumentlink",
'user': 'user', "user": "user",
'person': 'destinataer', # Legacy model maps to destinataer "person": "destinataer", # Legacy model maps to destinataer
} }
return mapping.get(model_name, 'unknown') return mapping.get(model_name, "unknown")
def get_entity_name(instance): def get_entity_name(instance):
"""Get a human-readable name for an entity""" """Get a human-readable name for an entity"""
if hasattr(instance, 'get_full_name') and callable(instance.get_full_name): if hasattr(instance, "get_full_name") and callable(instance.get_full_name):
return instance.get_full_name() return instance.get_full_name()
elif hasattr(instance, '__str__'): elif hasattr(instance, "__str__"):
return str(instance) return str(instance)
else: else:
return f"{instance.__class__.__name__} #{instance.pk}" return f"{instance.__class__.__name__} #{instance.pk}"
@@ -76,7 +78,7 @@ def get_entity_name(instance):
def store_pre_save_state(sender, instance, **kwargs): def store_pre_save_state(sender, instance, **kwargs):
"""Store the pre-save state for change tracking""" """Store the pre-save state for change tracking"""
request = get_current_request() request = get_current_request()
if not request or not hasattr(request, 'user'): if not request or not hasattr(request, "user"):
return return
# Skip if user is not authenticated # Skip if user is not authenticated
@@ -84,14 +86,14 @@ def store_pre_save_state(sender, instance, **kwargs):
return return
# Skip audit log entries themselves to avoid infinite loops # Skip audit log entries themselves to avoid infinite loops
if sender.__name__ == 'AuditLog': if sender.__name__ == "AuditLog":
return return
# Store the current state if this is an update # Store the current state if this is an update
if instance.pk: if instance.pk:
try: try:
old_instance = sender.objects.get(pk=instance.pk) old_instance = sender.objects.get(pk=instance.pk)
if not hasattr(_local, 'user_changes'): if not hasattr(_local, "user_changes"):
_local.user_changes = {} _local.user_changes = {}
_local.user_changes[instance.pk] = old_instance _local.user_changes[instance.pk] = old_instance
except sender.DoesNotExist: except sender.DoesNotExist:
@@ -102,7 +104,7 @@ def store_pre_save_state(sender, instance, **kwargs):
def log_model_save(sender, instance, created, **kwargs): def log_model_save(sender, instance, created, **kwargs):
"""Log model creation and updates""" """Log model creation and updates"""
request = get_current_request() request = get_current_request()
if not request or not hasattr(request, 'user'): if not request or not hasattr(request, "user"):
return return
# Skip if user is not authenticated # Skip if user is not authenticated
@@ -110,11 +112,11 @@ def log_model_save(sender, instance, created, **kwargs):
return return
# Skip audit log entries themselves to avoid infinite loops # Skip audit log entries themselves to avoid infinite loops
if sender.__name__ == 'AuditLog': if sender.__name__ == "AuditLog":
return return
# Skip certain system models # Skip certain system models
if sender.__name__ in ['Session', 'LogEntry', 'ContentType', 'Permission']: if sender.__name__ in ["Session", "LogEntry", "ContentType", "Permission"]:
return return
entity_type = get_entity_type_from_model(sender) entity_type = get_entity_type_from_model(sender)
@@ -126,16 +128,16 @@ def log_model_save(sender, instance, created, **kwargs):
description = f"Neue {entity_type.replace('_', ' ').title()} '{entity_name}' wurde erstellt" description = f"Neue {entity_type.replace('_', ' ').title()} '{entity_name}' wurde erstellt"
log_action( log_action(
request=request, request=request,
action='create', action="create",
entity_type=entity_type, entity_type=entity_type,
entity_id=entity_id, entity_id=entity_id,
entity_name=entity_name, entity_name=entity_name,
description=description description=description,
) )
else: else:
# Log update with changes # Log update with changes
changes = {} changes = {}
if hasattr(_local, 'user_changes') and instance.pk in _local.user_changes: if hasattr(_local, "user_changes") and instance.pk in _local.user_changes:
old_instance = _local.user_changes[instance.pk] old_instance = _local.user_changes[instance.pk]
changes = track_model_changes(old_instance, instance) changes = track_model_changes(old_instance, instance)
@@ -143,12 +145,12 @@ def log_model_save(sender, instance, created, **kwargs):
description = f"{entity_type.replace('_', ' ').title()} '{entity_name}' wurde aktualisiert" description = f"{entity_type.replace('_', ' ').title()} '{entity_name}' wurde aktualisiert"
log_action( log_action(
request=request, request=request,
action='update', action="update",
entity_type=entity_type, entity_type=entity_type,
entity_id=entity_id, entity_id=entity_id,
entity_name=entity_name, entity_name=entity_name,
description=description, description=description,
changes=changes changes=changes,
) )
@@ -156,7 +158,7 @@ def log_model_save(sender, instance, created, **kwargs):
def log_model_delete(sender, instance, **kwargs): def log_model_delete(sender, instance, **kwargs):
"""Log model deletion""" """Log model deletion"""
request = get_current_request() request = get_current_request()
if not request or not hasattr(request, 'user'): if not request or not hasattr(request, "user"):
return return
# Skip if user is not authenticated # Skip if user is not authenticated
@@ -164,25 +166,27 @@ def log_model_delete(sender, instance, **kwargs):
return return
# Skip audit log entries themselves # Skip audit log entries themselves
if sender.__name__ == 'AuditLog': if sender.__name__ == "AuditLog":
return return
# Skip certain system models # Skip certain system models
if sender.__name__ in ['Session', 'LogEntry', 'ContentType', 'Permission']: if sender.__name__ in ["Session", "LogEntry", "ContentType", "Permission"]:
return return
entity_type = get_entity_type_from_model(sender) entity_type = get_entity_type_from_model(sender)
entity_name = get_entity_name(instance) entity_name = get_entity_name(instance)
entity_id = str(instance.pk) entity_id = str(instance.pk)
description = f"{entity_type.replace('_', ' ').title()} '{entity_name}' wurde gelöscht" description = (
f"{entity_type.replace('_', ' ').title()} '{entity_name}' wurde gelöscht"
)
log_action( log_action(
request=request, request=request,
action='delete', action="delete",
entity_type=entity_type, entity_type=entity_type,
entity_id=entity_id, entity_id=entity_id,
entity_name=entity_name, entity_name=entity_name,
description=description description=description,
) )
@@ -192,11 +196,11 @@ def log_user_login(sender, request, user, **kwargs):
"""Log user login""" """Log user login"""
log_action( log_action(
request=request, request=request,
action='login', action="login",
entity_type='user', entity_type="user",
entity_id=str(user.pk), entity_id=str(user.pk),
entity_name=user.username, entity_name=user.username,
description=f"Benutzer {user.username} hat sich angemeldet" description=f"Benutzer {user.username} hat sich angemeldet",
) )
@@ -206,9 +210,9 @@ def log_user_logout(sender, request, user, **kwargs):
if user: # user might be None if session expired if user: # user might be None if session expired
log_action( log_action(
request=request, request=request,
action='logout', action="logout",
entity_type='user', entity_type="user",
entity_id=str(user.pk), entity_id=str(user.pk),
entity_name=user.username, entity_name=user.username,
description=f"Benutzer {user.username} hat sich abgemeldet" description=f"Benutzer {user.username} hat sich abgemeldet",
) )

View File

@@ -1,7 +1,8 @@
# Generated by Django 5.0.6 on 2025-08-13 20:59 # Generated by Django 5.0.6 on 2025-08-13 20:59
import django.db.models.deletion
import uuid import uuid
import django.db.models.deletion
from django.db import migrations, models from django.db import migrations, models
@@ -9,39 +10,76 @@ class Migration(migrations.Migration):
initial = True initial = True
dependencies = [ dependencies = []
]
operations = [ operations = [
migrations.CreateModel( migrations.CreateModel(
name='DokumentLink', name="DokumentLink",
fields=[ fields=[
('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)), (
('paperless_document_id', models.IntegerField()), "id",
('kontext', models.CharField(max_length=30)), models.UUIDField(
('titel', models.CharField(max_length=255)), default=uuid.uuid4,
editable=False,
primary_key=True,
serialize=False,
),
),
("paperless_document_id", models.IntegerField()),
("kontext", models.CharField(max_length=30)),
("titel", models.CharField(max_length=255)),
], ],
), ),
migrations.CreateModel( migrations.CreateModel(
name='Person', name="Person",
fields=[ fields=[
('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)), (
('familienzweig', models.CharField(max_length=100)), "id",
('vorname', models.CharField(max_length=100)), models.UUIDField(
('nachname', models.CharField(max_length=100)), default=uuid.uuid4,
('geburtsdatum', models.DateField(blank=True, null=True)), editable=False,
('email', models.EmailField(blank=True, max_length=254, null=True)), primary_key=True,
('iban', models.CharField(blank=True, max_length=34, null=True)), serialize=False,
),
),
("familienzweig", models.CharField(max_length=100)),
("vorname", models.CharField(max_length=100)),
("nachname", models.CharField(max_length=100)),
("geburtsdatum", models.DateField(blank=True, null=True)),
("email", models.EmailField(blank=True, max_length=254, null=True)),
("iban", models.CharField(blank=True, max_length=34, null=True)),
], ],
), ),
migrations.CreateModel( migrations.CreateModel(
name='Foerderung', name="Foerderung",
fields=[ fields=[
('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)), (
('jahr', models.IntegerField()), "id",
('betrag', models.DecimalField(decimal_places=2, max_digits=12)), models.UUIDField(
('verwendungsnachweis', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='stiftung.dokumentlink')), default=uuid.uuid4,
('person', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='stiftung.person')), editable=False,
primary_key=True,
serialize=False,
),
),
("jahr", models.IntegerField()),
("betrag", models.DecimalField(decimal_places=2, max_digits=12)),
(
"verwendungsnachweis",
models.ForeignKey(
blank=True,
null=True,
on_delete=django.db.models.deletion.SET_NULL,
to="stiftung.dokumentlink",
),
),
(
"person",
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
to="stiftung.person",
),
),
], ],
), ),
] ]

View File

@@ -9,104 +9,172 @@ from django.db import migrations, models
class Migration(migrations.Migration): class Migration(migrations.Migration):
dependencies = [ dependencies = [
('stiftung', '0001_initial'), ("stiftung", "0001_initial"),
] ]
operations = [ operations = [
migrations.AlterModelOptions( migrations.AlterModelOptions(
name='dokumentlink', name="dokumentlink",
options={'ordering': ['titel'], 'verbose_name': 'Dokument', 'verbose_name_plural': 'Dokumente'}, options={
"ordering": ["titel"],
"verbose_name": "Dokument",
"verbose_name_plural": "Dokumente",
},
), ),
migrations.AlterModelOptions( migrations.AlterModelOptions(
name='foerderung', name="foerderung",
options={'ordering': ['-jahr', '-betrag'], 'verbose_name': 'Förderung', 'verbose_name_plural': 'Förderungen'}, options={
"ordering": ["-jahr", "-betrag"],
"verbose_name": "Förderung",
"verbose_name_plural": "Förderungen",
},
), ),
migrations.AlterModelOptions( migrations.AlterModelOptions(
name='person', name="person",
options={'ordering': ['nachname', 'vorname'], 'verbose_name': 'Person', 'verbose_name_plural': 'Personen'}, options={
"ordering": ["nachname", "vorname"],
"verbose_name": "Person",
"verbose_name_plural": "Personen",
},
), ),
migrations.AddField( migrations.AddField(
model_name='dokumentlink', model_name="dokumentlink",
name='beschreibung', name="beschreibung",
field=models.TextField(blank=True, null=True), field=models.TextField(blank=True, null=True),
), ),
migrations.AddField( migrations.AddField(
model_name='foerderung', model_name="foerderung",
name='antragsdatum', name="antragsdatum",
field=models.DateField(default=django.utils.timezone.now), field=models.DateField(default=django.utils.timezone.now),
), ),
migrations.AddField( migrations.AddField(
model_name='foerderung', model_name="foerderung",
name='bemerkungen', name="bemerkungen",
field=models.TextField(blank=True, null=True), field=models.TextField(blank=True, null=True),
), ),
migrations.AddField( migrations.AddField(
model_name='foerderung', model_name="foerderung",
name='entscheidungsdatum', name="entscheidungsdatum",
field=models.DateField(blank=True, null=True), field=models.DateField(blank=True, null=True),
), ),
migrations.AddField( migrations.AddField(
model_name='foerderung', model_name="foerderung",
name='kategorie', name="kategorie",
field=models.CharField(choices=[('bildung', 'Bildung'), ('forschung', 'Forschung'), ('kultur', 'Kultur'), ('soziales', 'Soziales'), ('umwelt', 'Umwelt'), ('anderes', 'Anderes')], default='anderes', max_length=20), field=models.CharField(
choices=[
("bildung", "Bildung"),
("forschung", "Forschung"),
("kultur", "Kultur"),
("soziales", "Soziales"),
("umwelt", "Umwelt"),
("anderes", "Anderes"),
],
default="anderes",
max_length=20,
),
), ),
migrations.AddField( migrations.AddField(
model_name='foerderung', model_name="foerderung",
name='status', name="status",
field=models.CharField(choices=[('beantragt', 'Beantragt'), ('genehmigt', 'Genehmigt'), ('ausgezahlt', 'Ausgezahlt'), ('abgelehnt', 'Abgelehnt'), ('storniert', 'Storniert')], default='beantragt', max_length=20), field=models.CharField(
choices=[
("beantragt", "Beantragt"),
("genehmigt", "Genehmigt"),
("ausgezahlt", "Ausgezahlt"),
("abgelehnt", "Abgelehnt"),
("storniert", "Storniert"),
],
default="beantragt",
max_length=20,
),
), ),
migrations.AddField( migrations.AddField(
model_name='person', model_name="person",
name='adresse', name="adresse",
field=models.TextField(blank=True, null=True), field=models.TextField(blank=True, null=True),
), ),
migrations.AddField( migrations.AddField(
model_name='person', model_name="person",
name='aktiv', name="aktiv",
field=models.BooleanField(default=True), field=models.BooleanField(default=True),
), ),
migrations.AddField( migrations.AddField(
model_name='person', model_name="person",
name='notizen', name="notizen",
field=models.TextField(blank=True, null=True), field=models.TextField(blank=True, null=True),
), ),
migrations.AddField( migrations.AddField(
model_name='person', model_name="person",
name='telefon', name="telefon",
field=models.CharField(blank=True, max_length=20, null=True), field=models.CharField(blank=True, max_length=20, null=True),
), ),
migrations.AlterField( migrations.AlterField(
model_name='dokumentlink', model_name="dokumentlink",
name='kontext', name="kontext",
field=models.CharField(choices=[('antrag', 'Antrag'), ('verwendungsnachweis', 'Verwendungsnachweis'), ('rechnung', 'Rechnung'), ('vertrag', 'Vertrag'), ('bericht', 'Bericht'), ('anderes', 'Anderes')], default='anderes', max_length=30), field=models.CharField(
choices=[
("antrag", "Antrag"),
("verwendungsnachweis", "Verwendungsnachweis"),
("rechnung", "Rechnung"),
("vertrag", "Vertrag"),
("bericht", "Bericht"),
("anderes", "Anderes"),
],
default="anderes",
max_length=30,
),
), ),
migrations.AlterField( migrations.AlterField(
model_name='dokumentlink', model_name="dokumentlink",
name='paperless_document_id', name="paperless_document_id",
field=models.IntegerField(unique=True), field=models.IntegerField(unique=True),
), ),
migrations.AlterField( migrations.AlterField(
model_name='foerderung', model_name="foerderung",
name='jahr', name="jahr",
field=models.IntegerField(validators=[django.core.validators.MinValueValidator(1900), django.core.validators.MaxValueValidator(2100)]), field=models.IntegerField(
validators=[
django.core.validators.MinValueValidator(1900),
django.core.validators.MaxValueValidator(2100),
]
),
), ),
migrations.AlterField( migrations.AlterField(
model_name='foerderung', model_name="foerderung",
name='person', name="person",
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='stiftung.person', verbose_name='Person'), field=models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
to="stiftung.person",
verbose_name="Person",
),
), ),
migrations.AlterField( migrations.AlterField(
model_name='foerderung', model_name="foerderung",
name='verwendungsnachweis', name="verwendungsnachweis",
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='stiftung.dokumentlink', verbose_name='Verwendungsnachweis'), field=models.ForeignKey(
blank=True,
null=True,
on_delete=django.db.models.deletion.SET_NULL,
to="stiftung.dokumentlink",
verbose_name="Verwendungsnachweis",
),
), ),
migrations.AlterField( migrations.AlterField(
model_name='person', model_name="person",
name='familienzweig', name="familienzweig",
field=models.CharField(choices=[('hauptzweig', 'Hauptzweig'), ('nebenzweig', 'Nebenzweig'), ('verwandt', 'Verwandt'), ('anderer', 'Anderer')], default='hauptzweig', max_length=100), field=models.CharField(
choices=[
("hauptzweig", "Hauptzweig"),
("nebenzweig", "Nebenzweig"),
("verwandt", "Verwandt"),
("anderer", "Anderer"),
],
default="hauptzweig",
max_length=100,
),
), ),
migrations.AlterUniqueTogether( migrations.AlterUniqueTogether(
name='foerderung', name="foerderung",
unique_together={('person', 'jahr', 'kategorie')}, unique_together={("person", "jahr", "kategorie")},
), ),
] ]

View File

@@ -1,78 +1,293 @@
# Generated by Django 5.0.6 on 2025-08-13 21:43 # Generated by Django 5.0.6 on 2025-08-13 21:43
import uuid
import django.core.validators import django.core.validators
import django.db.models.deletion import django.db.models.deletion
import uuid
from django.db import migrations, models from django.db import migrations, models
class Migration(migrations.Migration): class Migration(migrations.Migration):
dependencies = [ dependencies = [
('stiftung', '0002_alter_dokumentlink_options_alter_foerderung_options_and_more'), (
"stiftung",
"0002_alter_dokumentlink_options_alter_foerderung_options_and_more",
),
] ]
operations = [ operations = [
migrations.CreateModel( migrations.CreateModel(
name='Land', name="Land",
fields=[ fields=[
('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)), (
('lfd_nr', models.CharField(max_length=20, unique=True, verbose_name='Lfd. Nr.')), "id",
('ew_nummer', models.CharField(blank=True, max_length=50, null=True, verbose_name='EW-Nummer')), models.UUIDField(
('amtsgericht', models.CharField(max_length=100, verbose_name='Amtsgericht')), default=uuid.uuid4,
('gemeinde', models.CharField(max_length=100, verbose_name='Gemeinde')), editable=False,
('gemarkung', models.CharField(max_length=100, verbose_name='Gemarkung')), primary_key=True,
('flur', models.CharField(max_length=50, verbose_name='Flur')), serialize=False,
('flurstueck', models.CharField(max_length=50, verbose_name='Flurstück')), ),
('groesse_qm', models.DecimalField(decimal_places=2, max_digits=12, validators=[django.core.validators.MinValueValidator(0.01)], verbose_name='Größe in qm')), ),
('gruenland_qm', models.DecimalField(decimal_places=2, default=0, max_digits=12, validators=[django.core.validators.MinValueValidator(0)], verbose_name='Grünland (qm)')), (
('acker_qm', models.DecimalField(decimal_places=2, default=0, max_digits=12, validators=[django.core.validators.MinValueValidator(0)], verbose_name='Acker (qm)')), "lfd_nr",
('wald_qm', models.DecimalField(decimal_places=2, default=0, max_digits=12, validators=[django.core.validators.MinValueValidator(0)], verbose_name='Wald (qm)')), models.CharField(
('sonstiges_qm', models.DecimalField(decimal_places=2, default=0, max_digits=12, validators=[django.core.validators.MinValueValidator(0)], verbose_name='Sonstiges (qm)')), max_length=20, unique=True, verbose_name="Lfd. Nr."
('verpachtete_gesamtflaeche', models.DecimalField(decimal_places=2, max_digits=12, validators=[django.core.validators.MinValueValidator(0)], verbose_name='Verpachtete Gesamtfläche (qm)')), ),
('flaeche_alte_liste', models.DecimalField(blank=True, decimal_places=2, max_digits=12, null=True, verbose_name='Fläche alte Liste (qm)')), ),
('verp_flaeche_aktuell', models.DecimalField(decimal_places=2, max_digits=12, validators=[django.core.validators.MinValueValidator(0)], verbose_name='Verp. Fläche aktuell (qm)')), (
('anteil_grundsteuer', models.DecimalField(blank=True, decimal_places=2, max_digits=8, null=True, verbose_name='Anteil Grundsteuer (%)')), "ew_nummer",
('anteil_lwk', models.DecimalField(blank=True, decimal_places=2, max_digits=8, null=True, verbose_name='Anteil LWK (%)')), models.CharField(
('aktiv', models.BooleanField(default=True, verbose_name='Aktiv')), blank=True, max_length=50, null=True, verbose_name="EW-Nummer"
('notizen', models.TextField(blank=True, null=True, verbose_name='Ergänzende Kommentare')), ),
('erstellt_am', models.DateTimeField(auto_now_add=True)), ),
('aktualisiert_am', models.DateTimeField(auto_now=True)), (
"amtsgericht",
models.CharField(max_length=100, verbose_name="Amtsgericht"),
),
("gemeinde", models.CharField(max_length=100, verbose_name="Gemeinde")),
(
"gemarkung",
models.CharField(max_length=100, verbose_name="Gemarkung"),
),
("flur", models.CharField(max_length=50, verbose_name="Flur")),
(
"flurstueck",
models.CharField(max_length=50, verbose_name="Flurstück"),
),
(
"groesse_qm",
models.DecimalField(
decimal_places=2,
max_digits=12,
validators=[django.core.validators.MinValueValidator(0.01)],
verbose_name="Größe in qm",
),
),
(
"gruenland_qm",
models.DecimalField(
decimal_places=2,
default=0,
max_digits=12,
validators=[django.core.validators.MinValueValidator(0)],
verbose_name="Grünland (qm)",
),
),
(
"acker_qm",
models.DecimalField(
decimal_places=2,
default=0,
max_digits=12,
validators=[django.core.validators.MinValueValidator(0)],
verbose_name="Acker (qm)",
),
),
(
"wald_qm",
models.DecimalField(
decimal_places=2,
default=0,
max_digits=12,
validators=[django.core.validators.MinValueValidator(0)],
verbose_name="Wald (qm)",
),
),
(
"sonstiges_qm",
models.DecimalField(
decimal_places=2,
default=0,
max_digits=12,
validators=[django.core.validators.MinValueValidator(0)],
verbose_name="Sonstiges (qm)",
),
),
(
"verpachtete_gesamtflaeche",
models.DecimalField(
decimal_places=2,
max_digits=12,
validators=[django.core.validators.MinValueValidator(0)],
verbose_name="Verpachtete Gesamtfläche (qm)",
),
),
(
"flaeche_alte_liste",
models.DecimalField(
blank=True,
decimal_places=2,
max_digits=12,
null=True,
verbose_name="Fläche alte Liste (qm)",
),
),
(
"verp_flaeche_aktuell",
models.DecimalField(
decimal_places=2,
max_digits=12,
validators=[django.core.validators.MinValueValidator(0)],
verbose_name="Verp. Fläche aktuell (qm)",
),
),
(
"anteil_grundsteuer",
models.DecimalField(
blank=True,
decimal_places=2,
max_digits=8,
null=True,
verbose_name="Anteil Grundsteuer (%)",
),
),
(
"anteil_lwk",
models.DecimalField(
blank=True,
decimal_places=2,
max_digits=8,
null=True,
verbose_name="Anteil LWK (%)",
),
),
("aktiv", models.BooleanField(default=True, verbose_name="Aktiv")),
(
"notizen",
models.TextField(
blank=True, null=True, verbose_name="Ergänzende Kommentare"
),
),
("erstellt_am", models.DateTimeField(auto_now_add=True)),
("aktualisiert_am", models.DateTimeField(auto_now=True)),
], ],
options={ options={
'verbose_name': 'Land', "verbose_name": "Land",
'verbose_name_plural': 'Ländereien', "verbose_name_plural": "Ländereien",
'ordering': ['gemeinde', 'gemarkung', 'flur', 'flurstueck'], "ordering": ["gemeinde", "gemarkung", "flur", "flurstueck"],
}, },
), ),
migrations.AlterField( migrations.AlterField(
model_name='dokumentlink', model_name="dokumentlink",
name='kontext', name="kontext",
field=models.CharField(choices=[('antrag', 'Antrag'), ('verwendungsnachweis', 'Verwendungsnachweis'), ('rechnung', 'Rechnung'), ('vertrag', 'Vertrag'), ('bericht', 'Bericht'), ('landkarte', 'Landkarte'), ('kataster', 'Kataster'), ('anderes', 'Anderes')], default='anderes', max_length=30), field=models.CharField(
choices=[
("antrag", "Antrag"),
("verwendungsnachweis", "Verwendungsnachweis"),
("rechnung", "Rechnung"),
("vertrag", "Vertrag"),
("bericht", "Bericht"),
("landkarte", "Landkarte"),
("kataster", "Kataster"),
("anderes", "Anderes"),
],
default="anderes",
max_length=30,
),
), ),
migrations.CreateModel( migrations.CreateModel(
name='Verpachtung', name="Verpachtung",
fields=[ fields=[
('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)), (
('vertragsnummer', models.CharField(max_length=50, unique=True, verbose_name='Vertragsnummer')), "id",
('pachtbeginn', models.DateField(verbose_name='Pachtbeginn')), models.UUIDField(
('pachtende', models.DateField(verbose_name='Pachtende')), default=uuid.uuid4,
('verlaengerung', models.DateField(blank=True, null=True, verbose_name='Verlängerung bis')), editable=False,
('pachtzins_pro_qm', models.DecimalField(decimal_places=4, max_digits=8, verbose_name='Pachtzins pro qm (€)')), primary_key=True,
('pachtzins_jaehrlich', models.DecimalField(decimal_places=2, max_digits=12, verbose_name='Jährlicher Pachtzins (€)')), serialize=False,
('verpachtete_flaeche', models.DecimalField(decimal_places=2, max_digits=12, verbose_name='Verpachtete Fläche (qm)')), ),
('status', models.CharField(choices=[('aktiv', 'Aktiv'), ('beendet', 'Beendet'), ('gekuendigt', 'Gekündigt'), ('verlängert', 'Verlängert')], default='aktiv', max_length=20)), ),
('bemerkungen', models.TextField(blank=True, null=True, verbose_name='Ergänzende Kommentare')), (
('erstellt_am', models.DateTimeField(auto_now_add=True)), "vertragsnummer",
('aktualisiert_am', models.DateTimeField(auto_now=True)), models.CharField(
('land', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='stiftung.land', verbose_name='Land')), max_length=50, unique=True, verbose_name="Vertragsnummer"
('paechter', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='stiftung.person', verbose_name='Pächter')), ),
('verwendungsnachweis', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='stiftung.dokumentlink', verbose_name='Verwendungsnachweis')), ),
("pachtbeginn", models.DateField(verbose_name="Pachtbeginn")),
("pachtende", models.DateField(verbose_name="Pachtende")),
(
"verlaengerung",
models.DateField(
blank=True, null=True, verbose_name="Verlängerung bis"
),
),
(
"pachtzins_pro_qm",
models.DecimalField(
decimal_places=4,
max_digits=8,
verbose_name="Pachtzins pro qm (€)",
),
),
(
"pachtzins_jaehrlich",
models.DecimalField(
decimal_places=2,
max_digits=12,
verbose_name="Jährlicher Pachtzins (€)",
),
),
(
"verpachtete_flaeche",
models.DecimalField(
decimal_places=2,
max_digits=12,
verbose_name="Verpachtete Fläche (qm)",
),
),
(
"status",
models.CharField(
choices=[
("aktiv", "Aktiv"),
("beendet", "Beendet"),
("gekuendigt", "Gekündigt"),
("verlängert", "Verlängert"),
],
default="aktiv",
max_length=20,
),
),
(
"bemerkungen",
models.TextField(
blank=True, null=True, verbose_name="Ergänzende Kommentare"
),
),
("erstellt_am", models.DateTimeField(auto_now_add=True)),
("aktualisiert_am", models.DateTimeField(auto_now=True)),
(
"land",
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
to="stiftung.land",
verbose_name="Land",
),
),
(
"paechter",
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
to="stiftung.person",
verbose_name="Pächter",
),
),
(
"verwendungsnachweis",
models.ForeignKey(
blank=True,
null=True,
on_delete=django.db.models.deletion.SET_NULL,
to="stiftung.dokumentlink",
verbose_name="Verwendungsnachweis",
),
),
], ],
options={ options={
'verbose_name': 'Verpachtung', "verbose_name": "Verpachtung",
'verbose_name_plural': 'Verpachtungen', "verbose_name_plural": "Verpachtungen",
'ordering': ['-pachtbeginn'], "ordering": ["-pachtbeginn"],
}, },
), ),
] ]

View File

@@ -1,36 +1,106 @@
# Generated by Django 5.0.6 on 2025-08-13 22:18 # Generated by Django 5.0.6 on 2025-08-13 22:18
import uuid import uuid
from django.db import migrations, models from django.db import migrations, models
class Migration(migrations.Migration): class Migration(migrations.Migration):
dependencies = [ dependencies = [
('stiftung', '0003_land_alter_dokumentlink_kontext_verpachtung'), ("stiftung", "0003_land_alter_dokumentlink_kontext_verpachtung"),
] ]
operations = [ operations = [
migrations.CreateModel( migrations.CreateModel(
name='CSVImport', name="CSVImport",
fields=[ fields=[
('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)), (
('import_type', models.CharField(choices=[('personen', 'Personen'), ('laendereien', 'Ländereien'), ('verpachtungen', 'Verpachtungen')], max_length=20, verbose_name='Import-Typ')), "id",
('filename', models.CharField(max_length=255, verbose_name='Dateiname')), models.UUIDField(
('file_size', models.IntegerField(verbose_name='Dateigröße (Bytes)')), default=uuid.uuid4,
('status', models.CharField(choices=[('pending', 'Ausstehend'), ('processing', 'Wird verarbeitet'), ('completed', 'Abgeschlossen'), ('failed', 'Fehlgeschlagen'), ('partial', 'Teilweise erfolgreich')], default='pending', max_length=20)), editable=False,
('total_rows', models.IntegerField(default=0, verbose_name='Gesamtzeilen')), primary_key=True,
('imported_rows', models.IntegerField(default=0, verbose_name='Importierte Zeilen')), serialize=False,
('failed_rows', models.IntegerField(default=0, verbose_name='Fehlgeschlagene Zeilen')), ),
('error_log', models.TextField(blank=True, null=True, verbose_name='Fehlerprotokoll')), ),
('created_by', models.CharField(blank=True, max_length=100, null=True, verbose_name='Erstellt von')), (
('started_at', models.DateTimeField(auto_now_add=True, verbose_name='Gestartet um')), "import_type",
('completed_at', models.DateTimeField(blank=True, null=True, verbose_name='Abgeschlossen um')), models.CharField(
choices=[
("personen", "Personen"),
("laendereien", "Ländereien"),
("verpachtungen", "Verpachtungen"),
],
max_length=20,
verbose_name="Import-Typ",
),
),
(
"filename",
models.CharField(max_length=255, verbose_name="Dateiname"),
),
("file_size", models.IntegerField(verbose_name="Dateigröße (Bytes)")),
(
"status",
models.CharField(
choices=[
("pending", "Ausstehend"),
("processing", "Wird verarbeitet"),
("completed", "Abgeschlossen"),
("failed", "Fehlgeschlagen"),
("partial", "Teilweise erfolgreich"),
],
default="pending",
max_length=20,
),
),
(
"total_rows",
models.IntegerField(default=0, verbose_name="Gesamtzeilen"),
),
(
"imported_rows",
models.IntegerField(default=0, verbose_name="Importierte Zeilen"),
),
(
"failed_rows",
models.IntegerField(
default=0, verbose_name="Fehlgeschlagene Zeilen"
),
),
(
"error_log",
models.TextField(
blank=True, null=True, verbose_name="Fehlerprotokoll"
),
),
(
"created_by",
models.CharField(
blank=True,
max_length=100,
null=True,
verbose_name="Erstellt von",
),
),
(
"started_at",
models.DateTimeField(
auto_now_add=True, verbose_name="Gestartet um"
),
),
(
"completed_at",
models.DateTimeField(
blank=True, null=True, verbose_name="Abgeschlossen um"
),
),
], ],
options={ options={
'verbose_name': 'CSV Import', "verbose_name": "CSV Import",
'verbose_name_plural': 'CSV Imports', "verbose_name_plural": "CSV Imports",
'ordering': ['-started_at'], "ordering": ["-started_at"],
}, },
), ),
] ]

View File

@@ -1,93 +1,298 @@
# Generated by Django 5.0.6 on 2025-08-14 10:38 # Generated by Django 5.0.6 on 2025-08-14 10:38
import django.db.models.deletion
import uuid import uuid
import django.db.models.deletion
from django.db import migrations, models from django.db import migrations, models
class Migration(migrations.Migration): class Migration(migrations.Migration):
dependencies = [ dependencies = [
('stiftung', '0004_csvimport'), ("stiftung", "0004_csvimport"),
] ]
operations = [ operations = [
migrations.CreateModel( migrations.CreateModel(
name='Destinataer', name="Destinataer",
fields=[ fields=[
('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)), (
('familienzweig', models.CharField(choices=[('hauptzweig', 'Hauptzweig'), ('nebenzweig', 'Nebenzweig'), ('verwandt', 'Verwandt'), ('anderer', 'Anderer')], default='hauptzweig', max_length=100)), "id",
('vorname', models.CharField(max_length=100, verbose_name='Vorname')), models.UUIDField(
('nachname', models.CharField(max_length=100, verbose_name='Nachname')), default=uuid.uuid4,
('geburtsdatum', models.DateField(blank=True, null=True, verbose_name='Geburtsdatum')), editable=False,
('email', models.EmailField(blank=True, max_length=254, null=True, verbose_name='E-Mail')), primary_key=True,
('telefon', models.CharField(blank=True, max_length=20, null=True, verbose_name='Telefon')), serialize=False,
('iban', models.CharField(blank=True, max_length=34, null=True, verbose_name='IBAN')), ),
('adresse', models.TextField(blank=True, null=True, verbose_name='Adresse')), ),
('berufsgruppe', models.CharField(choices=[('student', 'Student/Studentin'), ('wissenschaftler', 'Wissenschaftler/in'), ('künstler', 'Künstler/in'), ('sozialarbeiter', 'Sozialarbeiter/in'), ('umweltschützer', 'Umweltschützer/in'), ('andere', 'Andere')], default='andere', max_length=20, verbose_name='Berufsgruppe')), (
('ausbildungsstand', models.CharField(blank=True, max_length=100, null=True, verbose_name='Ausbildungsstand')), "familienzweig",
('institution', models.CharField(blank=True, max_length=200, null=True, verbose_name='Institution/Organisation')), models.CharField(
('projekt_beschreibung', models.TextField(blank=True, null=True, verbose_name='Projektbeschreibung')), choices=[
('jaehrliches_einkommen', models.DecimalField(blank=True, decimal_places=2, max_digits=12, null=True, verbose_name='Jährliches Einkommen (€)')), ("hauptzweig", "Hauptzweig"),
('finanzielle_notlage', models.BooleanField(default=False, verbose_name='Finanzielle Notlage')), ("nebenzweig", "Nebenzweig"),
('notizen', models.TextField(blank=True, null=True, verbose_name='Notizen')), ("verwandt", "Verwandt"),
('aktiv', models.BooleanField(default=True, verbose_name='Aktiv')), ("anderer", "Anderer"),
],
default="hauptzweig",
max_length=100,
),
),
("vorname", models.CharField(max_length=100, verbose_name="Vorname")),
("nachname", models.CharField(max_length=100, verbose_name="Nachname")),
(
"geburtsdatum",
models.DateField(
blank=True, null=True, verbose_name="Geburtsdatum"
),
),
(
"email",
models.EmailField(
blank=True, max_length=254, null=True, verbose_name="E-Mail"
),
),
(
"telefon",
models.CharField(
blank=True, max_length=20, null=True, verbose_name="Telefon"
),
),
(
"iban",
models.CharField(
blank=True, max_length=34, null=True, verbose_name="IBAN"
),
),
(
"adresse",
models.TextField(blank=True, null=True, verbose_name="Adresse"),
),
(
"berufsgruppe",
models.CharField(
choices=[
("student", "Student/Studentin"),
("wissenschaftler", "Wissenschaftler/in"),
("künstler", "Künstler/in"),
("sozialarbeiter", "Sozialarbeiter/in"),
("umweltschützer", "Umweltschützer/in"),
("andere", "Andere"),
],
default="andere",
max_length=20,
verbose_name="Berufsgruppe",
),
),
(
"ausbildungsstand",
models.CharField(
blank=True,
max_length=100,
null=True,
verbose_name="Ausbildungsstand",
),
),
(
"institution",
models.CharField(
blank=True,
max_length=200,
null=True,
verbose_name="Institution/Organisation",
),
),
(
"projekt_beschreibung",
models.TextField(
blank=True, null=True, verbose_name="Projektbeschreibung"
),
),
(
"jaehrliches_einkommen",
models.DecimalField(
blank=True,
decimal_places=2,
max_digits=12,
null=True,
verbose_name="Jährliches Einkommen (€)",
),
),
(
"finanzielle_notlage",
models.BooleanField(
default=False, verbose_name="Finanzielle Notlage"
),
),
(
"notizen",
models.TextField(blank=True, null=True, verbose_name="Notizen"),
),
("aktiv", models.BooleanField(default=True, verbose_name="Aktiv")),
], ],
options={ options={
'verbose_name': 'Destinatär', "verbose_name": "Destinatär",
'verbose_name_plural': 'Destinatäre', "verbose_name_plural": "Destinatäre",
'ordering': ['nachname', 'vorname'], "ordering": ["nachname", "vorname"],
}, },
), ),
migrations.CreateModel( migrations.CreateModel(
name='Paechter', name="Paechter",
fields=[ fields=[
('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)), (
('familienzweig', models.CharField(choices=[('hauptzweig', 'Hauptzweig'), ('nebenzweig', 'Nebenzweig'), ('verwandt', 'Verwandt'), ('anderer', 'Anderer')], default='hauptzweig', max_length=100)), "id",
('vorname', models.CharField(max_length=100, verbose_name='Vorname')), models.UUIDField(
('nachname', models.CharField(max_length=100, verbose_name='Nachname')), default=uuid.uuid4,
('geburtsdatum', models.DateField(blank=True, null=True, verbose_name='Geburtsdatum')), editable=False,
('email', models.EmailField(blank=True, max_length=254, null=True, verbose_name='E-Mail')), primary_key=True,
('telefon', models.CharField(blank=True, max_length=20, null=True, verbose_name='Telefon')), serialize=False,
('iban', models.CharField(blank=True, max_length=34, null=True, verbose_name='IBAN')), ),
('adresse', models.TextField(blank=True, null=True, verbose_name='Adresse')), ),
('pachtnummer', models.CharField(blank=True, max_length=50, null=True, verbose_name='Pachtnummer')), (
('pachtbeginn_erste', models.DateField(blank=True, null=True, verbose_name='Erster Pachtbeginn')), "familienzweig",
('pachtende_letzte', models.DateField(blank=True, null=True, verbose_name='Letztes Pachtende')), models.CharField(
('pachtzins_aktuell', models.DecimalField(blank=True, decimal_places=2, max_digits=12, null=True, verbose_name='Aktueller Pachtzins (€/Jahr)')), choices=[
('landwirtschaftliche_ausbildung', models.BooleanField(default=False, verbose_name='Landwirtschaftliche Ausbildung')), ("hauptzweig", "Hauptzweig"),
('berufserfahrung_jahre', models.IntegerField(blank=True, null=True, verbose_name='Berufserfahrung (Jahre)')), ("nebenzweig", "Nebenzweig"),
('spezialisierung', models.CharField(blank=True, max_length=100, null=True, verbose_name='Spezialisierung')), ("verwandt", "Verwandt"),
('notizen', models.TextField(blank=True, null=True, verbose_name='Notizen')), ("anderer", "Anderer"),
('aktiv', models.BooleanField(default=True, verbose_name='Aktiv')), ],
default="hauptzweig",
max_length=100,
),
),
("vorname", models.CharField(max_length=100, verbose_name="Vorname")),
("nachname", models.CharField(max_length=100, verbose_name="Nachname")),
(
"geburtsdatum",
models.DateField(
blank=True, null=True, verbose_name="Geburtsdatum"
),
),
(
"email",
models.EmailField(
blank=True, max_length=254, null=True, verbose_name="E-Mail"
),
),
(
"telefon",
models.CharField(
blank=True, max_length=20, null=True, verbose_name="Telefon"
),
),
(
"iban",
models.CharField(
blank=True, max_length=34, null=True, verbose_name="IBAN"
),
),
(
"adresse",
models.TextField(blank=True, null=True, verbose_name="Adresse"),
),
(
"pachtnummer",
models.CharField(
blank=True, max_length=50, null=True, verbose_name="Pachtnummer"
),
),
(
"pachtbeginn_erste",
models.DateField(
blank=True, null=True, verbose_name="Erster Pachtbeginn"
),
),
(
"pachtende_letzte",
models.DateField(
blank=True, null=True, verbose_name="Letztes Pachtende"
),
),
(
"pachtzins_aktuell",
models.DecimalField(
blank=True,
decimal_places=2,
max_digits=12,
null=True,
verbose_name="Aktueller Pachtzins (€/Jahr)",
),
),
(
"landwirtschaftliche_ausbildung",
models.BooleanField(
default=False, verbose_name="Landwirtschaftliche Ausbildung"
),
),
(
"berufserfahrung_jahre",
models.IntegerField(
blank=True, null=True, verbose_name="Berufserfahrung (Jahre)"
),
),
(
"spezialisierung",
models.CharField(
blank=True,
max_length=100,
null=True,
verbose_name="Spezialisierung",
),
),
(
"notizen",
models.TextField(blank=True, null=True, verbose_name="Notizen"),
),
("aktiv", models.BooleanField(default=True, verbose_name="Aktiv")),
], ],
options={ options={
'verbose_name': 'Pächter', "verbose_name": "Pächter",
'verbose_name_plural': 'Pächter', "verbose_name_plural": "Pächter",
'ordering': ['nachname', 'vorname'], "ordering": ["nachname", "vorname"],
}, },
), ),
migrations.AlterModelOptions( migrations.AlterModelOptions(
name='person', name="person",
options={'ordering': ['nachname', 'vorname'], 'verbose_name': 'Person (Legacy)', 'verbose_name_plural': 'Personen (Legacy)'}, options={
"ordering": ["nachname", "vorname"],
"verbose_name": "Person (Legacy)",
"verbose_name_plural": "Personen (Legacy)",
},
), ),
migrations.AlterUniqueTogether( migrations.AlterUniqueTogether(
name='foerderung', name="foerderung",
unique_together=set(), unique_together=set(),
), ),
migrations.AlterField( migrations.AlterField(
model_name='foerderung', model_name="foerderung",
name='person', name="person",
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='stiftung.person', verbose_name='Person (Legacy)'), field=models.ForeignKey(
blank=True,
null=True,
on_delete=django.db.models.deletion.CASCADE,
to="stiftung.person",
verbose_name="Person (Legacy)",
),
), ),
migrations.AddField( migrations.AddField(
model_name='foerderung', model_name="foerderung",
name='destinataer', name="destinataer",
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='stiftung.destinataer', verbose_name='Destinatär'), field=models.ForeignKey(
blank=True,
null=True,
on_delete=django.db.models.deletion.CASCADE,
to="stiftung.destinataer",
verbose_name="Destinatär",
),
), ),
migrations.AlterField( migrations.AlterField(
model_name='verpachtung', model_name="verpachtung",
name='paechter', name="paechter",
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='stiftung.paechter', verbose_name='Pächter'), field=models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
to="stiftung.paechter",
verbose_name="Pächter",
),
), ),
] ]

View File

@@ -6,17 +6,27 @@ from django.db import migrations, models
class Migration(migrations.Migration): class Migration(migrations.Migration):
dependencies = [ dependencies = [
('stiftung', '0005_destinataer_paechter_alter_person_options_and_more'), ("stiftung", "0005_destinataer_paechter_alter_person_options_and_more"),
] ]
operations = [ operations = [
migrations.RemoveField( migrations.RemoveField(
model_name='paechter', model_name="paechter",
name='familienzweig', name="familienzweig",
), ),
migrations.AlterField( migrations.AlterField(
model_name='csvimport', model_name="csvimport",
name='import_type', name="import_type",
field=models.CharField(choices=[('destinataere', 'Destinatäre'), ('paechter', 'Pächter'), ('laendereien', 'Ländereien'), ('verpachtungen', 'Verpachtungen'), ('personen', 'Personen (Legacy)')], max_length=20, verbose_name='Import-Typ'), field=models.CharField(
choices=[
("destinataere", "Destinatäre"),
("paechter", "Pächter"),
("laendereien", "Ländereien"),
("verpachtungen", "Verpachtungen"),
("personen", "Personen (Legacy)"),
],
max_length=20,
verbose_name="Import-Typ",
),
), ),
] ]

View File

@@ -6,51 +6,71 @@ from django.db import migrations, models
class Migration(migrations.Migration): class Migration(migrations.Migration):
dependencies = [ dependencies = [
('stiftung', '0006_remove_paechter_familienzweig_and_more'), ("stiftung", "0006_remove_paechter_familienzweig_and_more"),
] ]
operations = [ operations = [
migrations.RemoveField( migrations.RemoveField(
model_name='destinataer', model_name="destinataer",
name='adresse', name="adresse",
), ),
migrations.RemoveField( migrations.RemoveField(
model_name='paechter', model_name="paechter",
name='adresse', name="adresse",
), ),
migrations.AddField( migrations.AddField(
model_name='destinataer', model_name="destinataer",
name='ort', name="ort",
field=models.CharField(blank=True, max_length=100, null=True, verbose_name='Ort'), field=models.CharField(
blank=True, max_length=100, null=True, verbose_name="Ort"
),
), ),
migrations.AddField( migrations.AddField(
model_name='destinataer', model_name="destinataer",
name='plz', name="plz",
field=models.CharField(blank=True, max_length=10, null=True, verbose_name='PLZ'), field=models.CharField(
blank=True, max_length=10, null=True, verbose_name="PLZ"
),
), ),
migrations.AddField( migrations.AddField(
model_name='destinataer', model_name="destinataer",
name='strasse', name="strasse",
field=models.CharField(blank=True, max_length=200, null=True, verbose_name='Straße'), field=models.CharField(
blank=True, max_length=200, null=True, verbose_name="Straße"
),
), ),
migrations.AddField( migrations.AddField(
model_name='paechter', model_name="paechter",
name='ort', name="ort",
field=models.CharField(blank=True, max_length=100, null=True, verbose_name='Ort'), field=models.CharField(
blank=True, max_length=100, null=True, verbose_name="Ort"
),
), ),
migrations.AddField( migrations.AddField(
model_name='paechter', model_name="paechter",
name='personentyp', name="personentyp",
field=models.CharField(choices=[('natuerlich', 'Natürliche Person'), ('gesellschaft', 'Gesellschaft (GmbH, KG, etc.)')], default='natuerlich', max_length=20, verbose_name='Typ des Pächters'), field=models.CharField(
choices=[
("natuerlich", "Natürliche Person"),
("gesellschaft", "Gesellschaft (GmbH, KG, etc.)"),
],
default="natuerlich",
max_length=20,
verbose_name="Typ des Pächters",
),
), ),
migrations.AddField( migrations.AddField(
model_name='paechter', model_name="paechter",
name='plz', name="plz",
field=models.CharField(blank=True, max_length=10, null=True, verbose_name='PLZ'), field=models.CharField(
blank=True, max_length=10, null=True, verbose_name="PLZ"
),
), ),
migrations.AddField( migrations.AddField(
model_name='paechter', model_name="paechter",
name='strasse', name="strasse",
field=models.CharField(blank=True, max_length=200, null=True, verbose_name='Straße'), field=models.CharField(
blank=True, max_length=200, null=True, verbose_name="Straße"
),
), ),
] ]

View File

@@ -6,38 +6,57 @@ from django.db import migrations, models
class Migration(migrations.Migration): class Migration(migrations.Migration):
dependencies = [ dependencies = [
('stiftung', '0007_remove_destinataer_adresse_remove_paechter_adresse_and_more'), (
"stiftung",
"0007_remove_destinataer_adresse_remove_paechter_adresse_and_more",
),
] ]
operations = [ operations = [
migrations.AddField( migrations.AddField(
model_name='dokumentlink', model_name="dokumentlink",
name='destinataer_id', name="destinataer_id",
field=models.UUIDField(blank=True, null=True, verbose_name='Destinatär ID'), field=models.UUIDField(blank=True, null=True, verbose_name="Destinatär ID"),
), ),
migrations.AddField( migrations.AddField(
model_name='dokumentlink', model_name="dokumentlink",
name='foerderung_id', name="foerderung_id",
field=models.UUIDField(blank=True, null=True, verbose_name='Förderung ID'), field=models.UUIDField(blank=True, null=True, verbose_name="Förderung ID"),
), ),
migrations.AddField( migrations.AddField(
model_name='dokumentlink', model_name="dokumentlink",
name='land_id', name="land_id",
field=models.UUIDField(blank=True, null=True, verbose_name='Länderei ID'), field=models.UUIDField(blank=True, null=True, verbose_name="Länderei ID"),
), ),
migrations.AddField( migrations.AddField(
model_name='dokumentlink', model_name="dokumentlink",
name='paechter_id', name="paechter_id",
field=models.UUIDField(blank=True, null=True, verbose_name='Pächter ID'), field=models.UUIDField(blank=True, null=True, verbose_name="Pächter ID"),
), ),
migrations.AddField( migrations.AddField(
model_name='dokumentlink', model_name="dokumentlink",
name='verpachtung_id', name="verpachtung_id",
field=models.UUIDField(blank=True, null=True, verbose_name='Verpachtung ID'), field=models.UUIDField(
blank=True, null=True, verbose_name="Verpachtung ID"
),
), ),
migrations.AlterField( migrations.AlterField(
model_name='dokumentlink', model_name="dokumentlink",
name='kontext', name="kontext",
field=models.CharField(choices=[('pachtvertrag', 'Pachtvertrag'), ('antrag', 'Antrag'), ('verwendungsnachweis', 'Verwendungsnachweis'), ('rechnung', 'Rechnung'), ('vertrag', 'Vertrag'), ('bericht', 'Bericht'), ('landkarte', 'Landkarte'), ('kataster', 'Kataster'), ('anderes', 'Anderes')], default='anderes', max_length=30), field=models.CharField(
choices=[
("pachtvertrag", "Pachtvertrag"),
("antrag", "Antrag"),
("verwendungsnachweis", "Verwendungsnachweis"),
("rechnung", "Rechnung"),
("vertrag", "Vertrag"),
("bericht", "Bericht"),
("landkarte", "Landkarte"),
("kataster", "Kataster"),
("anderes", "Anderes"),
],
default="anderes",
max_length=30,
),
), ),
] ]

View File

@@ -6,13 +6,13 @@ from django.db import migrations, models
class Migration(migrations.Migration): class Migration(migrations.Migration):
dependencies = [ dependencies = [
('stiftung', '0008_dokumentlink_destinataer_id_and_more'), ("stiftung", "0008_dokumentlink_destinataer_id_and_more"),
] ]
operations = [ operations = [
migrations.AlterField( migrations.AlterField(
model_name='dokumentlink', model_name="dokumentlink",
name='paperless_document_id', name="paperless_document_id",
field=models.IntegerField(), field=models.IntegerField(),
), ),
] ]

View File

@@ -1,100 +1,344 @@
# Generated by Django 5.0.6 on 2025-08-24 17:48 # Generated by Django 5.0.6 on 2025-08-24 17:48
import django.db.models.deletion
import uuid import uuid
import django.db.models.deletion
from django.db import migrations, models from django.db import migrations, models
class Migration(migrations.Migration): class Migration(migrations.Migration):
dependencies = [ dependencies = [
('stiftung', '0009_alter_dokumentlink_paperless_document_id'), ("stiftung", "0009_alter_dokumentlink_paperless_document_id"),
] ]
operations = [ operations = [
migrations.CreateModel( migrations.CreateModel(
name='Rentmeister', name="Rentmeister",
fields=[ fields=[
('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)), (
('anrede', models.CharField(blank=True, choices=[('herr', 'Herr'), ('frau', 'Frau'), ('dr', 'Dr.'), ('prof', 'Prof.'), ('prof_dr', 'Prof. Dr.')], max_length=10, verbose_name='Anrede')), "id",
('vorname', models.CharField(max_length=100, verbose_name='Vorname')), models.UUIDField(
('nachname', models.CharField(max_length=100, verbose_name='Nachname')), default=uuid.uuid4,
('titel', models.CharField(blank=True, max_length=50, verbose_name='Titel')), editable=False,
('email', models.EmailField(blank=True, max_length=254, verbose_name='E-Mail')), primary_key=True,
('telefon', models.CharField(blank=True, max_length=20, verbose_name='Telefon')), serialize=False,
('mobil', models.CharField(blank=True, max_length=20, verbose_name='Mobil')), ),
('strasse', models.CharField(blank=True, max_length=200, verbose_name='Straße')), ),
('plz', models.CharField(blank=True, max_length=10, verbose_name='PLZ')), (
('ort', models.CharField(blank=True, max_length=100, verbose_name='Ort')), "anrede",
('iban', models.CharField(blank=True, max_length=34, verbose_name='IBAN')), models.CharField(
('bic', models.CharField(blank=True, max_length=11, verbose_name='BIC')), blank=True,
('bank_name', models.CharField(blank=True, max_length=100, verbose_name='Bank')), choices=[
('seit_datum', models.DateField(verbose_name='Rentmeister seit')), ("herr", "Herr"),
('bis_datum', models.DateField(blank=True, null=True, verbose_name='Rentmeister bis')), ("frau", "Frau"),
('aktiv', models.BooleanField(default=True, verbose_name='Aktiv')), ("dr", "Dr."),
('monatliche_verguetung', models.DecimalField(blank=True, decimal_places=2, max_digits=8, null=True, verbose_name='Monatliche Vergütung (€)')), ("prof", "Prof."),
('km_pauschale', models.DecimalField(decimal_places=2, default=0.3, max_digits=4, verbose_name='Kilometerpauschale (€/km)')), ("prof_dr", "Prof. Dr."),
('notizen', models.TextField(blank=True, verbose_name='Notizen')), ],
('erstellt_am', models.DateTimeField(auto_now_add=True)), max_length=10,
('aktualisiert_am', models.DateTimeField(auto_now=True)), verbose_name="Anrede",
),
),
("vorname", models.CharField(max_length=100, verbose_name="Vorname")),
("nachname", models.CharField(max_length=100, verbose_name="Nachname")),
(
"titel",
models.CharField(blank=True, max_length=50, verbose_name="Titel"),
),
(
"email",
models.EmailField(
blank=True, max_length=254, verbose_name="E-Mail"
),
),
(
"telefon",
models.CharField(blank=True, max_length=20, verbose_name="Telefon"),
),
(
"mobil",
models.CharField(blank=True, max_length=20, verbose_name="Mobil"),
),
(
"strasse",
models.CharField(blank=True, max_length=200, verbose_name="Straße"),
),
(
"plz",
models.CharField(blank=True, max_length=10, verbose_name="PLZ"),
),
(
"ort",
models.CharField(blank=True, max_length=100, verbose_name="Ort"),
),
(
"iban",
models.CharField(blank=True, max_length=34, verbose_name="IBAN"),
),
(
"bic",
models.CharField(blank=True, max_length=11, verbose_name="BIC"),
),
(
"bank_name",
models.CharField(blank=True, max_length=100, verbose_name="Bank"),
),
("seit_datum", models.DateField(verbose_name="Rentmeister seit")),
(
"bis_datum",
models.DateField(
blank=True, null=True, verbose_name="Rentmeister bis"
),
),
("aktiv", models.BooleanField(default=True, verbose_name="Aktiv")),
(
"monatliche_verguetung",
models.DecimalField(
blank=True,
decimal_places=2,
max_digits=8,
null=True,
verbose_name="Monatliche Vergütung (€)",
),
),
(
"km_pauschale",
models.DecimalField(
decimal_places=2,
default=0.3,
max_digits=4,
verbose_name="Kilometerpauschale (€/km)",
),
),
("notizen", models.TextField(blank=True, verbose_name="Notizen")),
("erstellt_am", models.DateTimeField(auto_now_add=True)),
("aktualisiert_am", models.DateTimeField(auto_now=True)),
], ],
options={ options={
'verbose_name': 'Rentmeister', "verbose_name": "Rentmeister",
'verbose_name_plural': 'Rentmeister', "verbose_name_plural": "Rentmeister",
'ordering': ['nachname', 'vorname'], "ordering": ["nachname", "vorname"],
}, },
), ),
migrations.CreateModel( migrations.CreateModel(
name='StiftungsKonto', name="StiftungsKonto",
fields=[ fields=[
('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)), (
('kontoname', models.CharField(max_length=200, verbose_name='Kontoname')), "id",
('bank_name', models.CharField(max_length=200, verbose_name='Bank')), models.UUIDField(
('iban', models.CharField(max_length=34, verbose_name='IBAN')), default=uuid.uuid4,
('bic', models.CharField(blank=True, max_length=11, verbose_name='BIC')), editable=False,
('konto_typ', models.CharField(choices=[('girokonto', 'Girokonto'), ('sparkonto', 'Sparkonto'), ('festgeld', 'Festgeld'), ('tagesgeld', 'Tagesgeld'), ('depot', 'Depot'), ('sonstiges', 'Sonstiges')], default='girokonto', max_length=20, verbose_name='Kontotyp')), primary_key=True,
('saldo', models.DecimalField(decimal_places=2, default=0.0, max_digits=10, verbose_name='Aktueller Saldo')), serialize=False,
('saldo_datum', models.DateField(blank=True, null=True, verbose_name='Saldo-Datum')), ),
('zinssatz', models.DecimalField(blank=True, decimal_places=2, max_digits=5, null=True, verbose_name='Zinssatz (%)')), ),
('laufzeit_bis', models.DateField(blank=True, null=True, verbose_name='Laufzeit bis')), (
('aktiv', models.BooleanField(default=True, verbose_name='Aktiv')), "kontoname",
('notizen', models.TextField(blank=True, verbose_name='Notizen')), models.CharField(max_length=200, verbose_name="Kontoname"),
('erstellt_am', models.DateTimeField(auto_now_add=True)), ),
('aktualisiert_am', models.DateTimeField(auto_now=True)), ("bank_name", models.CharField(max_length=200, verbose_name="Bank")),
("iban", models.CharField(max_length=34, verbose_name="IBAN")),
(
"bic",
models.CharField(blank=True, max_length=11, verbose_name="BIC"),
),
(
"konto_typ",
models.CharField(
choices=[
("girokonto", "Girokonto"),
("sparkonto", "Sparkonto"),
("festgeld", "Festgeld"),
("tagesgeld", "Tagesgeld"),
("depot", "Depot"),
("sonstiges", "Sonstiges"),
],
default="girokonto",
max_length=20,
verbose_name="Kontotyp",
),
),
(
"saldo",
models.DecimalField(
decimal_places=2,
default=0.0,
max_digits=10,
verbose_name="Aktueller Saldo",
),
),
(
"saldo_datum",
models.DateField(blank=True, null=True, verbose_name="Saldo-Datum"),
),
(
"zinssatz",
models.DecimalField(
blank=True,
decimal_places=2,
max_digits=5,
null=True,
verbose_name="Zinssatz (%)",
),
),
(
"laufzeit_bis",
models.DateField(
blank=True, null=True, verbose_name="Laufzeit bis"
),
),
("aktiv", models.BooleanField(default=True, verbose_name="Aktiv")),
("notizen", models.TextField(blank=True, verbose_name="Notizen")),
("erstellt_am", models.DateTimeField(auto_now_add=True)),
("aktualisiert_am", models.DateTimeField(auto_now=True)),
], ],
options={ options={
'verbose_name': 'Stiftungskonto', "verbose_name": "Stiftungskonto",
'verbose_name_plural': 'Stiftungskonten', "verbose_name_plural": "Stiftungskonten",
'ordering': ['bank_name', 'kontoname'], "ordering": ["bank_name", "kontoname"],
}, },
), ),
migrations.CreateModel( migrations.CreateModel(
name='Verwaltungskosten', name="Verwaltungskosten",
fields=[ fields=[
('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)), (
('bezeichnung', models.CharField(max_length=200, verbose_name='Bezeichnung')), "id",
('kategorie', models.CharField(choices=[('rechnung_intern', 'Interne Rechnung'), ('bueroausstattung', 'Büroausstattung'), ('fahrtkosten', 'Fahrtkosten'), ('porto', 'Porto & Versand'), ('telefon_internet', 'Telefon & Internet'), ('software', 'Software & Lizenzen'), ('beratung', 'Beratung & Dienstleistungen'), ('versicherung', 'Versicherungen'), ('steuerberatung', 'Steuerberatung'), ('bankgebuehren', 'Bankgebühren'), ('sonstiges', 'Sonstiges')], max_length=30, verbose_name='Kategorie')), models.UUIDField(
('betrag', models.DecimalField(decimal_places=2, max_digits=10, verbose_name='Betrag (€)')), default=uuid.uuid4,
('datum', models.DateField(verbose_name='Datum')), editable=False,
('lieferant_firma', models.CharField(blank=True, max_length=200, verbose_name='Lieferant/Firma')), primary_key=True,
('rechnungsnummer', models.CharField(blank=True, max_length=100, verbose_name='Rechnungsnummer')), serialize=False,
('status', models.CharField(choices=[('geplant', 'Geplant'), ('bestellt', 'Bestellt'), ('erhalten', 'Erhalten'), ('bezahlt', 'Bezahlt'), ('storniert', 'Storniert')], default='geplant', max_length=20, verbose_name='Status')), ),
('km_anzahl', models.DecimalField(blank=True, decimal_places=1, max_digits=8, null=True, verbose_name='Kilometer')), ),
('km_satz', models.DecimalField(blank=True, decimal_places=2, max_digits=4, null=True, verbose_name='€/km')), (
('von_ort', models.CharField(blank=True, max_length=100, verbose_name='Von (Ort)')), "bezeichnung",
('nach_ort', models.CharField(blank=True, max_length=100, verbose_name='Nach (Ort)')), models.CharField(max_length=200, verbose_name="Bezeichnung"),
('zweck', models.CharField(blank=True, max_length=200, verbose_name='Zweck der Fahrt')), ),
('beschreibung', models.TextField(blank=True, verbose_name='Beschreibung')), (
('notizen', models.TextField(blank=True, verbose_name='Notizen')), "kategorie",
('erstellt_am', models.DateTimeField(auto_now_add=True)), models.CharField(
('aktualisiert_am', models.DateTimeField(auto_now=True)), choices=[
('konto', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='stiftung.stiftungskonto', verbose_name='Konto')), ("rechnung_intern", "Interne Rechnung"),
('rentmeister', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='stiftung.rentmeister', verbose_name='Rentmeister')), ("bueroausstattung", "Büroausstattung"),
("fahrtkosten", "Fahrtkosten"),
("porto", "Porto & Versand"),
("telefon_internet", "Telefon & Internet"),
("software", "Software & Lizenzen"),
("beratung", "Beratung & Dienstleistungen"),
("versicherung", "Versicherungen"),
("steuerberatung", "Steuerberatung"),
("bankgebuehren", "Bankgebühren"),
("sonstiges", "Sonstiges"),
],
max_length=30,
verbose_name="Kategorie",
),
),
(
"betrag",
models.DecimalField(
decimal_places=2, max_digits=10, verbose_name="Betrag (€)"
),
),
("datum", models.DateField(verbose_name="Datum")),
(
"lieferant_firma",
models.CharField(
blank=True, max_length=200, verbose_name="Lieferant/Firma"
),
),
(
"rechnungsnummer",
models.CharField(
blank=True, max_length=100, verbose_name="Rechnungsnummer"
),
),
(
"status",
models.CharField(
choices=[
("geplant", "Geplant"),
("bestellt", "Bestellt"),
("erhalten", "Erhalten"),
("bezahlt", "Bezahlt"),
("storniert", "Storniert"),
],
default="geplant",
max_length=20,
verbose_name="Status",
),
),
(
"km_anzahl",
models.DecimalField(
blank=True,
decimal_places=1,
max_digits=8,
null=True,
verbose_name="Kilometer",
),
),
(
"km_satz",
models.DecimalField(
blank=True,
decimal_places=2,
max_digits=4,
null=True,
verbose_name="€/km",
),
),
(
"von_ort",
models.CharField(
blank=True, max_length=100, verbose_name="Von (Ort)"
),
),
(
"nach_ort",
models.CharField(
blank=True, max_length=100, verbose_name="Nach (Ort)"
),
),
(
"zweck",
models.CharField(
blank=True, max_length=200, verbose_name="Zweck der Fahrt"
),
),
(
"beschreibung",
models.TextField(blank=True, verbose_name="Beschreibung"),
),
("notizen", models.TextField(blank=True, verbose_name="Notizen")),
("erstellt_am", models.DateTimeField(auto_now_add=True)),
("aktualisiert_am", models.DateTimeField(auto_now=True)),
(
"konto",
models.ForeignKey(
blank=True,
null=True,
on_delete=django.db.models.deletion.SET_NULL,
to="stiftung.stiftungskonto",
verbose_name="Konto",
),
),
(
"rentmeister",
models.ForeignKey(
blank=True,
null=True,
on_delete=django.db.models.deletion.SET_NULL,
to="stiftung.rentmeister",
verbose_name="Rentmeister",
),
),
], ],
options={ options={
'verbose_name': 'Verwaltungskosten', "verbose_name": "Verwaltungskosten",
'verbose_name_plural': 'Verwaltungskosten', "verbose_name_plural": "Verwaltungskosten",
'ordering': ['-datum', '-erstellt_am'], "ordering": ["-datum", "-erstellt_am"],
}, },
), ),
] ]

View File

@@ -1,44 +1,156 @@
# Generated by Django 5.0.6 on 2025-08-24 19:27 # Generated by Django 5.0.6 on 2025-08-24 19:27
import django.db.models.deletion
import uuid import uuid
import django.db.models.deletion
from django.db import migrations, models from django.db import migrations, models
class Migration(migrations.Migration): class Migration(migrations.Migration):
dependencies = [ dependencies = [
('stiftung', '0010_rentmeister_stiftungskonto_verwaltungskosten'), ("stiftung", "0010_rentmeister_stiftungskonto_verwaltungskosten"),
] ]
operations = [ operations = [
migrations.CreateModel( migrations.CreateModel(
name='BankTransaction', name="BankTransaction",
fields=[ fields=[
('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)), (
('datum', models.DateField(verbose_name='Buchungsdatum')), "id",
('valuta', models.DateField(blank=True, null=True, verbose_name='Valutadatum')), models.UUIDField(
('betrag', models.DecimalField(decimal_places=2, max_digits=12, verbose_name='Betrag (€)')), default=uuid.uuid4,
('waehrung', models.CharField(default='EUR', max_length=3, verbose_name='Währung')), editable=False,
('verwendungszweck', models.TextField(verbose_name='Verwendungszweck')), primary_key=True,
('empfaenger_zahlungspflichtiger', models.CharField(blank=True, max_length=200, verbose_name='Empfänger/Zahlungspflichtiger')), serialize=False,
('iban_gegenpartei', models.CharField(blank=True, max_length=34, verbose_name='IBAN Gegenpartei')), ),
('bic_gegenpartei', models.CharField(blank=True, max_length=11, verbose_name='BIC Gegenpartei')), ),
('referenz', models.CharField(blank=True, max_length=100, verbose_name='Referenz/Transaktions-ID')), ("datum", models.DateField(verbose_name="Buchungsdatum")),
('transaction_type', models.CharField(choices=[('eingang', 'Eingang'), ('ausgang', 'Ausgang'), ('lastschrift', 'Lastschrift'), ('ueberweisung', 'Überweisung'), ('dauerauftrag', 'Dauerauftrag'), ('kartenzahlung', 'Kartenzahlung'), ('zinsen', 'Zinsen'), ('gebuehren', 'Gebühren'), ('sonstiges', 'Sonstiges')], default='sonstiges', max_length=20, verbose_name='Transaktionsart')), (
('status', models.CharField(choices=[('imported', 'Importiert'), ('verified', 'Geprüft'), ('assigned', 'Zugeordnet'), ('ignored', 'Ignoriert')], default='imported', max_length=20, verbose_name='Status')), "valuta",
('kommentare', models.TextField(blank=True, verbose_name='Kommentare')), models.DateField(blank=True, null=True, verbose_name="Valutadatum"),
('import_datei', models.CharField(blank=True, max_length=255, verbose_name='Import-Datei')), ),
('importiert_am', models.DateTimeField(auto_now_add=True, verbose_name='Importiert am')), (
('saldo_nach_buchung', models.DecimalField(blank=True, decimal_places=2, max_digits=12, null=True, verbose_name='Saldo nach Buchung')), "betrag",
('konto', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='stiftung.stiftungskonto', verbose_name='Konto')), models.DecimalField(
('verwaltungskosten', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='stiftung.verwaltungskosten', verbose_name='Zugeordnete Verwaltungskosten')), decimal_places=2, max_digits=12, verbose_name="Betrag (€)"
),
),
(
"waehrung",
models.CharField(
default="EUR", max_length=3, verbose_name="Währung"
),
),
("verwendungszweck", models.TextField(verbose_name="Verwendungszweck")),
(
"empfaenger_zahlungspflichtiger",
models.CharField(
blank=True,
max_length=200,
verbose_name="Empfänger/Zahlungspflichtiger",
),
),
(
"iban_gegenpartei",
models.CharField(
blank=True, max_length=34, verbose_name="IBAN Gegenpartei"
),
),
(
"bic_gegenpartei",
models.CharField(
blank=True, max_length=11, verbose_name="BIC Gegenpartei"
),
),
(
"referenz",
models.CharField(
blank=True,
max_length=100,
verbose_name="Referenz/Transaktions-ID",
),
),
(
"transaction_type",
models.CharField(
choices=[
("eingang", "Eingang"),
("ausgang", "Ausgang"),
("lastschrift", "Lastschrift"),
("ueberweisung", "Überweisung"),
("dauerauftrag", "Dauerauftrag"),
("kartenzahlung", "Kartenzahlung"),
("zinsen", "Zinsen"),
("gebuehren", "Gebühren"),
("sonstiges", "Sonstiges"),
],
default="sonstiges",
max_length=20,
verbose_name="Transaktionsart",
),
),
(
"status",
models.CharField(
choices=[
("imported", "Importiert"),
("verified", "Geprüft"),
("assigned", "Zugeordnet"),
("ignored", "Ignoriert"),
],
default="imported",
max_length=20,
verbose_name="Status",
),
),
("kommentare", models.TextField(blank=True, verbose_name="Kommentare")),
(
"import_datei",
models.CharField(
blank=True, max_length=255, verbose_name="Import-Datei"
),
),
(
"importiert_am",
models.DateTimeField(
auto_now_add=True, verbose_name="Importiert am"
),
),
(
"saldo_nach_buchung",
models.DecimalField(
blank=True,
decimal_places=2,
max_digits=12,
null=True,
verbose_name="Saldo nach Buchung",
),
),
(
"konto",
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
to="stiftung.stiftungskonto",
verbose_name="Konto",
),
),
(
"verwaltungskosten",
models.ForeignKey(
blank=True,
null=True,
on_delete=django.db.models.deletion.SET_NULL,
to="stiftung.verwaltungskosten",
verbose_name="Zugeordnete Verwaltungskosten",
),
),
], ],
options={ options={
'verbose_name': 'Banktransaktion', "verbose_name": "Banktransaktion",
'verbose_name_plural': 'Banktransaktionen', "verbose_name_plural": "Banktransaktionen",
'ordering': ['-datum', '-importiert_am'], "ordering": ["-datum", "-importiert_am"],
'unique_together': {('konto', 'datum', 'betrag', 'referenz')}, "unique_together": {("konto", "datum", "betrag", "referenz")},
}, },
), ),
] ]

View File

@@ -7,28 +7,55 @@ from django.db import migrations, models
class Migration(migrations.Migration): class Migration(migrations.Migration):
dependencies = [ dependencies = [
('stiftung', '0011_banktransaction'), ("stiftung", "0011_banktransaction"),
] ]
operations = [ operations = [
migrations.AddField( migrations.AddField(
model_name='verwaltungskosten', model_name="verwaltungskosten",
name='quellkonto', name="quellkonto",
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='ausgaben', to='stiftung.stiftungskonto', verbose_name='Quellkonto'), field=models.ForeignKey(
blank=True,
null=True,
on_delete=django.db.models.deletion.SET_NULL,
related_name="ausgaben",
to="stiftung.stiftungskonto",
verbose_name="Quellkonto",
),
), ),
migrations.AddField( migrations.AddField(
model_name='verwaltungskosten', model_name="verwaltungskosten",
name='zahlungskonto', name="zahlungskonto",
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='zahlungen', to='stiftung.stiftungskonto', verbose_name='Zahlungskonto'), field=models.ForeignKey(
blank=True,
null=True,
on_delete=django.db.models.deletion.SET_NULL,
related_name="zahlungen",
to="stiftung.stiftungskonto",
verbose_name="Zahlungskonto",
),
), ),
migrations.AlterField( migrations.AlterField(
model_name='verwaltungskosten', model_name="verwaltungskosten",
name='konto', name="konto",
field=models.ForeignKey(blank=True, help_text='Veraltet - verwende Zahlungskonto und Quellkonto', null=True, on_delete=django.db.models.deletion.SET_NULL, to='stiftung.stiftungskonto', verbose_name='Konto (Legacy)'), field=models.ForeignKey(
blank=True,
help_text="Veraltet - verwende Zahlungskonto und Quellkonto",
null=True,
on_delete=django.db.models.deletion.SET_NULL,
to="stiftung.stiftungskonto",
verbose_name="Konto (Legacy)",
),
), ),
migrations.AlterField( migrations.AlterField(
model_name='verwaltungskosten', model_name="verwaltungskosten",
name='rentmeister', name="rentmeister",
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='stiftung.rentmeister', verbose_name='Zuständiger Rentmeister'), field=models.ForeignKey(
blank=True,
null=True,
on_delete=django.db.models.deletion.SET_NULL,
to="stiftung.rentmeister",
verbose_name="Zuständiger Rentmeister",
),
), ),
] ]

View File

@@ -6,13 +6,25 @@ from django.db import migrations, models
class Migration(migrations.Migration): class Migration(migrations.Migration):
dependencies = [ dependencies = [
('stiftung', '0012_verwaltungskosten_quellkonto_and_more'), ("stiftung", "0012_verwaltungskosten_quellkonto_and_more"),
] ]
operations = [ operations = [
migrations.AlterField( migrations.AlterField(
model_name='verwaltungskosten', model_name="verwaltungskosten",
name='status', name="status",
field=models.CharField(choices=[('geplant', 'Geplant'), ('bestellt', 'Bestellt'), ('erhalten', 'Erhalten'), ('in_bearbeitung', 'In Bearbeitung'), ('bezahlt', 'Bezahlt'), ('storniert', 'Storniert')], default='geplant', max_length=20, verbose_name='Status'), field=models.CharField(
choices=[
("geplant", "Geplant"),
("bestellt", "Bestellt"),
("erhalten", "Erhalten"),
("in_bearbeitung", "In Bearbeitung"),
("bezahlt", "Bezahlt"),
("storniert", "Storniert"),
],
default="geplant",
max_length=20,
verbose_name="Status",
),
), ),
] ]

View File

@@ -6,13 +6,15 @@ from django.db import migrations, models
class Migration(migrations.Migration): class Migration(migrations.Migration):
dependencies = [ dependencies = [
('stiftung', '0013_alter_verwaltungskosten_status'), ("stiftung", "0013_alter_verwaltungskosten_status"),
] ]
operations = [ operations = [
migrations.AddField( migrations.AddField(
model_name='dokumentlink', model_name="dokumentlink",
name='rentmeister_id', name="rentmeister_id",
field=models.UUIDField(blank=True, null=True, verbose_name='Rentmeister ID'), field=models.UUIDField(
blank=True, null=True, verbose_name="Rentmeister ID"
),
), ),
] ]

View File

@@ -1,7 +1,8 @@
# Generated by Django 5.0.6 on 2025-08-26 08:33 # Generated by Django 5.0.6 on 2025-08-26 08:33
import django.db.models.deletion
import uuid import uuid
import django.db.models.deletion
from django.conf import settings from django.conf import settings
from django.db import migrations, models from django.db import migrations, models
@@ -9,55 +10,229 @@ from django.db import migrations, models
class Migration(migrations.Migration): class Migration(migrations.Migration):
dependencies = [ dependencies = [
('stiftung', '0014_dokumentlink_rentmeister_id'), ("stiftung", "0014_dokumentlink_rentmeister_id"),
migrations.swappable_dependency(settings.AUTH_USER_MODEL), migrations.swappable_dependency(settings.AUTH_USER_MODEL),
] ]
operations = [ operations = [
migrations.CreateModel( migrations.CreateModel(
name='BackupJob', name="BackupJob",
fields=[ fields=[
('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)), (
('backup_type', models.CharField(choices=[('full', 'Vollständiges Backup'), ('database', 'Nur Datenbank'), ('files', 'Nur Dateien')], max_length=20, verbose_name='Backup-Typ')), "id",
('status', models.CharField(choices=[('pending', 'Wartend'), ('running', 'Läuft'), ('completed', 'Abgeschlossen'), ('failed', 'Fehlgeschlagen')], default='pending', max_length=20, verbose_name='Status')), models.UUIDField(
('created_at', models.DateTimeField(auto_now_add=True, verbose_name='Erstellt am')), default=uuid.uuid4,
('started_at', models.DateTimeField(blank=True, null=True, verbose_name='Gestartet am')), editable=False,
('completed_at', models.DateTimeField(blank=True, null=True, verbose_name='Abgeschlossen am')), primary_key=True,
('backup_filename', models.CharField(blank=True, max_length=255, verbose_name='Backup-Dateiname')), serialize=False,
('backup_size', models.BigIntegerField(blank=True, null=True, verbose_name='Backup-Größe (Bytes)')), ),
('error_message', models.TextField(blank=True, verbose_name='Fehlermeldung')), ),
('database_size', models.BigIntegerField(blank=True, null=True, verbose_name='Datenbankgröße (Bytes)')), (
('files_count', models.IntegerField(blank=True, null=True, verbose_name='Anzahl Dateien')), "backup_type",
('created_by', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, to=settings.AUTH_USER_MODEL, verbose_name='Erstellt von')), models.CharField(
choices=[
("full", "Vollständiges Backup"),
("database", "Nur Datenbank"),
("files", "Nur Dateien"),
],
max_length=20,
verbose_name="Backup-Typ",
),
),
(
"status",
models.CharField(
choices=[
("pending", "Wartend"),
("running", "Läuft"),
("completed", "Abgeschlossen"),
("failed", "Fehlgeschlagen"),
],
default="pending",
max_length=20,
verbose_name="Status",
),
),
(
"created_at",
models.DateTimeField(auto_now_add=True, verbose_name="Erstellt am"),
),
(
"started_at",
models.DateTimeField(
blank=True, null=True, verbose_name="Gestartet am"
),
),
(
"completed_at",
models.DateTimeField(
blank=True, null=True, verbose_name="Abgeschlossen am"
),
),
(
"backup_filename",
models.CharField(
blank=True, max_length=255, verbose_name="Backup-Dateiname"
),
),
(
"backup_size",
models.BigIntegerField(
blank=True, null=True, verbose_name="Backup-Größe (Bytes)"
),
),
(
"error_message",
models.TextField(blank=True, verbose_name="Fehlermeldung"),
),
(
"database_size",
models.BigIntegerField(
blank=True, null=True, verbose_name="Datenbankgröße (Bytes)"
),
),
(
"files_count",
models.IntegerField(
blank=True, null=True, verbose_name="Anzahl Dateien"
),
),
(
"created_by",
models.ForeignKey(
null=True,
on_delete=django.db.models.deletion.SET_NULL,
to=settings.AUTH_USER_MODEL,
verbose_name="Erstellt von",
),
),
], ],
options={ options={
'verbose_name': 'Backup-Job', "verbose_name": "Backup-Job",
'verbose_name_plural': 'Backup-Jobs', "verbose_name_plural": "Backup-Jobs",
'ordering': ['-created_at'], "ordering": ["-created_at"],
}, },
), ),
migrations.CreateModel( migrations.CreateModel(
name='AuditLog', name="AuditLog",
fields=[ fields=[
('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)), (
('username', models.CharField(max_length=150, verbose_name='Benutzername')), "id",
('timestamp', models.DateTimeField(auto_now_add=True, verbose_name='Zeitpunkt')), models.UUIDField(
('action', models.CharField(choices=[('create', 'Erstellt'), ('update', 'Aktualisiert'), ('delete', 'Gelöscht'), ('link', 'Verknüpft'), ('unlink', 'Verknüpfung entfernt'), ('login', 'Anmeldung'), ('logout', 'Abmeldung'), ('backup', 'Backup erstellt'), ('restore', 'Wiederherstellung'), ('export', 'Export'), ('import', 'Import')], max_length=20, verbose_name='Aktion')), default=uuid.uuid4,
('entity_type', models.CharField(choices=[('destinataer', 'Destinatär'), ('land', 'Länderei'), ('paechter', 'Pächter'), ('verpachtung', 'Verpachtung'), ('foerderung', 'Förderung'), ('rentmeister', 'Rentmeister'), ('stiftungskonto', 'Stiftungskonto'), ('verwaltungskosten', 'Verwaltungskosten'), ('banktransaction', 'Bank-Transaktion'), ('dokumentlink', 'Dokument-Verknüpfung'), ('system', 'System'), ('user', 'Benutzer')], max_length=20, verbose_name='Entitätstyp')), editable=False,
('entity_id', models.CharField(blank=True, max_length=100, verbose_name='Entitäts-ID')), primary_key=True,
('entity_name', models.CharField(max_length=255, verbose_name='Entitätsname')), serialize=False,
('description', models.TextField(verbose_name='Beschreibung')), ),
('changes', models.JSONField(blank=True, null=True, verbose_name='Änderungen')), ),
('ip_address', models.GenericIPAddressField(blank=True, null=True, verbose_name='IP-Adresse')), (
('user_agent', models.TextField(blank=True, verbose_name='User Agent')), "username",
('session_key', models.CharField(blank=True, max_length=40, verbose_name='Session-Key')), models.CharField(max_length=150, verbose_name="Benutzername"),
('user', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, to=settings.AUTH_USER_MODEL, verbose_name='Benutzer')), ),
(
"timestamp",
models.DateTimeField(auto_now_add=True, verbose_name="Zeitpunkt"),
),
(
"action",
models.CharField(
choices=[
("create", "Erstellt"),
("update", "Aktualisiert"),
("delete", "Gelöscht"),
("link", "Verknüpft"),
("unlink", "Verknüpfung entfernt"),
("login", "Anmeldung"),
("logout", "Abmeldung"),
("backup", "Backup erstellt"),
("restore", "Wiederherstellung"),
("export", "Export"),
("import", "Import"),
],
max_length=20,
verbose_name="Aktion",
),
),
(
"entity_type",
models.CharField(
choices=[
("destinataer", "Destinatär"),
("land", "Länderei"),
("paechter", "Pächter"),
("verpachtung", "Verpachtung"),
("foerderung", "Förderung"),
("rentmeister", "Rentmeister"),
("stiftungskonto", "Stiftungskonto"),
("verwaltungskosten", "Verwaltungskosten"),
("banktransaction", "Bank-Transaktion"),
("dokumentlink", "Dokument-Verknüpfung"),
("system", "System"),
("user", "Benutzer"),
],
max_length=20,
verbose_name="Entitätstyp",
),
),
(
"entity_id",
models.CharField(
blank=True, max_length=100, verbose_name="Entitäts-ID"
),
),
(
"entity_name",
models.CharField(max_length=255, verbose_name="Entitätsname"),
),
("description", models.TextField(verbose_name="Beschreibung")),
(
"changes",
models.JSONField(blank=True, null=True, verbose_name="Änderungen"),
),
(
"ip_address",
models.GenericIPAddressField(
blank=True, null=True, verbose_name="IP-Adresse"
),
),
("user_agent", models.TextField(blank=True, verbose_name="User Agent")),
(
"session_key",
models.CharField(
blank=True, max_length=40, verbose_name="Session-Key"
),
),
(
"user",
models.ForeignKey(
null=True,
on_delete=django.db.models.deletion.SET_NULL,
to=settings.AUTH_USER_MODEL,
verbose_name="Benutzer",
),
),
], ],
options={ options={
'verbose_name': 'Audit Log Eintrag', "verbose_name": "Audit Log Eintrag",
'verbose_name_plural': 'Audit Log Einträge', "verbose_name_plural": "Audit Log Einträge",
'ordering': ['-timestamp'], "ordering": ["-timestamp"],
'indexes': [models.Index(fields=['timestamp'], name='stiftung_au_timesta_c4591e_idx'), models.Index(fields=['user', 'timestamp'], name='stiftung_au_user_id_e3fc12_idx'), models.Index(fields=['entity_type', 'timestamp'], name='stiftung_au_entity__68f25d_idx'), models.Index(fields=['action', 'timestamp'], name='stiftung_au_action_288765_idx')], "indexes": [
models.Index(
fields=["timestamp"], name="stiftung_au_timesta_c4591e_idx"
),
models.Index(
fields=["user", "timestamp"],
name="stiftung_au_user_id_e3fc12_idx",
),
models.Index(
fields=["entity_type", "timestamp"],
name="stiftung_au_entity__68f25d_idx",
),
models.Index(
fields=["action", "timestamp"],
name="stiftung_au_action_288765_idx",
),
],
}, },
), ),
] ]

View File

@@ -6,19 +6,57 @@ from django.db import migrations, models
class Migration(migrations.Migration): class Migration(migrations.Migration):
dependencies = [ dependencies = [
('stiftung', '0015_backupjob_auditlog'), ("stiftung", "0015_backupjob_auditlog"),
] ]
operations = [ operations = [
migrations.CreateModel( migrations.CreateModel(
name='ApplicationPermission', name="ApplicationPermission",
fields=[ fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), (
"id",
models.BigAutoField(
auto_created=True,
primary_key=True,
serialize=False,
verbose_name="ID",
),
),
], ],
options={ options={
'permissions': [('manage_destinataere', 'Kann Destinatäre verwalten'), ('view_destinataere', 'Kann Destinatäre anzeigen'), ('manage_land', 'Kann Ländereien verwalten'), ('view_land', 'Kann Ländereien anzeigen'), ('manage_paechter', 'Kann Pächter verwalten'), ('view_paechter', 'Kann Pächter anzeigen'), ('manage_verpachtungen', 'Kann Verpachtungen verwalten'), ('view_verpachtungen', 'Kann Verpachtungen anzeigen'), ('manage_foerderungen', 'Kann Förderungen verwalten'), ('view_foerderungen', 'Kann Förderungen anzeigen'), ('manage_documents', 'Kann Dokumente verwalten'), ('view_documents', 'Kann Dokumente anzeigen'), ('link_documents', 'Kann Dokumente verknüpfen'), ('manage_verwaltungskosten', 'Kann Verwaltungskosten verwalten'), ('view_verwaltungskosten', 'Kann Verwaltungskosten anzeigen'), ('approve_payments', 'Kann Zahlungen genehmigen'), ('manage_konten', 'Kann Stiftungskonten verwalten'), ('view_konten', 'Kann Stiftungskonten anzeigen'), ('manage_rentmeister', 'Kann Rentmeister verwalten'), ('view_rentmeister', 'Kann Rentmeister anzeigen'), ('access_administration', 'Kann Administration aufrufen'), ('view_audit_logs', 'Kann Audit-Logs anzeigen'), ('manage_backups', 'Kann Backups erstellen und verwalten'), ('manage_users', 'Kann Benutzer verwalten'), ('manage_permissions', 'Kann Berechtigungen verwalten'), ('import_data', 'Kann Daten importieren'), ('export_data', 'Kann Daten exportieren'), ('access_django_admin', 'Kann Django Admin aufrufen'), ('view_system_stats', 'Kann Systemstatistiken anzeigen')], "permissions": [
'managed': False, ("manage_destinataere", "Kann Destinatäre verwalten"),
'default_permissions': (), ("view_destinataere", "Kann Destinatäre anzeigen"),
("manage_land", "Kann Ländereien verwalten"),
("view_land", "Kann Ländereien anzeigen"),
("manage_paechter", "Kann Pächter verwalten"),
("view_paechter", "Kann Pächter anzeigen"),
("manage_verpachtungen", "Kann Verpachtungen verwalten"),
("view_verpachtungen", "Kann Verpachtungen anzeigen"),
("manage_foerderungen", "Kann Förderungen verwalten"),
("view_foerderungen", "Kann Förderungen anzeigen"),
("manage_documents", "Kann Dokumente verwalten"),
("view_documents", "Kann Dokumente anzeigen"),
("link_documents", "Kann Dokumente verknüpfen"),
("manage_verwaltungskosten", "Kann Verwaltungskosten verwalten"),
("view_verwaltungskosten", "Kann Verwaltungskosten anzeigen"),
("approve_payments", "Kann Zahlungen genehmigen"),
("manage_konten", "Kann Stiftungskonten verwalten"),
("view_konten", "Kann Stiftungskonten anzeigen"),
("manage_rentmeister", "Kann Rentmeister verwalten"),
("view_rentmeister", "Kann Rentmeister anzeigen"),
("access_administration", "Kann Administration aufrufen"),
("view_audit_logs", "Kann Audit-Logs anzeigen"),
("manage_backups", "Kann Backups erstellen und verwalten"),
("manage_users", "Kann Benutzer verwalten"),
("manage_permissions", "Kann Berechtigungen verwalten"),
("import_data", "Kann Daten importieren"),
("export_data", "Kann Daten exportieren"),
("access_django_admin", "Kann Django Admin aufrufen"),
("view_system_stats", "Kann Systemstatistiken anzeigen"),
],
"managed": False,
"default_permissions": (),
}, },
), ),
] ]

View File

@@ -7,48 +7,74 @@ from django.db import migrations, models
class Migration(migrations.Migration): class Migration(migrations.Migration):
dependencies = [ dependencies = [
('stiftung', '0016_applicationpermission'), ("stiftung", "0016_applicationpermission"),
] ]
operations = [ operations = [
migrations.AddField( migrations.AddField(
model_name='destinataer', model_name="destinataer",
name='haushaltsgroesse', name="haushaltsgroesse",
field=models.PositiveIntegerField(default=1, verbose_name='Haushaltsgröße'), field=models.PositiveIntegerField(default=1, verbose_name="Haushaltsgröße"),
), ),
migrations.AddField( migrations.AddField(
model_name='destinataer', model_name="destinataer",
name='ist_abkoemmling', name="ist_abkoemmling",
field=models.BooleanField(default=False, verbose_name='Abkömmling gem. Satzung'), field=models.BooleanField(
default=False, verbose_name="Abkömmling gem. Satzung"
),
), ),
migrations.AddField( migrations.AddField(
model_name='destinataer', model_name="destinataer",
name='letzter_studiennachweis', name="letzter_studiennachweis",
field=models.DateField(blank=True, null=True, verbose_name='Letzter Studiennachweis'), field=models.DateField(
blank=True, null=True, verbose_name="Letzter Studiennachweis"
),
), ),
migrations.AddField( migrations.AddField(
model_name='destinataer', model_name="destinataer",
name='monatliche_bezuege', name="monatliche_bezuege",
field=models.DecimalField(blank=True, decimal_places=2, max_digits=10, null=True, verbose_name='Monatliche Bezüge (€)'), field=models.DecimalField(
blank=True,
decimal_places=2,
max_digits=10,
null=True,
verbose_name="Monatliche Bezüge (€)",
),
), ),
migrations.AddField( migrations.AddField(
model_name='destinataer', model_name="destinataer",
name='standard_konto', name="standard_konto",
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='stiftung.stiftungskonto', verbose_name='Standard Auszahlungskonto'), field=models.ForeignKey(
blank=True,
null=True,
on_delete=django.db.models.deletion.SET_NULL,
to="stiftung.stiftungskonto",
verbose_name="Standard Auszahlungskonto",
),
), ),
migrations.AddField( migrations.AddField(
model_name='destinataer', model_name="destinataer",
name='studiennachweis_erforderlich', name="studiennachweis_erforderlich",
field=models.BooleanField(default=False, verbose_name='Studiennachweis erforderlich'), field=models.BooleanField(
default=False, verbose_name="Studiennachweis erforderlich"
),
), ),
migrations.AddField( migrations.AddField(
model_name='destinataer', model_name="destinataer",
name='unterstuetzung_bestaetigt', name="unterstuetzung_bestaetigt",
field=models.BooleanField(default=False, verbose_name='Unterstützung bestätigt'), field=models.BooleanField(
default=False, verbose_name="Unterstützung bestätigt"
),
), ),
migrations.AddField( migrations.AddField(
model_name='destinataer', model_name="destinataer",
name='vermoegen', name="vermoegen",
field=models.DecimalField(blank=True, decimal_places=2, max_digits=12, null=True, verbose_name='Vermögen (€)'), field=models.DecimalField(
blank=True,
decimal_places=2,
max_digits=12,
null=True,
verbose_name="Vermögen (€)",
),
), ),
] ]

View File

@@ -6,13 +6,19 @@ from django.db import migrations, models
class Migration(migrations.Migration): class Migration(migrations.Migration):
dependencies = [ dependencies = [
('stiftung', '0017_destinataer_haushaltsgroesse_and_more'), ("stiftung", "0017_destinataer_haushaltsgroesse_and_more"),
] ]
operations = [ operations = [
migrations.AddField( migrations.AddField(
model_name='destinataer', model_name="destinataer",
name='vierteljaehrlicher_betrag', name="vierteljaehrlicher_betrag",
field=models.DecimalField(blank=True, decimal_places=2, max_digits=12, null=True, verbose_name='Vierteljährlicher Betrag (€)'), field=models.DecimalField(
blank=True,
decimal_places=2,
max_digits=12,
null=True,
verbose_name="Vierteljährlicher Betrag (€)",
),
), ),
] ]

View File

@@ -1,35 +1,91 @@
# Generated by Django 5.0.6 on 2025-08-29 13:40 # Generated by Django 5.0.6 on 2025-08-29 13:40
import django.db.models.deletion
import uuid import uuid
import django.db.models.deletion
from django.db import migrations, models from django.db import migrations, models
class Migration(migrations.Migration): class Migration(migrations.Migration):
dependencies = [ dependencies = [
('stiftung', '0018_destinataer_vierteljaehrlicher_betrag'), ("stiftung", "0018_destinataer_vierteljaehrlicher_betrag"),
] ]
operations = [ operations = [
migrations.CreateModel( migrations.CreateModel(
name='DestinataerUnterstuetzung', name="DestinataerUnterstuetzung",
fields=[ 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 (€)')), "id",
('faellig_am', models.DateField(verbose_name='Fällig am')), models.UUIDField(
('status', models.CharField(choices=[('geplant', 'Geplant'), ('in_bearbeitung', 'In Bearbeitung'), ('ausgezahlt', 'Ausgezahlt'), ('storniert', 'Storniert')], default='geplant', max_length=20, verbose_name='Status')), default=uuid.uuid4,
('beschreibung', models.CharField(blank=True, max_length=255, verbose_name='Beschreibung')), editable=False,
('erstellt_am', models.DateTimeField(auto_now_add=True)), primary_key=True,
('aktualisiert_am', models.DateTimeField(auto_now=True)), serialize=False,
('destinataer', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='unterstuetzungen', to='stiftung.destinataer', verbose_name='Destinatär')), ),
('konto', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to='stiftung.stiftungskonto', verbose_name='Zahlungskonto')), ),
(
"betrag",
models.DecimalField(
decimal_places=2, max_digits=12, verbose_name="Betrag (€)"
),
),
("faellig_am", models.DateField(verbose_name="Fällig am")),
(
"status",
models.CharField(
choices=[
("geplant", "Geplant"),
("in_bearbeitung", "In Bearbeitung"),
("ausgezahlt", "Ausgezahlt"),
("storniert", "Storniert"),
],
default="geplant",
max_length=20,
verbose_name="Status",
),
),
(
"beschreibung",
models.CharField(
blank=True, max_length=255, verbose_name="Beschreibung"
),
),
("erstellt_am", models.DateTimeField(auto_now_add=True)),
("aktualisiert_am", models.DateTimeField(auto_now=True)),
(
"destinataer",
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
related_name="unterstuetzungen",
to="stiftung.destinataer",
verbose_name="Destinatär",
),
),
(
"konto",
models.ForeignKey(
on_delete=django.db.models.deletion.PROTECT,
to="stiftung.stiftungskonto",
verbose_name="Zahlungskonto",
),
),
], ],
options={ options={
'verbose_name': 'Destinatärunterstützung', "verbose_name": "Destinatärunterstützung",
'verbose_name_plural': 'Destinatärunterstützungen', "verbose_name_plural": "Destinatärunterstützungen",
'ordering': ['-faellig_am', '-erstellt_am'], "ordering": ["-faellig_am", "-erstellt_am"],
'indexes': [models.Index(fields=['status', 'faellig_am'], name='stiftung_de_status_1e9799_idx'), models.Index(fields=['destinataer', 'status'], name='stiftung_de_destina_ba7286_idx')], "indexes": [
models.Index(
fields=["status", "faellig_am"],
name="stiftung_de_status_1e9799_idx",
),
models.Index(
fields=["destinataer", "status"],
name="stiftung_de_destina_ba7286_idx",
),
],
}, },
), ),
] ]

View File

@@ -1,7 +1,8 @@
# Generated by Django 5.0.6 on 2025-08-29 16:05 # Generated by Django 5.0.6 on 2025-08-29 16:05
import django.db.models.deletion
import uuid import uuid
import django.db.models.deletion
from django.conf import settings from django.conf import settings
from django.db import migrations, models from django.db import migrations, models
@@ -9,26 +10,65 @@ from django.db import migrations, models
class Migration(migrations.Migration): class Migration(migrations.Migration):
dependencies = [ dependencies = [
('stiftung', '0019_destinataerunterstuetzung'), ("stiftung", "0019_destinataerunterstuetzung"),
migrations.swappable_dependency(settings.AUTH_USER_MODEL), migrations.swappable_dependency(settings.AUTH_USER_MODEL),
] ]
operations = [ operations = [
migrations.CreateModel( migrations.CreateModel(
name='DestinataerNotiz', name="DestinataerNotiz",
fields=[ fields=[
('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)), (
('titel', models.CharField(blank=True, max_length=200, verbose_name='Titel')), "id",
('text', models.TextField(blank=True, verbose_name='Notiz')), models.UUIDField(
('datei', models.FileField(blank=True, null=True, upload_to='destinataer_notizen/', verbose_name='Anhang')), default=uuid.uuid4,
('erstellt_am', models.DateTimeField(auto_now_add=True, verbose_name='Erstellt am')), editable=False,
('destinataer', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='notizen_eintraege', to='stiftung.destinataer', verbose_name='Destinatär')), primary_key=True,
('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')), serialize=False,
),
),
(
"titel",
models.CharField(blank=True, max_length=200, verbose_name="Titel"),
),
("text", models.TextField(blank=True, verbose_name="Notiz")),
(
"datei",
models.FileField(
blank=True,
null=True,
upload_to="destinataer_notizen/",
verbose_name="Anhang",
),
),
(
"erstellt_am",
models.DateTimeField(auto_now_add=True, verbose_name="Erstellt am"),
),
(
"destinataer",
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
related_name="notizen_eintraege",
to="stiftung.destinataer",
verbose_name="Destinatär",
),
),
(
"erstellt_von",
models.ForeignKey(
blank=True,
null=True,
on_delete=django.db.models.deletion.SET_NULL,
to=settings.AUTH_USER_MODEL,
verbose_name="Erstellt von",
),
),
], ],
options={ options={
'verbose_name': 'Destinatär-Notiz', "verbose_name": "Destinatär-Notiz",
'verbose_name_plural': 'Destinatär-Notizen', "verbose_name_plural": "Destinatär-Notizen",
'ordering': ['-erstellt_am'], "ordering": ["-erstellt_am"],
}, },
), ),
] ]

View File

@@ -1,134 +1,354 @@
# Generated by Django 5.0.6 on 2025-08-30 14:20 # Generated by Django 5.0.6 on 2025-08-30 14:20
import uuid
import django.core.validators import django.core.validators
import django.db.models.deletion import django.db.models.deletion
import uuid
from django.db import migrations, models from django.db import migrations, models
class Migration(migrations.Migration): class Migration(migrations.Migration):
dependencies = [ dependencies = [
('stiftung', '0020_destinataernotiz'), ("stiftung", "0020_destinataernotiz"),
] ]
operations = [ operations = [
migrations.AddField( migrations.AddField(
model_name='land', model_name="land",
name='adresse', name="adresse",
field=models.CharField(blank=True, max_length=200, null=True, verbose_name='Adresse/Ortsangabe'), field=models.CharField(
blank=True, max_length=200, null=True, verbose_name="Adresse/Ortsangabe"
),
), ),
migrations.AddField( migrations.AddField(
model_name='land', model_name="land",
name='aktueller_paechter', name="aktueller_paechter",
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='gepachtete_laendereien', to='stiftung.paechter', verbose_name='Aktueller Pächter'), field=models.ForeignKey(
blank=True,
null=True,
on_delete=django.db.models.deletion.SET_NULL,
related_name="gepachtete_laendereien",
to="stiftung.paechter",
verbose_name="Aktueller Pächter",
),
), ),
migrations.AddField( migrations.AddField(
model_name='land', model_name="land",
name='grundbuchblatt', name="grundbuchblatt",
field=models.CharField(blank=True, max_length=50, null=True, verbose_name='Grundbuchblatt'), field=models.CharField(
blank=True, max_length=50, null=True, verbose_name="Grundbuchblatt"
),
), ),
migrations.AddField( migrations.AddField(
model_name='land', model_name="land",
name='grundsteuer_umlage', name="grundsteuer_umlage",
field=models.BooleanField(default=True, verbose_name='Grundsteuer umlagefähig'), field=models.BooleanField(
default=True, verbose_name="Grundsteuer umlagefähig"
),
), ),
migrations.AddField( migrations.AddField(
model_name='land', model_name="land",
name='jagdpacht_anteil_umlage', name="jagdpacht_anteil_umlage",
field=models.BooleanField(default=False, verbose_name='Jagdpachtanteile umlagefähig'), field=models.BooleanField(
default=False, verbose_name="Jagdpachtanteile umlagefähig"
),
), ),
migrations.AddField( migrations.AddField(
model_name='land', model_name="land",
name='pachtbeginn', name="pachtbeginn",
field=models.DateField(blank=True, null=True, verbose_name='Pachtbeginn'), field=models.DateField(blank=True, null=True, verbose_name="Pachtbeginn"),
), ),
migrations.AddField( migrations.AddField(
model_name='land', model_name="land",
name='pachtende', name="pachtende",
field=models.DateField(blank=True, null=True, verbose_name='Pachtende'), field=models.DateField(blank=True, null=True, verbose_name="Pachtende"),
), ),
migrations.AddField( migrations.AddField(
model_name='land', model_name="land",
name='pachtzins_pauschal', name="pachtzins_pauschal",
field=models.DecimalField(blank=True, decimal_places=2, max_digits=12, null=True, validators=[django.core.validators.MinValueValidator(0)], verbose_name='Pachtzins pauschal/Jahr (€)'), field=models.DecimalField(
blank=True,
decimal_places=2,
max_digits=12,
null=True,
validators=[django.core.validators.MinValueValidator(0)],
verbose_name="Pachtzins pauschal/Jahr (€)",
),
), ),
migrations.AddField( migrations.AddField(
model_name='land', model_name="land",
name='pachtzins_pro_ha', name="pachtzins_pro_ha",
field=models.DecimalField(blank=True, decimal_places=2, max_digits=12, null=True, validators=[django.core.validators.MinValueValidator(0)], verbose_name='Pachtzins pro ha (€)'), field=models.DecimalField(
blank=True,
decimal_places=2,
max_digits=12,
null=True,
validators=[django.core.validators.MinValueValidator(0)],
verbose_name="Pachtzins pro ha (€)",
),
), ),
migrations.AddField( migrations.AddField(
model_name='land', model_name="land",
name='paechter_anschrift', name="paechter_anschrift",
field=models.TextField(blank=True, null=True, verbose_name='Pächter Anschrift'), field=models.TextField(
blank=True, null=True, verbose_name="Pächter Anschrift"
),
), ),
migrations.AddField( migrations.AddField(
model_name='land', model_name="land",
name='paechter_name', name="paechter_name",
field=models.CharField(blank=True, max_length=150, null=True, verbose_name='Pächter Name'), field=models.CharField(
blank=True, max_length=150, null=True, verbose_name="Pächter Name"
),
), ),
migrations.AddField( migrations.AddField(
model_name='land', model_name="land",
name='ust_option', name="ust_option",
field=models.BooleanField(default=False, verbose_name='USt-Option'), field=models.BooleanField(default=False, verbose_name="USt-Option"),
), ),
migrations.AddField( migrations.AddField(
model_name='land', model_name="land",
name='ust_satz', name="ust_satz",
field=models.DecimalField(decimal_places=2, default=19.0, max_digits=4, verbose_name='USt-Satz (%)'), field=models.DecimalField(
decimal_places=2,
default=19.0,
max_digits=4,
verbose_name="USt-Satz (%)",
),
), ),
migrations.AddField( migrations.AddField(
model_name='land', model_name="land",
name='verbandsbeitraege_umlage', name="verbandsbeitraege_umlage",
field=models.BooleanField(default=True, verbose_name='Verbandsbeiträge umlagefähig'), field=models.BooleanField(
default=True, verbose_name="Verbandsbeiträge umlagefähig"
),
), ),
migrations.AddField( migrations.AddField(
model_name='land', model_name="land",
name='verlaengerung_klausel', name="verlaengerung_klausel",
field=models.BooleanField(default=False, verbose_name='Automatische Verlängerung'), field=models.BooleanField(
default=False, verbose_name="Automatische Verlängerung"
),
), ),
migrations.AddField( migrations.AddField(
model_name='land', model_name="land",
name='versicherungen_umlage', name="versicherungen_umlage",
field=models.BooleanField(default=True, verbose_name='Versicherungen umlagefähig'), field=models.BooleanField(
default=True, verbose_name="Versicherungen umlagefähig"
),
), ),
migrations.AddField( migrations.AddField(
model_name='land', model_name="land",
name='zahlungsweise', name="zahlungsweise",
field=models.CharField(choices=[('jaehrlich', 'Jährlich'), ('halbjaehrlich', 'Halbjährlich'), ('vierteljaehrlich', 'Vierteljährlich'), ('monatlich', 'Monatlich')], default='jaehrlich', max_length=20, verbose_name='Zahlungsweise'), field=models.CharField(
choices=[
("jaehrlich", "Jährlich"),
("halbjaehrlich", "Halbjährlich"),
("vierteljaehrlich", "Vierteljährlich"),
("monatlich", "Monatlich"),
],
default="jaehrlich",
max_length=20,
verbose_name="Zahlungsweise",
),
), ),
migrations.CreateModel( migrations.CreateModel(
name='LandAbrechnung', name="LandAbrechnung",
fields=[ fields=[
('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)), (
('abrechnungsjahr', models.IntegerField(validators=[django.core.validators.MinValueValidator(2000)], verbose_name='Abrechnungsjahr')), "id",
('pacht_vereinnahmt', models.DecimalField(decimal_places=2, default=0, max_digits=12, validators=[django.core.validators.MinValueValidator(0)], verbose_name='Pacht vereinnahmt (€)')), models.UUIDField(
('umlagen_vereinnahmt', models.DecimalField(decimal_places=2, default=0, max_digits=12, validators=[django.core.validators.MinValueValidator(0)], verbose_name='Umlagen vereinnahmt (€)')), default=uuid.uuid4,
('sonstige_einnahmen', models.DecimalField(decimal_places=2, default=0, max_digits=12, validators=[django.core.validators.MinValueValidator(0)], verbose_name='Sonstige Einnahmen (€)')), editable=False,
('zahlungen', models.JSONField(blank=True, help_text='Liste von Objekten {datum, betrag, art}', null=True, verbose_name='Zahlungstermine')), primary_key=True,
('grundsteuer_bescheid_nr', models.CharField(blank=True, max_length=80, null=True, verbose_name='Grundsteuer-Bescheid Nr.')), serialize=False,
('grundsteuer_betrag', models.DecimalField(decimal_places=2, default=0, max_digits=12, validators=[django.core.validators.MinValueValidator(0)], verbose_name='Grundsteuer Betrag (€)')), ),
('versicherungen_betrag', models.DecimalField(decimal_places=2, default=0, max_digits=12, validators=[django.core.validators.MinValueValidator(0)], verbose_name='Versicherungen Betrag (€)')), ),
('verbandsbeitraege_betrag', models.DecimalField(decimal_places=2, default=0, max_digits=12, validators=[django.core.validators.MinValueValidator(0)], verbose_name='Verbandsbeiträge Betrag (€)')), (
('sonstige_abgaben_betrag', models.DecimalField(decimal_places=2, default=0, max_digits=12, validators=[django.core.validators.MinValueValidator(0)], verbose_name='Sonstige öffentliche Abgaben (€)')), "abrechnungsjahr",
('instandhaltung_betrag', models.DecimalField(decimal_places=2, default=0, max_digits=12, validators=[django.core.validators.MinValueValidator(0)], verbose_name='Instandhaltung/Reparaturen (€)')), models.IntegerField(
('verwaltung_recht_betrag', models.DecimalField(decimal_places=2, default=0, max_digits=12, validators=[django.core.validators.MinValueValidator(0)], verbose_name='Verwaltung/Recht (€)')), validators=[django.core.validators.MinValueValidator(2000)],
('vorsteuer_aus_umlagen', models.DecimalField(decimal_places=2, default=0, max_digits=12, validators=[django.core.validators.MinValueValidator(0)], verbose_name='Vorsteuer aus umgelegten Kosten (€)')), verbose_name="Abrechnungsjahr",
('offene_posten', models.DecimalField(decimal_places=2, default=0, max_digits=12, verbose_name='Offene Posten (€)')), ),
('bemerkungen', models.TextField(blank=True, null=True, verbose_name='Bemerkungen Abrechnung')), ),
('pachtvertrag_datei', models.FileField(blank=True, null=True, upload_to='land_abrechnungen/vertraege/', verbose_name='Pachtvertrag (Datei)')), (
('grundsteuer_bescheid_datei', models.FileField(blank=True, null=True, upload_to='land_abrechnungen/bescheide/', verbose_name='Grundsteuerbescheid (Datei)')), "pacht_vereinnahmt",
('versicherungsnachweis_datei', models.FileField(blank=True, null=True, upload_to='land_abrechnungen/versicherungen/', verbose_name='Versicherungsnachweis (Datei)')), models.DecimalField(
('erstellt_am', models.DateTimeField(auto_now_add=True)), decimal_places=2,
('aktualisiert_am', models.DateTimeField(auto_now=True)), default=0,
('land', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='abrechnungen', to='stiftung.land', verbose_name='Länderei')), max_digits=12,
validators=[django.core.validators.MinValueValidator(0)],
verbose_name="Pacht vereinnahmt (€)",
),
),
(
"umlagen_vereinnahmt",
models.DecimalField(
decimal_places=2,
default=0,
max_digits=12,
validators=[django.core.validators.MinValueValidator(0)],
verbose_name="Umlagen vereinnahmt (€)",
),
),
(
"sonstige_einnahmen",
models.DecimalField(
decimal_places=2,
default=0,
max_digits=12,
validators=[django.core.validators.MinValueValidator(0)],
verbose_name="Sonstige Einnahmen (€)",
),
),
(
"zahlungen",
models.JSONField(
blank=True,
help_text="Liste von Objekten {datum, betrag, art}",
null=True,
verbose_name="Zahlungstermine",
),
),
(
"grundsteuer_bescheid_nr",
models.CharField(
blank=True,
max_length=80,
null=True,
verbose_name="Grundsteuer-Bescheid Nr.",
),
),
(
"grundsteuer_betrag",
models.DecimalField(
decimal_places=2,
default=0,
max_digits=12,
validators=[django.core.validators.MinValueValidator(0)],
verbose_name="Grundsteuer Betrag (€)",
),
),
(
"versicherungen_betrag",
models.DecimalField(
decimal_places=2,
default=0,
max_digits=12,
validators=[django.core.validators.MinValueValidator(0)],
verbose_name="Versicherungen Betrag (€)",
),
),
(
"verbandsbeitraege_betrag",
models.DecimalField(
decimal_places=2,
default=0,
max_digits=12,
validators=[django.core.validators.MinValueValidator(0)],
verbose_name="Verbandsbeiträge Betrag (€)",
),
),
(
"sonstige_abgaben_betrag",
models.DecimalField(
decimal_places=2,
default=0,
max_digits=12,
validators=[django.core.validators.MinValueValidator(0)],
verbose_name="Sonstige öffentliche Abgaben (€)",
),
),
(
"instandhaltung_betrag",
models.DecimalField(
decimal_places=2,
default=0,
max_digits=12,
validators=[django.core.validators.MinValueValidator(0)],
verbose_name="Instandhaltung/Reparaturen (€)",
),
),
(
"verwaltung_recht_betrag",
models.DecimalField(
decimal_places=2,
default=0,
max_digits=12,
validators=[django.core.validators.MinValueValidator(0)],
verbose_name="Verwaltung/Recht (€)",
),
),
(
"vorsteuer_aus_umlagen",
models.DecimalField(
decimal_places=2,
default=0,
max_digits=12,
validators=[django.core.validators.MinValueValidator(0)],
verbose_name="Vorsteuer aus umgelegten Kosten (€)",
),
),
(
"offene_posten",
models.DecimalField(
decimal_places=2,
default=0,
max_digits=12,
verbose_name="Offene Posten (€)",
),
),
(
"bemerkungen",
models.TextField(
blank=True, null=True, verbose_name="Bemerkungen Abrechnung"
),
),
(
"pachtvertrag_datei",
models.FileField(
blank=True,
null=True,
upload_to="land_abrechnungen/vertraege/",
verbose_name="Pachtvertrag (Datei)",
),
),
(
"grundsteuer_bescheid_datei",
models.FileField(
blank=True,
null=True,
upload_to="land_abrechnungen/bescheide/",
verbose_name="Grundsteuerbescheid (Datei)",
),
),
(
"versicherungsnachweis_datei",
models.FileField(
blank=True,
null=True,
upload_to="land_abrechnungen/versicherungen/",
verbose_name="Versicherungsnachweis (Datei)",
),
),
("erstellt_am", models.DateTimeField(auto_now_add=True)),
("aktualisiert_am", models.DateTimeField(auto_now=True)),
(
"land",
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
related_name="abrechnungen",
to="stiftung.land",
verbose_name="Länderei",
),
),
], ],
options={ options={
'verbose_name': 'Landabrechnung', "verbose_name": "Landabrechnung",
'verbose_name_plural': 'Landabrechnungen', "verbose_name_plural": "Landabrechnungen",
'ordering': ['-abrechnungsjahr', 'land__gemeinde', 'land__gemarkung'], "ordering": ["-abrechnungsjahr", "land__gemeinde", "land__gemarkung"],
'unique_together': {('land', 'abrechnungsjahr')}, "unique_together": {("land", "abrechnungsjahr")},
}, },
), ),
] ]

View File

@@ -1,57 +1,185 @@
# Generated by Django 5.0.6 on 2025-08-30 16:59 # Generated by Django 5.0.6 on 2025-08-30 16:59
import uuid
import django.core.validators import django.core.validators
import django.db.models.deletion import django.db.models.deletion
import uuid
from django.db import migrations, models from django.db import migrations, models
class Migration(migrations.Migration): class Migration(migrations.Migration):
dependencies = [ dependencies = [
('stiftung', '0021_land_adresse_land_aktueller_paechter_and_more'), ("stiftung", "0021_land_adresse_land_aktueller_paechter_and_more"),
] ]
operations = [ operations = [
migrations.AddField( migrations.AddField(
model_name='dokumentlink', model_name="dokumentlink",
name='land_verpachtung_id', name="land_verpachtung_id",
field=models.UUIDField(blank=True, null=True, verbose_name='Landverpachtung ID (Neu)'), field=models.UUIDField(
blank=True, null=True, verbose_name="Landverpachtung ID (Neu)"
),
), ),
migrations.AlterField( migrations.AlterField(
model_name='dokumentlink', model_name="dokumentlink",
name='verpachtung_id', name="verpachtung_id",
field=models.UUIDField(blank=True, null=True, verbose_name='Verpachtung ID (Legacy)'), field=models.UUIDField(
blank=True, null=True, verbose_name="Verpachtung ID (Legacy)"
),
), ),
migrations.CreateModel( migrations.CreateModel(
name='LandVerpachtung', name="LandVerpachtung",
fields=[ fields=[
('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)), (
('vertragsnummer', models.CharField(max_length=50, unique=True, verbose_name='Vertragsnummer')), "id",
('pachtbeginn', models.DateField(verbose_name='Pachtbeginn')), models.UUIDField(
('pachtende', models.DateField(blank=True, null=True, verbose_name='Pachtende')), default=uuid.uuid4,
('verlaengerung_klausel', models.BooleanField(default=False, verbose_name='Automatische Verlängerung')), editable=False,
('verpachtete_flaeche', models.DecimalField(decimal_places=2, max_digits=12, validators=[django.core.validators.MinValueValidator(0.01)], verbose_name='Verpachtete Fläche (qm)')), primary_key=True,
('pachtzins_pauschal', models.DecimalField(decimal_places=2, max_digits=12, validators=[django.core.validators.MinValueValidator(0)], verbose_name='Pachtzins pauschal/Jahr (€)')), serialize=False,
('pachtzins_pro_ha', models.DecimalField(blank=True, decimal_places=2, max_digits=12, null=True, validators=[django.core.validators.MinValueValidator(0)], verbose_name='Pachtzins pro ha (€)')), ),
('zahlungsweise', models.CharField(choices=[('jaehrlich', 'Jährlich'), ('halbjaehrlich', 'Halbjährlich'), ('vierteljaehrlich', 'Vierteljährlich'), ('monatlich', 'Monatlich')], default='jaehrlich', max_length=20, verbose_name='Zahlungsweise')), ),
('ust_option', models.BooleanField(default=False, verbose_name='USt-Option')), (
('ust_satz', models.DecimalField(decimal_places=2, default=19.0, max_digits=4, verbose_name='USt-Satz (%)')), "vertragsnummer",
('grundsteuer_umlage', models.BooleanField(default=True, verbose_name='Grundsteuer umlagefähig')), models.CharField(
('versicherungen_umlage', models.BooleanField(default=True, verbose_name='Versicherungen umlagefähig')), max_length=50, unique=True, verbose_name="Vertragsnummer"
('verbandsbeitraege_umlage', models.BooleanField(default=True, verbose_name='Verbandsbeiträge umlagefähig')), ),
('jagdpacht_anteil_umlage', models.BooleanField(default=False, verbose_name='Jagdpachtanteile umlagefähig')), ),
('status', models.CharField(choices=[('aktiv', 'Aktiv'), ('beendet', 'Beendet'), ('gekuendigt', 'Gekündigt'), ('verlängert', 'Verlängert')], default='aktiv', max_length=20, verbose_name='Status')), ("pachtbeginn", models.DateField(verbose_name="Pachtbeginn")),
('bemerkungen', models.TextField(blank=True, null=True, verbose_name='Bemerkungen')), (
('erstellt_am', models.DateTimeField(auto_now_add=True)), "pachtende",
('aktualisiert_am', models.DateTimeField(auto_now=True)), models.DateField(blank=True, null=True, verbose_name="Pachtende"),
('land', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='neue_verpachtungen', to='stiftung.land', verbose_name='Länderei')), ),
('paechter', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='neue_verpachtungen', to='stiftung.paechter', verbose_name='Pächter')), (
"verlaengerung_klausel",
models.BooleanField(
default=False, verbose_name="Automatische Verlängerung"
),
),
(
"verpachtete_flaeche",
models.DecimalField(
decimal_places=2,
max_digits=12,
validators=[django.core.validators.MinValueValidator(0.01)],
verbose_name="Verpachtete Fläche (qm)",
),
),
(
"pachtzins_pauschal",
models.DecimalField(
decimal_places=2,
max_digits=12,
validators=[django.core.validators.MinValueValidator(0)],
verbose_name="Pachtzins pauschal/Jahr (€)",
),
),
(
"pachtzins_pro_ha",
models.DecimalField(
blank=True,
decimal_places=2,
max_digits=12,
null=True,
validators=[django.core.validators.MinValueValidator(0)],
verbose_name="Pachtzins pro ha (€)",
),
),
(
"zahlungsweise",
models.CharField(
choices=[
("jaehrlich", "Jährlich"),
("halbjaehrlich", "Halbjährlich"),
("vierteljaehrlich", "Vierteljährlich"),
("monatlich", "Monatlich"),
],
default="jaehrlich",
max_length=20,
verbose_name="Zahlungsweise",
),
),
(
"ust_option",
models.BooleanField(default=False, verbose_name="USt-Option"),
),
(
"ust_satz",
models.DecimalField(
decimal_places=2,
default=19.0,
max_digits=4,
verbose_name="USt-Satz (%)",
),
),
(
"grundsteuer_umlage",
models.BooleanField(
default=True, verbose_name="Grundsteuer umlagefähig"
),
),
(
"versicherungen_umlage",
models.BooleanField(
default=True, verbose_name="Versicherungen umlagefähig"
),
),
(
"verbandsbeitraege_umlage",
models.BooleanField(
default=True, verbose_name="Verbandsbeiträge umlagefähig"
),
),
(
"jagdpacht_anteil_umlage",
models.BooleanField(
default=False, verbose_name="Jagdpachtanteile umlagefähig"
),
),
(
"status",
models.CharField(
choices=[
("aktiv", "Aktiv"),
("beendet", "Beendet"),
("gekuendigt", "Gekündigt"),
("verlängert", "Verlängert"),
],
default="aktiv",
max_length=20,
verbose_name="Status",
),
),
(
"bemerkungen",
models.TextField(blank=True, null=True, verbose_name="Bemerkungen"),
),
("erstellt_am", models.DateTimeField(auto_now_add=True)),
("aktualisiert_am", models.DateTimeField(auto_now=True)),
(
"land",
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
related_name="neue_verpachtungen",
to="stiftung.land",
verbose_name="Länderei",
),
),
(
"paechter",
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
related_name="neue_verpachtungen",
to="stiftung.paechter",
verbose_name="Pächter",
),
),
], ],
options={ options={
'verbose_name': 'Landverpachtung', "verbose_name": "Landverpachtung",
'verbose_name_plural': 'Landverpachtungen', "verbose_name_plural": "Landverpachtungen",
'ordering': ['-pachtbeginn', 'land'], "ordering": ["-pachtbeginn", "land"],
}, },
), ),
] ]

View File

@@ -6,11 +6,11 @@ from django.db import migrations
class Migration(migrations.Migration): class Migration(migrations.Migration):
dependencies = [ dependencies = [
('stiftung', '0022_dokumentlink_land_verpachtung_id_and_more'), ("stiftung", "0022_dokumentlink_land_verpachtung_id_and_more"),
] ]
operations = [ operations = [
migrations.DeleteModel( migrations.DeleteModel(
name='Verpachtung', name="Verpachtung",
), ),
] ]

View File

@@ -6,13 +6,13 @@ from django.db import migrations, models
class Migration(migrations.Migration): class Migration(migrations.Migration):
dependencies = [ dependencies = [
('stiftung', '0023_remove_legacy_verpachtung'), ("stiftung", "0023_remove_legacy_verpachtung"),
] ]
operations = [ operations = [
migrations.AddField( migrations.AddField(
model_name='dokumentlink', model_name="dokumentlink",
name='abrechnung_id', name="abrechnung_id",
field=models.UUIDField(blank=True, null=True, verbose_name='Abrechnung ID'), field=models.UUIDField(blank=True, null=True, verbose_name="Abrechnung ID"),
), ),
] ]

View File

@@ -1,37 +1,90 @@
# Generated by Django 5.0.6 on 2025-08-31 22:08 # Generated by Django 5.0.6 on 2025-08-31 22:08
import uuid import uuid
from django.db import migrations, models from django.db import migrations, models
class Migration(migrations.Migration): class Migration(migrations.Migration):
dependencies = [ dependencies = [
('stiftung', '0024_dokumentlink_abrechnung_id'), ("stiftung", "0024_dokumentlink_abrechnung_id"),
] ]
operations = [ operations = [
migrations.CreateModel( migrations.CreateModel(
name='AppConfiguration', name="AppConfiguration",
fields=[ 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')), "id",
('display_name', models.CharField(max_length=200, verbose_name='Display Name')), models.UUIDField(
('description', models.TextField(blank=True, null=True, verbose_name='Description')), default=uuid.uuid4,
('value', models.TextField(verbose_name='Value')), editable=False,
('default_value', models.TextField(verbose_name='Default Value')), primary_key=True,
('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')), serialize=False,
('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')), "key",
('created_at', models.DateTimeField(auto_now_add=True)), models.CharField(
('updated_at', models.DateTimeField(auto_now=True)), 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={ options={
'verbose_name': 'App Configuration', "verbose_name": "App Configuration",
'verbose_name_plural': 'App Configurations', "verbose_name_plural": "App Configurations",
'ordering': ['category', 'order', 'display_name'], "ordering": ["category", "order", "display_name"],
}, },
), ),
] ]

View File

@@ -1,7 +1,8 @@
# Generated by Django 5.0.6 on 2025-09-01 20:03 # Generated by Django 5.0.6 on 2025-09-01 20:03
import django.db.models.deletion
import uuid import uuid
import django.db.models.deletion
from django.conf import settings from django.conf import settings
from django.db import migrations, models from django.db import migrations, models
@@ -9,81 +10,192 @@ from django.db import migrations, models
class Migration(migrations.Migration): class Migration(migrations.Migration):
dependencies = [ dependencies = [
('stiftung', '0025_appconfiguration'), ("stiftung", "0025_appconfiguration"),
migrations.swappable_dependency(settings.AUTH_USER_MODEL), migrations.swappable_dependency(settings.AUTH_USER_MODEL),
] ]
operations = [ operations = [
migrations.AddField( migrations.AddField(
model_name='destinataerunterstuetzung', model_name="destinataerunterstuetzung",
name='ausgezahlt_am', name="ausgezahlt_am",
field=models.DateField(blank=True, null=True, verbose_name='Ausgezahlt am'), field=models.DateField(blank=True, null=True, verbose_name="Ausgezahlt am"),
), ),
migrations.AddField( migrations.AddField(
model_name='destinataerunterstuetzung', model_name="destinataerunterstuetzung",
name='ausgezahlt_von', 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'), 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( migrations.AddField(
model_name='destinataerunterstuetzung', model_name="destinataerunterstuetzung",
name='empfaenger_iban', name="empfaenger_iban",
field=models.CharField(blank=True, max_length=34, verbose_name='Empfänger IBAN'), field=models.CharField(
blank=True, max_length=34, verbose_name="Empfänger IBAN"
),
), ),
migrations.AddField( migrations.AddField(
model_name='destinataerunterstuetzung', model_name="destinataerunterstuetzung",
name='empfaenger_name', name="empfaenger_name",
field=models.CharField(blank=True, max_length=200, verbose_name='Empfänger Name'), field=models.CharField(
blank=True, max_length=200, verbose_name="Empfänger Name"
),
), ),
migrations.AddField( migrations.AddField(
model_name='destinataerunterstuetzung', model_name="destinataerunterstuetzung",
name='verwendungszweck', name="verwendungszweck",
field=models.CharField(blank=True, max_length=140, verbose_name='Verwendungszweck'), field=models.CharField(
blank=True, max_length=140, verbose_name="Verwendungszweck"
),
), ),
migrations.AlterField( migrations.AlterField(
model_name='destinataerunterstuetzung', model_name="destinataerunterstuetzung",
name='status', 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'), 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( migrations.CreateModel(
name='UnterstuetzungWiederkehrend', name="UnterstuetzungWiederkehrend",
fields=[ 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 (€)')), "id",
('intervall', models.CharField(choices=[('monatlich', 'Monatlich'), ('quartalsweise', 'Vierteljährlich'), ('halbjaehrlich', 'Halbjährlich'), ('jaehrlich', 'Jährlich')], max_length=20, verbose_name='Intervall')), models.UUIDField(
('beschreibung', models.CharField(blank=True, max_length=255, verbose_name='Beschreibung')), default=uuid.uuid4,
('empfaenger_iban', models.CharField(max_length=34, verbose_name='Empfänger IBAN')), editable=False,
('empfaenger_name', models.CharField(max_length=200, verbose_name='Empfänger Name')), primary_key=True,
('verwendungszweck', models.CharField(blank=True, max_length=140, verbose_name='Verwendungszweck')), serialize=False,
('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')), "betrag",
('erstellt_am', models.DateTimeField(auto_now_add=True)), models.DecimalField(
('destinataer', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='wiederkehrende_unterstuetzungen', to='stiftung.destinataer', verbose_name='Destinatär')), decimal_places=2, max_digits=12, verbose_name="Betrag (€)"
('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')), ),
(
"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={ options={
'verbose_name': 'Wiederkehrende Unterstützung', "verbose_name": "Wiederkehrende Unterstützung",
'verbose_name_plural': 'Wiederkehrende Unterstützungen', "verbose_name_plural": "Wiederkehrende Unterstützungen",
'ordering': ['-erstellt_am'], "ordering": ["-erstellt_am"],
}, },
), ),
migrations.AddField( migrations.AddField(
model_name='destinataerunterstuetzung', model_name="destinataerunterstuetzung",
name='wiederkehrend_von', 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'), field=models.ForeignKey(
blank=True,
null=True,
on_delete=django.db.models.deletion.SET_NULL,
to="stiftung.unterstuetzungwiederkehrend",
verbose_name="Wiederkehrende Zahlung",
),
), ),
migrations.AddIndex( migrations.AddIndex(
model_name='destinataerunterstuetzung', model_name="destinataerunterstuetzung",
index=models.Index(fields=['wiederkehrend_von'], name='stiftung_de_wiederk_3d5afc_idx'), index=models.Index(
fields=["wiederkehrend_von"], name="stiftung_de_wiederk_3d5afc_idx"
),
), ),
migrations.AddIndex( migrations.AddIndex(
model_name='unterstuetzungwiederkehrend', model_name="unterstuetzungwiederkehrend",
index=models.Index(fields=['aktiv', 'naechste_generierung'], name='stiftung_un_aktiv_b957e5_idx'), index=models.Index(
fields=["aktiv", "naechste_generierung"],
name="stiftung_un_aktiv_b957e5_idx",
),
), ),
migrations.AddIndex( migrations.AddIndex(
model_name='unterstuetzungwiederkehrend', model_name="unterstuetzungwiederkehrend",
index=models.Index(fields=['destinataer', 'aktiv'], name='stiftung_un_destina_2232fc_idx'), index=models.Index(
fields=["destinataer", "aktiv"], name="stiftung_un_destina_2232fc_idx"
),
), ),
] ]

View File

@@ -1,38 +1,106 @@
# Generated by Django 5.0.6 on 2025-09-02 19:56 # Generated by Django 5.0.6 on 2025-09-02 19:56
import uuid import uuid
from django.db import migrations, models from django.db import migrations, models
class Migration(migrations.Migration): class Migration(migrations.Migration):
dependencies = [ dependencies = [
('stiftung', '0026_enhance_unterstuetzung_model'), ("stiftung", "0026_enhance_unterstuetzung_model"),
] ]
operations = [ operations = [
migrations.CreateModel( migrations.CreateModel(
name='HelpBox', name="HelpBox",
fields=[ 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')), "id",
('title', models.CharField(max_length=200, verbose_name='Titel der Hilfsbox')), models.UUIDField(
('content', models.TextField(help_text='Sie können Markdown verwenden: **fett**, *kursiv*, `code`, [Link](url), etc.', verbose_name='Inhalt (Markdown unterstützt)')), default=uuid.uuid4,
('is_active', models.BooleanField(default=True, verbose_name='Aktiv')), editable=False,
('created_at', models.DateTimeField(auto_now_add=True, verbose_name='Erstellt am')), primary_key=True,
('updated_at', models.DateTimeField(auto_now=True, verbose_name='Aktualisiert am')), serialize=False,
('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')), ),
(
"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={ options={
'verbose_name': 'Hilfs-Infobox', "verbose_name": "Hilfs-Infobox",
'verbose_name_plural': 'Hilfs-Infoboxen', "verbose_name_plural": "Hilfs-Infoboxen",
'ordering': ['page_key'], "ordering": ["page_key"],
}, },
), ),
migrations.AlterField( migrations.AlterField(
model_name='appconfiguration', model_name="appconfiguration",
name='category', 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'), 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

@@ -6,13 +6,34 @@ from django.db import migrations, models
class Migration(migrations.Migration): class Migration(migrations.Migration):
dependencies = [ dependencies = [
('stiftung', '0027_helpbox_alter_appconfiguration_category'), ("stiftung", "0027_helpbox_alter_appconfiguration_category"),
] ]
operations = [ operations = [
migrations.AlterField( migrations.AlterField(
model_name='helpbox', model_name="helpbox",
name='page_key', 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'), 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",
),
), ),
] ]

File diff suppressed because it is too large Load Diff

View File

@@ -1,11 +1,14 @@
from rest_framework import serializers from rest_framework import serializers
from .models import Person, Foerderung
from .models import Foerderung, Person
class PersonSerializer(serializers.ModelSerializer): class PersonSerializer(serializers.ModelSerializer):
class Meta: class Meta:
model = Person model = Person
fields = "__all__" fields = "__all__"
class FoerderungSerializer(serializers.ModelSerializer): class FoerderungSerializer(serializers.ModelSerializer):
class Meta: class Meta:
model = Foerderung model = Foerderung

View File

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

View File

@@ -1,6 +1,7 @@
""" """
PDF-specific template tags and filters PDF-specific template tags and filters
""" """
from django import template from django import template
from django.utils.safestring import mark_safe from django.utils.safestring import mark_safe
@@ -17,7 +18,7 @@ def lookup(obj, field_name):
return None return None
# Handle dict-like objects # Handle dict-like objects
if hasattr(obj, '__getitem__') and not isinstance(obj, str): if hasattr(obj, "__getitem__") and not isinstance(obj, str):
try: try:
return obj[field_name] return obj[field_name]
except (KeyError, TypeError): except (KeyError, TypeError):
@@ -36,8 +37,8 @@ def lookup(obj, field_name):
return attr return attr
# Try to handle nested field access (e.g., "person.name") # Try to handle nested field access (e.g., "person.name")
if '.' in field_name: if "." in field_name:
parts = field_name.split('.') parts = field_name.split(".")
current = obj current = obj
for part in parts: for part in parts:
if current is None: if current is None:
@@ -57,7 +58,7 @@ def get_display_value(obj, field_name):
value = lookup(obj, field_name) value = lookup(obj, field_name)
# Try to get display value for choice fields # Try to get display value for choice fields
display_method = f'get_{field_name}_display' display_method = f"get_{field_name}_display"
if hasattr(obj, display_method): if hasattr(obj, display_method):
display_value = getattr(obj, display_method)() display_value = getattr(obj, display_method)()
if display_value: if display_value:
@@ -73,9 +74,9 @@ def format_currency(value):
Usage: {{ value|format_currency }} Usage: {{ value|format_currency }}
""" """
if value is None: if value is None:
return '-' return "-"
try: try:
return f"{float(value):,.2f}".replace(',', ' ') return f"{float(value):,.2f}".replace(",", " ")
except (ValueError, TypeError): except (ValueError, TypeError):
return str(value) return str(value)
@@ -87,10 +88,10 @@ def format_status_badge(status):
Usage: {{ status|format_status_badge }} Usage: {{ status|format_status_badge }}
""" """
if not status: if not status:
return '-' return "-"
status_lower = str(status).lower() status_lower = str(status).lower()
css_class = f'status-{status_lower}' css_class = f"status-{status_lower}"
return mark_safe(f'<span class="status-badge {css_class}">{status}</span>') return mark_safe(f'<span class="status-badge {css_class}">{status}</span>')
@@ -102,13 +103,13 @@ def truncate_field(value, max_length=50):
Usage: {{ value|truncate_field:30 }} Usage: {{ value|truncate_field:30 }}
""" """
if value is None: if value is None:
return '-' return "-"
str_value = str(value) str_value = str(value)
if len(str_value) <= max_length: if len(str_value) <= max_length:
return str_value return str_value
return str_value[:max_length-3] + '...' return str_value[: max_length - 3] + "..."
@register.simple_tag @register.simple_tag
@@ -117,29 +118,29 @@ def get_field_value(obj, field_config):
Get formatted field value based on field configuration Get formatted field value based on field configuration
Usage: {% get_field_value object field_config %} Usage: {% get_field_value object field_config %}
""" """
field_name = field_config.get('field_name') field_name = field_config.get("field_name")
field_type = field_config.get('field_type', 'text') field_type = field_config.get("field_type", "text")
value = lookup(obj, field_name) value = lookup(obj, field_name)
if value is None: if value is None:
return '-' return "-"
if field_type == 'currency': if field_type == "currency":
return format_currency(value) return format_currency(value)
elif field_type == 'date': elif field_type == "date":
try: try:
return value.strftime('%d.%m.%Y') return value.strftime("%d.%m.%Y")
except (AttributeError, ValueError): except (AttributeError, ValueError):
return str(value) return str(value)
elif field_type == 'datetime': elif field_type == "datetime":
try: try:
return value.strftime('%d.%m.%Y %H:%M') return value.strftime("%d.%m.%Y %H:%M")
except (AttributeError, ValueError): except (AttributeError, ValueError):
return str(value) return str(value)
elif field_type == 'status': elif field_type == "status":
return format_status_badge(value) return format_status_badge(value)
elif field_type == 'boolean': elif field_type == "boolean":
return 'Ja' if value else 'Nein' return "Ja" if value else "Nein"
else: else:
return truncate_field(value) return truncate_field(value)

View File

@@ -1,160 +1,337 @@
from django.urls import path from django.urls import path
from . import views from . import views
app_name = 'stiftung' app_name = "stiftung"
urlpatterns = [ urlpatterns = [
# Dashboard (Startseite) # Dashboard (Startseite)
path('', views.dashboard, name='dashboard'), path("", views.dashboard, name="dashboard"),
# Home (für Kompatibilität mit bestehenden Templates) # Home (für Kompatibilität mit bestehenden Templates)
path('home/', views.home, name='home'), path("home/", views.home, name="home"),
# CSV Import URLs # CSV Import URLs
path('import/', views.csv_import_list, name='csv_import_list'), path("import/", views.csv_import_list, name="csv_import_list"),
path('import/neu/', views.csv_import_create, name='csv_import_create'), path("import/neu/", views.csv_import_create, name="csv_import_create"),
# Destinatär URLs (Förderungsempfänger) # Destinatär URLs (Förderungsempfänger)
path('destinataere/', views.destinataer_list, name='destinataer_list'), path("destinataere/", views.destinataer_list, name="destinataer_list"),
path('destinataere/<uuid:pk>/', views.destinataer_detail, name='destinataer_detail'), path(
path('destinataere/neu/', views.destinataer_create, name='destinataer_create'), "destinataere/<uuid:pk>/", views.destinataer_detail, name="destinataer_detail"
path('destinataere/<uuid:pk>/bearbeiten/', views.destinataer_update, name='destinataer_update'), ),
path('destinataere/<uuid:pk>/loeschen/', views.destinataer_delete, name='destinataer_delete'), path("destinataere/neu/", views.destinataer_create, name="destinataer_create"),
path('destinataere/<uuid:pk>/notiz/', views.destinataer_notiz_create, name='destinataer_notiz_create'), path(
path('destinataere/<uuid:pk>/export/', views.destinataer_export, name='destinataer_export'), "destinataere/<uuid:pk>/bearbeiten/",
views.destinataer_update,
name="destinataer_update",
),
path(
"destinataere/<uuid:pk>/loeschen/",
views.destinataer_delete,
name="destinataer_delete",
),
path(
"destinataere/<uuid:pk>/notiz/",
views.destinataer_notiz_create,
name="destinataer_notiz_create",
),
path(
"destinataere/<uuid:pk>/export/",
views.destinataer_export,
name="destinataer_export",
),
# Paechter URLs (Landpächter) # Paechter URLs (Landpächter)
path('paechter/', views.paechter_list, name='paechter_list'), path("paechter/", views.paechter_list, name="paechter_list"),
path('paechter/<uuid:pk>/', views.paechter_detail, name='paechter_detail'), path("paechter/<uuid:pk>/", views.paechter_detail, name="paechter_detail"),
path('paechter/neu/', views.paechter_create, name='paechter_create'), path("paechter/neu/", views.paechter_create, name="paechter_create"),
path('paechter/<uuid:pk>/bearbeiten/', views.paechter_update, name='paechter_update'), path(
path('paechter/<uuid:pk>/loeschen/', views.paechter_delete, name='paechter_delete'), "paechter/<uuid:pk>/bearbeiten/", views.paechter_update, name="paechter_update"
path('paechter/<uuid:pk>/export/', views.paechter_export, name='paechter_export'), ),
path("paechter/<uuid:pk>/loeschen/", views.paechter_delete, name="paechter_delete"),
path("paechter/<uuid:pk>/export/", views.paechter_export, name="paechter_export"),
# Legacy Person URLs removed (Destinatäre ersetzen Personen) # Legacy Person URLs removed (Destinatäre ersetzen Personen)
# Land URLs # Land URLs
path('laendereien/', views.land_list, name='land_list'), path("laendereien/", views.land_list, name="land_list"),
path('laendereien/<uuid:pk>/', views.land_detail, name='land_detail'), path("laendereien/<uuid:pk>/", views.land_detail, name="land_detail"),
path('laendereien/neu/', views.land_create, name='land_create'), path("laendereien/neu/", views.land_create, name="land_create"),
path('laendereien/<uuid:pk>/bearbeiten/', views.land_update, name='land_update'), path("laendereien/<uuid:pk>/bearbeiten/", views.land_update, name="land_update"),
path('laendereien/<uuid:pk>/loeschen/', views.land_delete, name='land_delete'), path("laendereien/<uuid:pk>/loeschen/", views.land_delete, name="land_delete"),
path('laendereien/<uuid:pk>/export/', views.land_export, name='land_export'), path("laendereien/<uuid:pk>/export/", views.land_export, name="land_export"),
# Landabrechnung URLs # Landabrechnung URLs
path('landabrechnungen/', views.land_abrechnung_list, name='land_abrechnung_list'), path("landabrechnungen/", views.land_abrechnung_list, name="land_abrechnung_list"),
path('landabrechnungen/<uuid:pk>/', views.land_abrechnung_detail, name='land_abrechnung_detail'), path(
path('landabrechnungen/neu/', views.land_abrechnung_create, name='land_abrechnung_create'), "landabrechnungen/<uuid:pk>/",
path('landabrechnungen/<uuid:pk>/bearbeiten/', views.land_abrechnung_update, name='land_abrechnung_update'), views.land_abrechnung_detail,
path('landabrechnungen/<uuid:pk>/loeschen/', views.land_abrechnung_delete, name='land_abrechnung_delete'), name="land_abrechnung_detail",
),
path(
"landabrechnungen/neu/",
views.land_abrechnung_create,
name="land_abrechnung_create",
),
path(
"landabrechnungen/<uuid:pk>/bearbeiten/",
views.land_abrechnung_update,
name="land_abrechnung_update",
),
path(
"landabrechnungen/<uuid:pk>/loeschen/",
views.land_abrechnung_delete,
name="land_abrechnung_delete",
),
# Vereinheitlichte Verpachtung URLs (direkt im Land) # Vereinheitlichte Verpachtung URLs (direkt im Land)
path('laendereien/<uuid:land_pk>/verpachtung/neu/', views.land_verpachtung_create, name='land_verpachtung_create'), path(
path('laendereien/<uuid:land_pk>/verpachtung/bearbeiten/', views.land_verpachtung_edit, name='land_verpachtung_edit'), "laendereien/<uuid:land_pk>/verpachtung/neu/",
path('laendereien/<uuid:land_pk>/verpachtung/beenden/', views.land_verpachtung_end, name='land_verpachtung_end'), views.land_verpachtung_create,
name="land_verpachtung_create",
),
path(
"laendereien/<uuid:land_pk>/verpachtung/bearbeiten/",
views.land_verpachtung_edit,
name="land_verpachtung_edit",
),
path(
"laendereien/<uuid:land_pk>/verpachtung/beenden/",
views.land_verpachtung_end,
name="land_verpachtung_end",
),
# LandVerpachtung URLs (neue Verpachtungen) # LandVerpachtung URLs (neue Verpachtungen)
path('laendereien/verpachtungen/<uuid:pk>/', views.land_verpachtung_detail, name='land_verpachtung_detail'), path(
path('laendereien/verpachtungen/<uuid:pk>/bearbeiten/', views.land_verpachtung_update, name='land_verpachtung_update'), "laendereien/verpachtungen/<uuid:pk>/",
path('laendereien/verpachtungen/<uuid:pk>/beenden/', views.land_verpachtung_end_direct, name='land_verpachtung_end_direct'), 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 # Förderung URLs
path('foerderungen/', views.foerderung_list, name='foerderung_list'), path("foerderungen/", views.foerderung_list, name="foerderung_list"),
path('foerderungen/<uuid:pk>/', views.foerderung_detail, name='foerderung_detail'), path("foerderungen/<uuid:pk>/", views.foerderung_detail, name="foerderung_detail"),
path('foerderungen/neu/', views.foerderung_create, name='foerderung_create'), path("foerderungen/neu/", views.foerderung_create, name="foerderung_create"),
path('foerderungen/<uuid:pk>/bearbeiten/', views.foerderung_update, name='foerderung_update'), path(
path('foerderungen/<uuid:pk>/loeschen/', views.foerderung_delete, name='foerderung_delete'), "foerderungen/<uuid:pk>/bearbeiten/",
views.foerderung_update,
name="foerderung_update",
),
path(
"foerderungen/<uuid:pk>/loeschen/",
views.foerderung_delete,
name="foerderung_delete",
),
# Dokumente URLs # Dokumente URLs
path('dokumente/', views.dokument_list, name='dokument_list'), path("dokumente/", views.dokument_list, name="dokument_list"),
path('dokumente/<uuid:pk>/', views.dokument_detail, name='dokument_detail'), path("dokumente/<uuid:pk>/", views.dokument_detail, name="dokument_detail"),
path('dokumente/neu/', views.dokument_create, name='dokument_create'), path("dokumente/neu/", views.dokument_create, name="dokument_create"),
path('dokumente/<uuid:pk>/bearbeiten/', views.dokument_update, name='dokument_update'), path(
path('dokumente/<uuid:pk>/loeschen/', views.dokument_delete, name='dokument_delete'), "dokumente/<uuid:pk>/bearbeiten/", views.dokument_update, name="dokument_update"
),
path(
"dokumente/<uuid:pk>/loeschen/", views.dokument_delete, name="dokument_delete"
),
# Dokumentenverwaltung (Paperless-Integration, Verwaltung & Verknüpfung) # Dokumentenverwaltung (Paperless-Integration, Verwaltung & Verknüpfung)
path('dokumente/verwaltung/', views.dokument_management, name='dokument_management'), path(
"dokumente/verwaltung/", views.dokument_management, name="dokument_management"
),
# Legacy document URLs removed - use dokument_management instead # Legacy document URLs removed - use dokument_management instead
# Dokument-Verknüpfung # Dokument-Verknüpfung
path('api/link-document/search/', views.link_document_search, name='link_document_search'), path(
path('api/link-document/create/', views.link_document_create, name='link_document_create'), "api/link-document/search/",
path('api/link-document/list/', views.link_document_list, name='link_document_list'), views.link_document_search,
path('api/link-document/update/', views.link_document_update, name='link_document_update'), name="link_document_search",
path('api/link-document/delete/<uuid:link_id>/', views.link_document_delete, name='link_document_delete'), ),
path(
"api/link-document/create/",
views.link_document_create,
name="link_document_create",
),
path(
"api/link-document/list/", views.link_document_list, name="link_document_list"
),
path(
"api/link-document/update/",
views.link_document_update,
name="link_document_update",
),
path(
"api/link-document/delete/<uuid:link_id>/",
views.link_document_delete,
name="link_document_delete",
),
# Legacy dokument_verknuepfung URL removed - use dokument_management instead # Legacy dokument_verknuepfung URL removed - use dokument_management instead
# Jahresbericht URLs # Jahresbericht URLs
path('berichte/', views.bericht_list, name='bericht_list'), path("berichte/", views.bericht_list, name="bericht_list"),
path('berichte/jahresbericht/', views.jahresbericht_generate_redirect, name='jahresbericht_generate_redirect'), path(
path('berichte/jahresbericht/<int:jahr>/', views.jahresbericht_generate, name='jahresbericht_generate'), "berichte/jahresbericht/",
path('berichte/jahresbericht/<int:jahr>/pdf/', views.jahresbericht_pdf, name='jahresbericht_pdf'), views.jahresbericht_generate_redirect,
name="jahresbericht_generate_redirect",
),
path(
"berichte/jahresbericht/<int:jahr>/",
views.jahresbericht_generate,
name="jahresbericht_generate",
),
path(
"berichte/jahresbericht/<int:jahr>/pdf/",
views.jahresbericht_pdf,
name="jahresbericht_pdf",
),
# Geschäftsführung URLs # Geschäftsführung URLs
path('geschaeftsfuehrung/', views.geschaeftsfuehrung, name='geschaeftsfuehrung'), path("geschaeftsfuehrung/", views.geschaeftsfuehrung, name="geschaeftsfuehrung"),
path('geschaeftsfuehrung/konten/', views.konto_list, name='konto_list'), path("geschaeftsfuehrung/konten/", views.konto_list, name="konto_list"),
path('geschaeftsfuehrung/konten/neu/', views.konto_create, name='konto_create'), path("geschaeftsfuehrung/konten/neu/", views.konto_create, name="konto_create"),
path('geschaeftsfuehrung/konten/<uuid:pk>/', views.konto_detail, name='konto_detail'), path(
path('geschaeftsfuehrung/konten/<uuid:pk>/bearbeiten/', views.konto_edit, name='konto_edit'), "geschaeftsfuehrung/konten/<uuid:pk>/", views.konto_detail, name="konto_detail"
path('geschaeftsfuehrung/verwaltungskosten/', views.verwaltungskosten_list, name='verwaltungskosten_list'), ),
path('geschaeftsfuehrung/verwaltungskosten/neu/', views.verwaltungskosten_create, name='verwaltungskosten_create'), path(
path('geschaeftsfuehrung/verwaltungskosten/<uuid:pk>/bearbeiten/', views.verwaltungskosten_edit, name='verwaltungskosten_edit'), "geschaeftsfuehrung/konten/<uuid:pk>/bearbeiten/",
path('verwaltungskosten/mark-paid/', views.mark_expense_paid, name='mark_expense_paid'), views.konto_edit,
path('geschaeftsfuehrung/rentmeister/', views.rentmeister_list, name='rentmeister_list'), name="konto_edit",
path('geschaeftsfuehrung/rentmeister/neu/', views.rentmeister_create, name='rentmeister_create'), ),
path('geschaeftsfuehrung/rentmeister/<uuid:pk>/', views.rentmeister_detail, name='rentmeister_detail'), path(
path('geschaeftsfuehrung/rentmeister/<uuid:pk>/bearbeiten/', views.rentmeister_edit, name='rentmeister_edit'), "geschaeftsfuehrung/verwaltungskosten/",
path('geschaeftsfuehrung/rentmeister/<uuid:pk>/ausgaben/', views.rentmeister_ausgaben, name='rentmeister_ausgaben'), views.verwaltungskosten_list,
name="verwaltungskosten_list",
),
path(
"geschaeftsfuehrung/verwaltungskosten/neu/",
views.verwaltungskosten_create,
name="verwaltungskosten_create",
),
path(
"geschaeftsfuehrung/verwaltungskosten/<uuid:pk>/bearbeiten/",
views.verwaltungskosten_edit,
name="verwaltungskosten_edit",
),
path(
"verwaltungskosten/mark-paid/",
views.mark_expense_paid,
name="mark_expense_paid",
),
path(
"geschaeftsfuehrung/rentmeister/",
views.rentmeister_list,
name="rentmeister_list",
),
path(
"geschaeftsfuehrung/rentmeister/neu/",
views.rentmeister_create,
name="rentmeister_create",
),
path(
"geschaeftsfuehrung/rentmeister/<uuid:pk>/",
views.rentmeister_detail,
name="rentmeister_detail",
),
path(
"geschaeftsfuehrung/rentmeister/<uuid:pk>/bearbeiten/",
views.rentmeister_edit,
name="rentmeister_edit",
),
path(
"geschaeftsfuehrung/rentmeister/<uuid:pk>/ausgaben/",
views.rentmeister_ausgaben,
name="rentmeister_ausgaben",
),
# Administration URLs # Administration URLs
path('administration/', views.administration, name='administration'), path("administration/", views.administration, name="administration"),
path('administration/settings/', views.app_settings, name='app_settings'), path("administration/settings/", views.app_settings, name="app_settings"),
path('administration/audit-log/', views.audit_log_list, name='audit_log_list'), path("administration/audit-log/", views.audit_log_list, name="audit_log_list"),
path('administration/backup/', views.backup_management, name='backup_management'), path("administration/backup/", views.backup_management, name="backup_management"),
path('administration/backup/<uuid:backup_id>/download/', views.backup_download, name='backup_download'), path(
path('administration/backup/restore/', views.backup_restore, name='backup_restore'), "administration/backup/<uuid:backup_id>/download/",
path('administration/unterstuetzungen/', views.unterstuetzungen_list, name='unterstuetzungen_list'), views.backup_download,
path('administration/unterstuetzungen/<uuid:pk>/bearbeiten/', views.unterstuetzung_edit, name='unterstuetzung_edit'), name="backup_download",
path('administration/unterstuetzungen/<uuid:pk>/loeschen/', views.unterstuetzung_delete, name='unterstuetzung_delete'), ),
path("administration/backup/restore/", views.backup_restore, name="backup_restore"),
path(
"administration/unterstuetzungen/",
views.unterstuetzungen_list,
name="unterstuetzungen_list",
),
path(
"administration/unterstuetzungen/<uuid:pk>/bearbeiten/",
views.unterstuetzung_edit,
name="unterstuetzung_edit",
),
path(
"administration/unterstuetzungen/<uuid:pk>/loeschen/",
views.unterstuetzung_delete,
name="unterstuetzung_delete",
),
# Unterstützungen URLs (direct access from Destinataer) # Unterstützungen URLs (direct access from Destinataer)
path('unterstuetzungen/', views.unterstuetzungen_all, name='unterstuetzungen_all'), path("unterstuetzungen/", views.unterstuetzungen_all, name="unterstuetzungen_all"),
path('unterstuetzungen/neu/', views.unterstuetzung_create, name='unterstuetzung_create'), path(
path('unterstuetzungen/<uuid:pk>/', views.unterstuetzung_detail, name='unterstuetzung_detail'), "unterstuetzungen/neu/",
path('unterstuetzungen/<uuid:pk>/bezahlt/', views.unterstuetzung_mark_paid, name='unterstuetzung_mark_paid'), views.unterstuetzung_create,
path('unterstuetzungen/wiederkehrend/', views.wiederkehrende_unterstuetzungen, name='wiederkehrende_unterstuetzungen'), 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 # AJAX endpoints
path('api/destinataer/<uuid:destinataer_id>/info/', views.get_destinataer_info, name='get_destinataer_info'), path(
"api/destinataer/<uuid:destinataer_id>/info/",
views.get_destinataer_info,
name="get_destinataer_info",
),
# Authentication URLs # Authentication URLs
path('login/', views.user_login, name='login'), path("login/", views.user_login, name="login"),
path('logout/', views.user_logout, name='logout'), path("logout/", views.user_logout, name="logout"),
# User Management URLs # User Management URLs
path('administration/users/', views.user_management, name='user_management'), path("administration/users/", views.user_management, name="user_management"),
path('administration/users/create/', views.user_create, name='user_create'), path("administration/users/create/", views.user_create, name="user_create"),
path('administration/users/<int:pk>/', views.user_detail, name='user_detail'), path("administration/users/<int:pk>/", views.user_detail, name="user_detail"),
path('administration/users/<int:pk>/edit/', views.user_edit, name='user_edit'), path("administration/users/<int:pk>/edit/", views.user_edit, name="user_edit"),
path('administration/users/<int:pk>/password/', views.user_change_password, name='user_change_password'), path(
path('administration/users/<int:pk>/permissions/', views.user_permissions, name='user_permissions'), "administration/users/<int:pk>/password/",
path('administration/users/<int:pk>/delete/', views.user_delete, name='user_delete'), views.user_change_password,
name="user_change_password",
),
path(
"administration/users/<int:pk>/permissions/",
views.user_permissions,
name="user_permissions",
),
path(
"administration/users/<int:pk>/delete/", views.user_delete, name="user_delete"
),
# Hilfsbox URLs # Hilfsbox URLs
path('help-box/edit/', views.edit_help_box, name='edit_help_box'), path("help-box/edit/", views.edit_help_box, name="edit_help_box"),
path('help-box/admin/', views.edit_help_box, name='help_boxes_admin'), path("help-box/admin/", views.edit_help_box, name="help_boxes_admin"),
# API URLs # API URLs
path('api/land-stats/', views.land_stats_api, name='land_stats_api'), path("api/land-stats/", views.land_stats_api, name="land_stats_api"),
path('api/health/', views.health_check, name='health_check'), path("api/health/", views.health_check, name="health_check"),
path('api/paperless/ping/', views.paperless_ping, name='paperless_ping'), path("api/paperless/ping/", views.paperless_ping, name="paperless_ping"),
path('api/paperless/documents/', views.paperless_documents, name='paperless_documents'), path(
path('api/paperless/tags/', views.paperless_tags_only, name='paperless_tags_only'), "api/paperless/documents/",
path('api/paperless/debug/', views.paperless_debug, name='paperless_debug'), views.paperless_documents,
path('api/paperless/documents/<int:doc_id>/', views.paperless_document_redirect, name='paperless_document_redirect'), name="paperless_documents",
),
path("api/paperless/tags/", views.paperless_tags_only, name="paperless_tags_only"),
path("api/paperless/debug/", views.paperless_debug, name="paperless_debug"),
path(
"api/paperless/documents/<int:doc_id>/",
views.paperless_document_redirect,
name="paperless_document_redirect",
),
# Gramps integration (probe) # Gramps integration (probe)
path('api/gramps/search/', views.gramps_search_api, name='gramps_search_api'), path("api/gramps/search/", views.gramps_search_api, name="gramps_search_api"),
path('api/gramps/debug/', views.gramps_debug_api, name='gramps_debug_api'), path("api/gramps/debug/", views.gramps_debug_api, name="gramps_debug_api"),
] ]

View File

@@ -1,7 +1,9 @@
""" """
Configuration utilities for the Stiftung application Configuration utilities for the Stiftung application
""" """
from django.conf import settings from django.conf import settings
from stiftung.models import AppConfiguration from stiftung.models import AppConfiguration
@@ -36,14 +38,16 @@ def get_paperless_config():
dict: Dictionary containing all Paperless configuration dict: Dictionary containing all Paperless configuration
""" """
return { return {
'api_url': get_config('paperless_api_url', 'http://192.168.178.167:30070'), "api_url": get_config("paperless_api_url", "http://192.168.178.167:30070"),
'api_token': get_config('paperless_api_token', ''), "api_token": get_config("paperless_api_token", ""),
'destinataere_tag': get_config('paperless_destinataere_tag', 'Stiftung_Destinatäre'), "destinataere_tag": get_config(
'destinataere_tag_id': get_config('paperless_destinataere_tag_id', '210'), "paperless_destinataere_tag", "Stiftung_Destinatäre"
'land_tag': get_config('paperless_land_tag', 'Stiftung_Land_und_Pächter'), ),
'land_tag_id': get_config('paperless_land_tag_id', '204'), "destinataere_tag_id": get_config("paperless_destinataere_tag_id", "210"),
'admin_tag': get_config('paperless_admin_tag', 'Stiftung_Administration'), "land_tag": get_config("paperless_land_tag", "Stiftung_Land_und_Pächter"),
'admin_tag_id': get_config('paperless_admin_tag_id', '216'), "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"),
} }
@@ -70,4 +74,4 @@ def is_paperless_configured():
bool: True if API URL and token are configured bool: True if API URL and token are configured
""" """
config = get_paperless_config() config = get_paperless_config()
return bool(config['api_url'] and config['api_token']) return bool(config["api_url"] and config["api_token"])

View File

@@ -1,18 +1,21 @@
""" """
PDF generation utilities with corporate identity support PDF generation utilities with corporate identity support
""" """
import os
import base64 import base64
import os
from io import BytesIO from io import BytesIO
from django.conf import settings from django.conf import settings
from django.template.loader import render_to_string
from django.http import HttpResponse from django.http import HttpResponse
from django.template.loader import render_to_string
from django.utils import timezone from django.utils import timezone
# Try to import WeasyPrint, fall back gracefully if not available # Try to import WeasyPrint, fall back gracefully if not available
try: try:
from weasyprint import HTML, CSS from weasyprint import CSS, HTML
from weasyprint.text.fonts import FontConfiguration from weasyprint.text.fonts import FontConfiguration
WEASYPRINT_AVAILABLE = True WEASYPRINT_AVAILABLE = True
IMPORT_ERROR = None IMPORT_ERROR = None
except ImportError as e: except ImportError as e:
@@ -49,16 +52,28 @@ class PDFGenerator:
def get_corporate_settings(self): def get_corporate_settings(self):
"""Get corporate identity settings from configuration""" """Get corporate identity settings from configuration"""
return { return {
'stiftung_name': AppConfiguration.get_setting('corporate_stiftung_name', 'Stiftung'), "stiftung_name": AppConfiguration.get_setting(
'logo_path': AppConfiguration.get_setting('corporate_logo_path', ''), "corporate_stiftung_name", "Stiftung"
'primary_color': AppConfiguration.get_setting('corporate_primary_color', '#2c3e50'), ),
'secondary_color': AppConfiguration.get_setting('corporate_secondary_color', '#3498db'), "logo_path": AppConfiguration.get_setting("corporate_logo_path", ""),
'address_line1': AppConfiguration.get_setting('corporate_address_line1', ''), "primary_color": AppConfiguration.get_setting(
'address_line2': AppConfiguration.get_setting('corporate_address_line2', ''), "corporate_primary_color", "#2c3e50"
'phone': AppConfiguration.get_setting('corporate_phone', ''), ),
'email': AppConfiguration.get_setting('corporate_email', ''), "secondary_color": AppConfiguration.get_setting(
'website': AppConfiguration.get_setting('corporate_website', ''), "corporate_secondary_color", "#3498db"
'footer_text': AppConfiguration.get_setting('corporate_footer_text', 'Dieser Bericht wurde automatisch generiert.'), ),
"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): def get_logo_base64(self, logo_path):
@@ -70,25 +85,25 @@ class PDFGenerator:
possible_paths = [ possible_paths = [
logo_path, logo_path,
os.path.join(settings.MEDIA_ROOT, logo_path), os.path.join(settings.MEDIA_ROOT, logo_path),
os.path.join(settings.STATIC_ROOT or '', logo_path), os.path.join(settings.STATIC_ROOT or "", logo_path),
os.path.join(settings.BASE_DIR, 'static', logo_path), os.path.join(settings.BASE_DIR, "static", logo_path),
] ]
for path in possible_paths: for path in possible_paths:
if os.path.exists(path): if os.path.exists(path):
try: try:
with open(path, 'rb') as img_file: with open(path, "rb") as img_file:
img_data = base64.b64encode(img_file.read()).decode('utf-8') img_data = base64.b64encode(img_file.read()).decode("utf-8")
# Determine MIME type # Determine MIME type
ext = os.path.splitext(path)[1].lower() ext = os.path.splitext(path)[1].lower()
if ext in ['.jpg', '.jpeg']: if ext in [".jpg", ".jpeg"]:
mime_type = 'image/jpeg' mime_type = "image/jpeg"
elif ext == '.png': elif ext == ".png":
mime_type = 'image/png' mime_type = "image/png"
elif ext == '.svg': elif ext == ".svg":
mime_type = 'image/svg+xml' mime_type = "image/svg+xml"
else: else:
mime_type = 'image/png' # default mime_type = "image/png" # default
return f"data:{mime_type};base64,{img_data}" return f"data:{mime_type};base64,{img_data}"
except Exception: except Exception:
@@ -98,8 +113,8 @@ class PDFGenerator:
def get_base_css(self, corporate_settings): def get_base_css(self, corporate_settings):
"""Generate base CSS for corporate identity""" """Generate base CSS for corporate identity"""
primary_color = corporate_settings.get('primary_color', '#2c3e50') primary_color = corporate_settings.get("primary_color", "#2c3e50")
secondary_color = corporate_settings.get('secondary_color', '#3498db') secondary_color = corporate_settings.get("secondary_color", "#3498db")
return f""" return f"""
@page {{ @page {{
@@ -320,8 +335,10 @@ class PDFGenerator:
</body> </body>
</html> </html>
""" """
response = HttpResponse(error_html, content_type='text/html') response = HttpResponse(error_html, content_type="text/html")
response['Content-Disposition'] = f'inline; filename="{filename.replace(".pdf", "_preview.html")}"' response["Content-Disposition"] = (
f'inline; filename="{filename.replace(".pdf", "_preview.html")}"'
)
return response return response
try: try:
@@ -333,12 +350,13 @@ class PDFGenerator:
# Generate PDF # Generate PDF
html_doc = HTML(string=html_content) html_doc = HTML(string=html_content)
pdf_bytes = html_doc.write_pdf(stylesheets=[css] if css else None, pdf_bytes = html_doc.write_pdf(
font_config=self.font_config) stylesheets=[css] if css else None, font_config=self.font_config
)
# Create response # Create response
response = HttpResponse(pdf_bytes, content_type='application/pdf') response = HttpResponse(pdf_bytes, content_type="application/pdf")
response['Content-Disposition'] = f'attachment; filename="{filename}"' response["Content-Disposition"] = f'attachment; filename="{filename}"'
return response return response
except Exception as e: except Exception as e:
@@ -369,11 +387,15 @@ class PDFGenerator:
</html> </html>
""" """
response = HttpResponse(error_html, content_type='text/html') response = HttpResponse(error_html, content_type="text/html")
response['Content-Disposition'] = f'inline; filename="error_{filename.replace(".pdf", ".html")}"' response["Content-Disposition"] = (
f'inline; filename="error_{filename.replace(".pdf", ".html")}"'
)
return response return response
def export_data_list_pdf(self, data, fields_config, title, filename_prefix, request_user=None): def export_data_list_pdf(
self, data, fields_config, title, filename_prefix, request_user=None
):
""" """
Export a list of data as formatted PDF Export a list of data as formatted PDF
@@ -385,32 +407,37 @@ class PDFGenerator:
request_user: User making the request (for audit purposes) request_user: User making the request (for audit purposes)
""" """
corporate_settings = self.get_corporate_settings() corporate_settings = self.get_corporate_settings()
logo_base64 = self.get_logo_base64(corporate_settings.get('logo_path', '')) logo_base64 = self.get_logo_base64(corporate_settings.get("logo_path", ""))
# Prepare context # Prepare context
context = { context = {
'corporate_settings': corporate_settings, "corporate_settings": corporate_settings,
'logo_base64': logo_base64, "logo_base64": logo_base64,
'title': title, "title": title,
'data': data, "data": data,
'fields_config': fields_config, "fields_config": fields_config,
'generation_date': timezone.now(), "generation_date": timezone.now(),
'generated_by': (request_user.get_full_name() "generated_by": (
if hasattr(request_user, 'get_full_name') and request_user.get_full_name() request_user.get_full_name()
else request_user.username if hasattr(request_user, "get_full_name")
if hasattr(request_user, 'username') and request_user.username and request_user.get_full_name()
else 'System'), else (
'total_count': len(data) if hasattr(data, '__len__') else data.count(), 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 # Render HTML
html_content = render_to_string('pdf/data_list.html', context) html_content = render_to_string("pdf/data_list.html", context)
# Generate CSS # Generate CSS
css_content = self.get_base_css(corporate_settings) css_content = self.get_base_css(corporate_settings)
# Generate filename # Generate filename
timestamp = timezone.now().strftime('%Y%m%d_%H%M%S') timestamp = timezone.now().strftime("%Y%m%d_%H%M%S")
filename = f"{filename_prefix}_{timestamp}.pdf" filename = f"{filename_prefix}_{timestamp}.pdf"
return self.generate_pdf_response(html_content, filename, css_content) return self.generate_pdf_response(html_content, filename, css_content)

File diff suppressed because it is too large Load Diff