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,18 +15,20 @@ 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
Args: Args:
request: Django request object request: Django request object
action: Action type (create, update, delete, etc.) action: Action type (create, update, delete, etc.)
@@ -35,28 +39,28 @@ def log_action(request, action, entity_type, entity_id, entity_name, description
changes: Dictionary of field changes (optional) 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(
user=user, user=user,
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
@@ -64,14 +68,14 @@ def log_create(request, entity_type, entity_id, entity_name, description=None):
"""Log entity creation""" """Log entity creation"""
if not description: if not description:
description = f"Neue {entity_type.replace('_', ' ').title()} '{entity_name}' wurde erstellt" description = f"Neue {entity_type.replace('_', ' ').title()} '{entity_name}' wurde erstellt"
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,
) )
@@ -81,60 +85,78 @@ def log_update(request, entity_type, entity_id, entity_name, changes, descriptio
changed_fields = list(changes.keys()) if changes else [] changed_fields = list(changes.keys()) if changes else []
fields_str = ", ".join(changed_fields) fields_str = ", ".join(changed_fields)
description = f"{entity_type.replace('_', ' ').title()} '{entity_name}' wurde aktualisiert: {fields_str}" description = f"{entity_type.replace('_', ' ').title()} '{entity_name}' wurde aktualisiert: {fields_str}"
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,51 +165,54 @@ 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,
) )
def track_model_changes(old_instance, new_instance, exclude_fields=None): def track_model_changes(old_instance, new_instance, exclude_fields=None):
""" """
Track changes between model instances Track changes between model instances
Args: Args:
old_instance: Original model instance old_instance: Original model instance
new_instance: Updated model instance new_instance: Updated model instance
exclude_fields: List of fields to exclude from tracking exclude_fields: List of fields to exclude from tracking
Returns: Returns:
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 = {}
if old_instance and new_instance: if old_instance and new_instance:
for field in new_instance._meta.fields: for field in new_instance._meta.fields:
field_name = field.name field_name = field.name
if field_name in exclude_fields: if field_name in exclude_fields:
continue continue
old_value = getattr(old_instance, field_name, None) old_value = getattr(old_instance, field_name, None)
new_value = getattr(new_instance, field_name, None) new_value = getattr(new_instance, field_name, None)
# Convert to string for comparison # Convert to string for comparison
old_str = str(old_value) if old_value is not None else None old_str = str(old_value) if old_value is not None else None
new_str = str(new_value) if new_value is not None else None 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,38 +220,39 @@ 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"""
if self.audit_entity_type: if self.audit_entity_type:
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}"
def log_create_action(self, instance): def log_create_action(self, instance):
"""Log creation of an instance""" """Log creation of an instance"""
log_create( log_create(
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):
"""Log update of an instance""" """Log update of an instance"""
changes = track_model_changes(old_instance, new_instance) changes = track_model_changes(old_instance, new_instance)
@@ -236,16 +262,16 @@ 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):
"""Log deletion of an instance""" """Log deletion of an instance"""
log_delete( log_delete(
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,48 +30,48 @@ 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")
# 3. Create metadata file # 3. Create metadata file
create_backup_metadata(staging_dir, backup_job) create_backup_metadata(staging_dir, backup_job)
# 4. Create compressed archive # 4. Create compressed archive
create_compressed_backup(staging_dir, backup_path) create_compressed_backup(staging_dir, backup_path)
# 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,37 +80,42 @@ 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)
if result.returncode != 0: if result.returncode != 0:
raise Exception(f"pg_dump failed: {result.stderr}") raise Exception(f"pg_dump failed: {result.stderr}")
return db_backup_file return db_backup_file
except Exception as e: except Exception as e:
print(f"Database backup failed: {e}") print(f"Database backup failed: {e}")
return None return None
@@ -117,28 +124,28 @@ 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:
if os.path.exists(source_path): if os.path.exists(source_path):
basename = os.path.basename(source_path) basename = os.path.basename(source_path)
dest_path = os.path.join(files_dir, basename) dest_path = os.path.join(files_dir, basename)
if os.path.isdir(source_path): if os.path.isdir(source_path):
shutil.copytree(source_path, dest_path) shutil.copytree(source_path, dest_path)
else: else:
shutil.copy2(source_path, dest_path) shutil.copy2(source_path, dest_path)
return files_dir return files_dir
except Exception as e: except Exception as e:
print(f"Files backup failed: {e}") print(f"Files backup failed: {e}")
return None return None
@@ -147,26 +154,28 @@ def create_files_backup(staging_dir):
def create_backup_metadata(staging_dir, backup_job): def create_backup_metadata(staging_dir, backup_job):
"""Create metadata file with backup information""" """Create metadata file with backup information"""
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,42 +235,47 @@ 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}")
except Exception as e: except Exception as e:
raise Exception(f"Database restore failed: {e}") raise Exception(f"Database restore failed: {e}")
@@ -270,29 +285,31 @@ 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():
source_path = os.path.join(files_dir, source_name) source_path = os.path.join(files_dir, source_name)
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:
shutil.copy2(dest_path, backup_path) shutil.copy2(dest_path, backup_path)
# Restore files # Restore files
if os.path.isdir(source_path): if os.path.isdir(source_path):
shutil.copytree(source_path, dest_path) shutil.copytree(source_path, dest_path)
else: else:
shutil.copy2(source_path, dest_path) shutil.copy2(source_path, dest_path)
except Exception as e: except Exception as e:
raise Exception(f"Files restore failed: {e}") raise Exception(f"Files restore failed: {e}")
@@ -302,19 +319,19 @@ def cleanup_old_backups(keep_count=10):
try: try:
backup_dir = get_backup_directory() backup_dir = get_backup_directory()
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)))
# Sort by modification time (newest first) # Sort by modification time (newest first)
backup_files.sort(key=lambda x: x[1], reverse=True) backup_files.sort(key=lambda x: x[1], reverse=True)
# Remove old backups # Remove old backups
for filepath, _ in backup_files[keep_count:]: for filepath, _ in backup_files[keep_count:]:
os.remove(filepath) os.remove(filepath)
print(f"Removed old backup: {os.path.basename(filepath)}") print(f"Removed old backup: {os.path.basename(filepath)}")
except Exception as e: except Exception as e:
print(f"Cleanup failed: {e}") print(f"Cleanup failed: {e}")

File diff suppressed because it is too large Load Diff

View File

@@ -3,60 +3,64 @@ 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)
self.stdout.write( self.stdout.write(
self.style.SUCCESS( self.style.SUCCESS(
f'Checking for recurring payments due up to {cutoff_date.strftime("%d.%m.%Y")}...' f'Checking for recurring payments due up to {cutoff_date.strftime("%d.%m.%Y")}...'
) )
) )
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
for template in templates: for template in templates:
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,84 +1,93 @@
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
with transaction.atomic(): with transaction.atomic():
for verpachtung in aktive_verpachtungen: for verpachtung in aktive_verpachtungen:
land = verpachtung.land land = verpachtung.land
# Prüfen ob bereits migriert # Prüfen ob bereits migriert
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)
# Pachtzins übertragen # Pachtzins übertragen
land.pachtzins_pauschal = verpachtung.pachtzins_jaehrlich land.pachtzins_pauschal = verpachtung.pachtzins_jaehrlich
# Verpachtete Fläche aktualisieren (falls nicht gesetzt) # Verpachtete Fläche aktualisieren (falls nicht gesetzt)
if land.verp_flaeche_aktuell == 0: if land.verp_flaeche_aktuell == 0:
land.verp_flaeche_aktuell = verpachtung.verpachtete_flaeche land.verp_flaeche_aktuell = verpachtung.verpachtete_flaeche
land.save() land.save()
migrated_count += 1 migrated_count += 1
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"
) )
) )
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"""
parts = [] parts = []
@@ -88,5 +97,5 @@ class Command(BaseCommand):
parts.append(f"{paechter.plz} {paechter.ort}") parts.append(f"{paechter.plz} {paechter.ort}")
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,110 +12,116 @@ 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(
verpachtung.land, verpachtung.land,
@@ -123,131 +129,143 @@ 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}"
) )
if dry_run: if dry_run:
# Rollback transaction in dry run # Rollback transaction in dry run
transaction.set_rollback(True) transaction.set_rollback(True)
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"""
if not start_date: if not start_date:
return [] return []
years = [] years = []
start_year = start_date.year start_year = start_date.year
end_year = end_date.year if end_date else date.today().year end_year = end_date.year if end_date else date.today().year
if target_year: if target_year:
if start_year <= target_year <= end_year: if start_year <= target_year <= end_year:
return [target_year] return [target_year]
else: else:
return [] return []
for year in range(start_year, end_year + 1): for year in range(start_year, end_year + 1):
years.append(year) years.append(year)
return years return years
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
proportion = Decimal(str(days_active)) / Decimal(str(days_in_year)) proportion = Decimal(str(days_active)) / Decimal(str(days_in_year))
return Decimal(str(verpachtung.pachtzins_jaehrlich)) * proportion return Decimal(str(verpachtung.pachtzins_jaehrlich)) * proportion
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)
contract_start = max(verpachtung.pachtbeginn, year_start) contract_start = max(verpachtung.pachtbeginn, year_start)
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
proportion = Decimal(str(days_active)) / Decimal(str(days_in_year)) proportion = Decimal(str(days_active)) / Decimal(str(days_in_year))
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
if not created and force: if not created and force:
# Update existing # Update existing
abrechnung.pacht_vereinnahmt += rent_amount abrechnung.pacht_vereinnahmt += rent_amount
abrechnung.umlagen_vereinnahmt += umlage_amount abrechnung.umlagen_vereinnahmt += umlage_amount
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
abrechnung.save() abrechnung.save()
updated = True updated = True
return created, updated return created, updated

View File

@@ -1,111 +1,127 @@
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
with transaction.atomic(): with transaction.atomic():
current_land = None current_land = None
for verpachtung in alle_verpachtungen: for verpachtung in alle_verpachtungen:
land = verpachtung.land land = verpachtung.land
# Für jedes Land nur die neueste aktive Verpachtung als "aktuell" setzen # Für jedes Land nur die neueste aktive Verpachtung als "aktuell" setzen
if current_land != land: if current_land != land:
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)
land.pachtzins_pauschal = verpachtung.pachtzins_jaehrlich land.pachtzins_pauschal = verpachtung.pachtzins_jaehrlich
# Verpachtete Fläche synchronisieren # Verpachtete Fläche synchronisieren
land.verp_flaeche_aktuell = verpachtung.verpachtete_flaeche land.verp_flaeche_aktuell = verpachtung.verpachtete_flaeche
land.save() land.save()
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"""
parts = [] parts = []
@@ -115,5 +131,5 @@ class Command(BaseCommand):
parts.append(f"{paechter.plz} {paechter.ort}") parts.append(f"{paechter.plz} {paechter.ort}")
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()
@@ -18,54 +20,54 @@ class AuditMiddleware(MiddlewareMixin):
""" """
Middleware that sets up request context for audit logging Middleware that sets up request context for audit logging
""" """
def process_request(self, request): def process_request(self, request):
"""Store request in thread-local storage for access in signal handlers""" """Store request in thread-local storage for access in signal handlers"""
_local.request = request _local.request = request
_local.user_changes = {} # Store pre-save state for change tracking _local.user_changes = {} # Store pre-save state for change tracking
return None return None
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):
"""Map Django model to audit entity type""" """Map Django model to audit entity type"""
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,22 +78,22 @@ 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
if not request.user.is_authenticated: if not request.user.is_authenticated:
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,53 +104,53 @@ 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
if not request.user.is_authenticated: if not request.user.is_authenticated:
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)
entity_name = get_entity_name(instance) entity_name = get_entity_name(instance)
entity_id = str(instance.pk) entity_id = str(instance.pk)
if created: if created:
# Log creation # Log creation
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)
if changes: # Only log if there are actual changes if changes: # Only log if there are actual changes
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,33 +158,35 @@ 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
if not request.user.is_authenticated: if not request.user.is_authenticated:
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
@@ -15,14 +16,14 @@ def lookup(obj, field_name):
""" """
if obj is None: if obj is None:
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):
pass pass
# Handle objects with attributes # Handle objects with attributes
if hasattr(obj, field_name): if hasattr(obj, field_name):
attr = getattr(obj, field_name) attr = getattr(obj, field_name)
@@ -34,17 +35,17 @@ def lookup(obj, field_name):
# Method requires arguments, return as is # Method requires arguments, return as is
return attr return attr
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:
return None return None
current = lookup(current, part) current = lookup(current, part)
return current return current
return None return None
@@ -55,14 +56,14 @@ def get_display_value(obj, field_name):
Usage: {{ object|get_display_value:"field_name" }} Usage: {{ object|get_display_value:"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:
return display_value return display_value
return value return 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,11 +88,11 @@ 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,61 +1,65 @@
""" """
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
def get_config(key, default=None, fallback_to_settings=True): def get_config(key, default=None, fallback_to_settings=True):
""" """
Get a configuration value from the database or fall back to Django settings Get a configuration value from the database or fall back to Django settings
Args: Args:
key: The configuration key key: The configuration key
default: Default value if not found default: Default value if not found
fallback_to_settings: If True, try to get from Django settings using the key in uppercase fallback_to_settings: If True, try to get from Django settings using the key in uppercase
Returns: Returns:
The configuration value The configuration value
""" """
# Try to get from AppConfiguration first # Try to get from AppConfiguration first
value = AppConfiguration.get_setting(key, None) value = AppConfiguration.get_setting(key, None)
# Fall back to Django settings if value is None or empty string # Fall back to Django settings if value is None or empty string
if not value and fallback_to_settings: if not value and fallback_to_settings:
settings_key = key.upper() settings_key = key.upper()
return getattr(settings, settings_key, default) return getattr(settings, settings_key, default)
return value if value is not None else default return value if value is not None else default
def get_paperless_config(): def get_paperless_config():
""" """
Get all Paperless-related configuration values Get all Paperless-related configuration values
Returns: Returns:
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"),
} }
def set_config(key, value, **kwargs): def set_config(key, value, **kwargs):
""" """
Set a configuration value Set a configuration value
Args: Args:
key: The configuration key key: The configuration key
value: The value to set value: The value to set
**kwargs: Additional parameters for AppConfiguration.set_setting **kwargs: Additional parameters for AppConfiguration.set_setting
Returns: Returns:
AppConfiguration: The configuration object AppConfiguration: The configuration object
""" """
@@ -65,9 +69,9 @@ def set_config(key, value, **kwargs):
def is_paperless_configured(): def is_paperless_configured():
""" """
Check if Paperless is properly configured Check if Paperless is properly configured
Returns: Returns:
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:
@@ -35,72 +38,84 @@ from stiftung.models import AppConfiguration
class PDFGenerator: class PDFGenerator:
"""Corporate identity PDF generator""" """Corporate identity PDF generator"""
def __init__(self): def __init__(self):
if WEASYPRINT_AVAILABLE: if WEASYPRINT_AVAILABLE:
self.font_config = FontConfiguration() self.font_config = FontConfiguration()
else: else:
self.font_config = None self.font_config = None
def is_available(self): def is_available(self):
"""Check if PDF generation is available""" """Check if PDF generation is available"""
return WEASYPRINT_AVAILABLE return WEASYPRINT_AVAILABLE
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):
"""Convert logo to base64 for embedding in PDF""" """Convert logo to base64 for embedding in PDF"""
if not logo_path: if not logo_path:
return None return None
# Try different possible paths # Try different possible paths
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:
continue continue
return None return None
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 {{
size: A4; size: A4;
@@ -291,7 +306,7 @@ class PDFGenerator:
page-break-before: always; page-break-before: always;
}} }}
""" """
def generate_pdf_response(self, html_content, filename, css_content=None): def generate_pdf_response(self, html_content, filename, css_content=None):
"""Generate PDF response from HTML content""" """Generate PDF response from HTML content"""
if not WEASYPRINT_AVAILABLE: if not WEASYPRINT_AVAILABLE:
@@ -320,27 +335,30 @@ 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:
# Create CSS string # Create CSS string
if css_content: if css_content:
css = CSS(string=css_content, font_config=self.font_config) css = CSS(string=css_content, font_config=self.font_config)
else: else:
css = None css = None
# 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:
# Fallback: return error message as HTML # Fallback: return error message as HTML
error_html = f""" error_html = f"""
@@ -368,15 +386,19 @@ 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="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
Args: Args:
data: QuerySet or list of model instances data: QuerySet or list of model instances
fields_config: dict with field names as keys and display names as values fields_config: dict with field names as keys and display names as values
@@ -385,34 +407,39 @@ 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