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

File diff suppressed because it is too large Load Diff

View File

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

View File

@@ -4,8 +4,10 @@ Provides functions to log user actions throughout the application
"""
import json
from django.utils import timezone
from django.contrib.auth import get_user_model
from django.utils import timezone
from stiftung.models import AuditLog
User = get_user_model()
@@ -13,18 +15,20 @@ User = get_user_model()
def get_client_ip(request):
"""Extract the client IP address from the request"""
x_forwarded_for = request.META.get('HTTP_X_FORWARDED_FOR')
x_forwarded_for = request.META.get("HTTP_X_FORWARDED_FOR")
if x_forwarded_for:
ip = x_forwarded_for.split(',')[0]
ip = x_forwarded_for.split(",")[0]
else:
ip = request.META.get('REMOTE_ADDR')
ip = request.META.get("REMOTE_ADDR")
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
Args:
request: Django request object
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)
"""
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
ip_address = get_client_ip(request)
user_agent = request.META.get('HTTP_USER_AGENT', '')
session_key = request.session.session_key if hasattr(request, 'session') else ''
user_agent = request.META.get("HTTP_USER_AGENT", "")
session_key = request.session.session_key if hasattr(request, "session") else ""
# Create audit log entry
audit_entry = AuditLog.objects.create(
user=user,
username=username,
action=action,
entity_type=entity_type,
entity_id=str(entity_id) if entity_id else '',
entity_id=str(entity_id) if entity_id else "",
entity_name=entity_name,
description=description,
changes=changes,
ip_address=ip_address,
user_agent=user_agent[:500], # Truncate to avoid very long user agents
session_key=session_key
session_key=session_key,
)
return audit_entry
@@ -64,14 +68,14 @@ def log_create(request, entity_type, entity_id, entity_name, description=None):
"""Log entity creation"""
if not description:
description = f"Neue {entity_type.replace('_', ' ').title()} '{entity_name}' wurde erstellt"
return log_action(
request=request,
action='create',
action="create",
entity_type=entity_type,
entity_id=entity_id,
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 []
fields_str = ", ".join(changed_fields)
description = f"{entity_type.replace('_', ' ').title()} '{entity_name}' wurde aktualisiert: {fields_str}"
return log_action(
request=request,
action='update',
action="update",
entity_type=entity_type,
entity_id=entity_id,
entity_name=entity_name,
description=description,
changes=changes
changes=changes,
)
def log_delete(request, entity_type, entity_id, entity_name, description=None):
"""Log entity deletion"""
if not description:
description = f"{entity_type.replace('_', ' ').title()} '{entity_name}' wurde gelöscht"
description = (
f"{entity_type.replace('_', ' ').title()} '{entity_name}' wurde gelöscht"
)
return log_action(
request=request,
action='delete',
action="delete",
entity_type=entity_type,
entity_id=entity_id,
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"""
if not description:
description = f"{entity_type.replace('_', ' ').title()} '{entity_name}' wurde mit {target_type.replace('_', ' ')} '{target_name}' verknüpft"
return log_action(
request=request,
action='link',
action="link",
entity_type=entity_type,
entity_id=entity_id,
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"""
if not description:
description = f"Verknüpfung zwischen {entity_type.replace('_', ' ').title()} '{entity_name}' und {target_type.replace('_', ' ')} '{target_name}' wurde entfernt"
return log_action(
request=request,
action='unlink',
action="unlink",
entity_type=entity_type,
entity_id=entity_id,
entity_name=entity_name,
description=description
description=description,
)
@@ -143,51 +165,54 @@ def log_system_action(request, action, description, details=None):
return log_action(
request=request,
action=action,
entity_type='system',
entity_id='',
entity_name='System',
entity_type="system",
entity_id="",
entity_name="System",
description=description,
changes=details
changes=details,
)
def track_model_changes(old_instance, new_instance, exclude_fields=None):
"""
Track changes between model instances
Args:
old_instance: Original model instance
new_instance: Updated model instance
new_instance: Updated model instance
exclude_fields: List of fields to exclude from tracking
Returns:
Dictionary of changes in format {field: {'old': old_value, 'new': new_value}}
"""
if exclude_fields is None:
exclude_fields = ['id', 'erstellt_am', 'aktualisiert_am', 'created_at', 'updated_at']
exclude_fields = [
"id",
"erstellt_am",
"aktualisiert_am",
"created_at",
"updated_at",
]
changes = {}
if old_instance and new_instance:
for field in new_instance._meta.fields:
field_name = field.name
if field_name in exclude_fields:
continue
old_value = getattr(old_instance, field_name, None)
new_value = getattr(new_instance, field_name, None)
# Convert to string for comparison
old_str = str(old_value) if old_value is not None else None
new_str = str(new_value) if new_value is not None else None
if old_str != new_str:
changes[field_name] = {
'old': old_str,
'new': new_str
}
changes[field_name] = {"old": old_str, "new": new_str}
return changes
@@ -195,38 +220,39 @@ class AuditLogMixin:
"""
Mixin for views that provides audit logging functionality
"""
audit_entity_type = None
audit_entity_name_field = 'name'
audit_entity_name_field = "name"
def get_audit_entity_type(self):
"""Get the entity type for audit logging"""
if self.audit_entity_type:
return self.audit_entity_type
# Try to derive from model name
if hasattr(self, 'model') and self.model:
if hasattr(self, "model") and self.model:
return self.model.__name__.lower()
return 'unknown'
return "unknown"
def get_audit_entity_name(self, instance):
"""Get the entity name for audit logging"""
if hasattr(instance, self.audit_entity_name_field):
return str(getattr(instance, self.audit_entity_name_field))
elif hasattr(instance, '__str__'):
elif hasattr(instance, "__str__"):
return str(instance)
else:
return f"{self.get_audit_entity_type()} #{instance.pk}"
def log_create_action(self, instance):
"""Log creation of an instance"""
log_create(
request=self.request,
entity_type=self.get_audit_entity_type(),
entity_id=instance.pk,
entity_name=self.get_audit_entity_name(instance)
entity_name=self.get_audit_entity_name(instance),
)
def log_update_action(self, old_instance, new_instance):
"""Log update of an instance"""
changes = track_model_changes(old_instance, new_instance)
@@ -236,16 +262,16 @@ class AuditLogMixin:
entity_type=self.get_audit_entity_type(),
entity_id=new_instance.pk,
entity_name=self.get_audit_entity_name(new_instance),
changes=changes
changes=changes,
)
def log_delete_action(self, instance):
"""Log deletion of an instance"""
log_delete(
request=self.request,
entity_type=self.get_audit_entity_type(),
entity_id=instance.pk,
entity_name=self.get_audit_entity_name(instance)
entity_name=self.get_audit_entity_name(instance),
)
@@ -255,11 +281,11 @@ def log_login(request, user):
try:
return log_action(
request=request,
action='login',
entity_type='user',
action="login",
entity_type="user",
entity_id=user.pk,
entity_name=user.get_username(),
description=f"User '{user.get_username()}' logged in"
description=f"User '{user.get_username()}' logged in",
)
except Exception:
return None
@@ -268,14 +294,14 @@ def log_login(request, user):
def log_logout(request, user):
"""Log a successful user logout."""
try:
username = user.get_username() if user else 'Unknown'
username = user.get_username() if user else "Unknown"
return log_action(
request=request,
action='logout',
entity_type='user',
entity_id=getattr(user, 'pk', ''),
action="logout",
entity_type="user",
entity_id=getattr(user, "pk", ""),
entity_name=username,
description=f"User '{username}' logged out"
description=f"User '{username}' logged out",
)
except Exception:
return None

View File

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

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

View File

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

View File

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

View File

@@ -1,84 +1,93 @@
import logging
from django.core.management.base import BaseCommand
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__)
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):
parser.add_argument(
'--dry-run',
action='store_true',
help='Zeigt nur an, was gemacht würde, ohne Änderungen zu speichern',
"--dry-run",
action="store_true",
help="Zeigt nur an, was gemacht würde, ohne Änderungen zu speichern",
)
def handle(self, *args, **options):
dry_run = options['dry_run']
dry_run = options["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
aktive_verpachtungen = Verpachtung.objects.filter(status='aktiv')
self.stdout.write(f'Gefunden: {aktive_verpachtungen.count()} aktive Verpachtungen')
aktive_verpachtungen = Verpachtung.objects.filter(status="aktiv")
self.stdout.write(
f"Gefunden: {aktive_verpachtungen.count()} aktive Verpachtungen"
)
migrated_count = 0
skipped_count = 0
with transaction.atomic():
for verpachtung in aktive_verpachtungen:
land = verpachtung.land
# Prüfen ob bereits migriert
if land.aktueller_paechter is not None:
self.stdout.write(
self.style.WARNING(
f'Übersprungen: {land} hat bereits einen aktuellen Pächter'
f"Übersprungen: {land} hat bereits einen aktuellen Pächter"
)
)
skipped_count += 1
continue
# Migration durchführen
self.stdout.write(f'Migriere: {land} -> {verpachtung.paechter}')
self.stdout.write(f"Migriere: {land} -> {verpachtung.paechter}")
if not dry_run:
# Pächter-Daten ins Land übertragen
land.aktueller_paechter = verpachtung.paechter
land.paechter_name = verpachtung.paechter.get_full_name()
land.paechter_anschrift = self._get_paechter_anschrift(verpachtung.paechter)
land.paechter_anschrift = self._get_paechter_anschrift(
verpachtung.paechter
)
land.pachtbeginn = verpachtung.pachtbeginn
land.pachtende = verpachtung.pachtende
land.verlaengerung_klausel = bool(verpachtung.verlaengerung)
# Pachtzins übertragen
land.pachtzins_pauschal = verpachtung.pachtzins_jaehrlich
# Verpachtete Fläche aktualisieren (falls nicht gesetzt)
if land.verp_flaeche_aktuell == 0:
land.verp_flaeche_aktuell = verpachtung.verpachtete_flaeche
land.save()
migrated_count += 1
if dry_run:
self.stdout.write(
self.style.SUCCESS(
f'DRY RUN abgeschlossen: {migrated_count} Verpachtungen würden migriert, {skipped_count} übersprungen'
f"DRY RUN abgeschlossen: {migrated_count} Verpachtungen würden migriert, {skipped_count} übersprungen"
)
)
else:
self.stdout.write(
self.style.SUCCESS(
f'Migration abgeschlossen: {migrated_count} Verpachtungen migriert, {skipped_count} übersprungen'
f"Migration abgeschlossen: {migrated_count} Verpachtungen migriert, {skipped_count} übersprungen"
)
)
def _get_paechter_anschrift(self, paechter):
"""Erstellt eine Anschrift aus den Pächter-Daten"""
parts = []
@@ -88,5 +97,5 @@ class Command(BaseCommand):
parts.append(f"{paechter.plz} {paechter.ort}")
elif 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]
"""
from datetime import date
from decimal import Decimal
from django.core.management.base import BaseCommand, CommandError
from django.db import transaction
from decimal import Decimal
from datetime import date
from stiftung.models import Verpachtung, LandVerpachtung, LandAbrechnung
from stiftung.models import LandAbrechnung, LandVerpachtung, Verpachtung
class Command(BaseCommand):
help = 'Synchronize existing Verpachtungen with LandAbrechnungen'
help = "Synchronize existing Verpachtungen with LandAbrechnungen"
def add_arguments(self, parser):
parser.add_argument(
'--dry-run',
action='store_true',
help='Show what would be done without making changes',
"--dry-run",
action="store_true",
help="Show what would be done without making changes",
)
parser.add_argument(
'--year',
"--year",
type=int,
help='Only sync data for specific year',
help="Only sync data for specific year",
)
parser.add_argument(
'--force',
action='store_true',
help='Force update even if Abrechnungen already exist',
"--force",
action="store_true",
help="Force update even if Abrechnungen already exist",
)
def handle(self, *args, **options):
dry_run = options['dry_run']
target_year = options['year']
force = options['force']
dry_run = options["dry_run"]
target_year = options["year"]
force = options["force"]
self.stdout.write(
self.style.SUCCESS('🔄 Starting Abrechnung synchronization...')
self.style.SUCCESS("🔄 Starting Abrechnung synchronization...")
)
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
stats = {
'legacy_contracts': 0,
'new_contracts': 0,
'abrechnungen_created': 0,
'abrechnungen_updated': 0,
'total_rent_amount': Decimal('0.00'),
'years_processed': set(),
"legacy_contracts": 0,
"new_contracts": 0,
"abrechnungen_created": 0,
"abrechnungen_updated": 0,
"total_rent_amount": Decimal("0.00"),
"years_processed": set(),
}
try:
with transaction.atomic():
# Process Legacy Verpachtungen
self.stdout.write('\n📄 Processing Legacy Verpachtungen...')
self.stdout.write("\n📄 Processing Legacy Verpachtungen...")
legacy_verpachtungen = Verpachtung.objects.all()
for verpachtung in legacy_verpachtungen:
stats['legacy_contracts'] += 1
stats["legacy_contracts"] += 1
years_affected = self._get_affected_years(
verpachtung.pachtbeginn,
verpachtung.verlaengerung or verpachtung.pachtende,
target_year
target_year,
)
for year in years_affected:
stats['years_processed'].add(year)
rent_amount = self._calculate_legacy_rent_for_year(verpachtung, year)
stats["years_processed"].add(year)
rent_amount = self._calculate_legacy_rent_for_year(
verpachtung, year
)
if not dry_run:
created, updated = self._update_abrechnung(
verpachtung.land,
year,
rent_amount,
Decimal('0.00'), # No umlage for legacy
Decimal("0.00"), # No umlage for legacy
f"Legacy-Verpachtung {verpachtung.vertragsnummer}",
force
force,
)
if created:
stats['abrechnungen_created'] += 1
stats["abrechnungen_created"] += 1
if updated:
stats['abrechnungen_updated'] += 1
stats['total_rent_amount'] += rent_amount
stats["abrechnungen_updated"] += 1
stats["total_rent_amount"] += rent_amount
self.stdout.write(
f" 📊 {verpachtung.vertragsnummer} ({year}): {rent_amount:.2f}"
)
# Process New LandVerpachtungen
self.stdout.write('\n🆕 Processing New LandVerpachtungen...')
# Process New LandVerpachtungen
self.stdout.write("\n🆕 Processing New LandVerpachtungen...")
land_verpachtungen = LandVerpachtung.objects.all()
for verpachtung in land_verpachtungen:
stats['new_contracts'] += 1
stats["new_contracts"] += 1
years_affected = self._get_affected_years(
verpachtung.pachtbeginn,
verpachtung.pachtende,
target_year
verpachtung.pachtbeginn, verpachtung.pachtende, target_year
)
for year in years_affected:
stats['years_processed'].add(year)
rent_amount = self._calculate_new_rent_for_year(verpachtung, year)
umlage_amount = Decimal('0.00') # To be calculated later
stats["years_processed"].add(year)
rent_amount = self._calculate_new_rent_for_year(
verpachtung, year
)
umlage_amount = Decimal("0.00") # To be calculated later
if not dry_run:
created, updated = self._update_abrechnung(
verpachtung.land,
@@ -123,131 +129,143 @@ class Command(BaseCommand):
rent_amount,
umlage_amount,
f"LandVerpachtung {verpachtung.vertragsnummer}",
force
force,
)
if created:
stats['abrechnungen_created'] += 1
stats["abrechnungen_created"] += 1
if updated:
stats['abrechnungen_updated'] += 1
stats['total_rent_amount'] += rent_amount
stats["abrechnungen_updated"] += 1
stats["total_rent_amount"] += rent_amount
self.stdout.write(
f" 📊 {verpachtung.vertragsnummer} ({year}): {rent_amount:.2f}"
)
if dry_run:
# Rollback transaction in dry run
transaction.set_rollback(True)
except Exception as e:
self.stdout.write(
self.style.ERROR(f'❌ Error during synchronization: {str(e)}')
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
self.stdout.write('\n' + '='*50)
self.stdout.write(self.style.SUCCESS('📈 SYNCHRONIZATION SUMMARY'))
self.stdout.write('='*50)
self.stdout.write("\n" + "=" * 50)
self.stdout.write(self.style.SUCCESS("📈 SYNCHRONIZATION SUMMARY"))
self.stdout.write("=" * 50)
self.stdout.write(f"Legacy contracts processed: {stats['legacy_contracts']}")
self.stdout.write(f"New contracts processed: {stats['new_contracts']}")
self.stdout.write(f"Years affected: {', '.join(map(str, sorted(stats['years_processed'])))}")
self.stdout.write(
f"Years affected: {', '.join(map(str, sorted(stats['years_processed'])))}"
)
self.stdout.write(f"Abrechnungen created: {stats['abrechnungen_created']}")
self.stdout.write(f"Abrechnungen updated: {stats['abrechnungen_updated']}")
self.stdout.write(f"Total rent amount: {stats['total_rent_amount']:.2f}")
if dry_run:
self.stdout.write(self.style.WARNING('\n📋 This was a DRY RUN - no changes were saved'))
self.stdout.write(
self.style.WARNING("\n📋 This was a DRY RUN - no changes were saved")
)
else:
self.stdout.write(self.style.SUCCESS('\n✅ Synchronization completed successfully!'))
self.stdout.write(
self.style.SUCCESS("\n✅ Synchronization completed successfully!")
)
def _get_affected_years(self, start_date, end_date, target_year=None):
"""Get all years affected by a contract"""
if not start_date:
return []
years = []
start_year = start_date.year
end_year = end_date.year if end_date else date.today().year
if target_year:
if start_year <= target_year <= end_year:
return [target_year]
else:
return []
for year in range(start_year, end_year + 1):
years.append(year)
return years
def _calculate_legacy_rent_for_year(self, verpachtung, year):
"""Calculate rent for legacy Verpachtung for specific year"""
if not verpachtung.pachtzins_jaehrlich or not verpachtung.pachtbeginn:
return Decimal('0.00')
return Decimal("0.00")
year_start = date(year, 1, 1)
year_end = date(year, 12, 31)
contract_end_date = verpachtung.verlaengerung if verpachtung.verlaengerung else verpachtung.pachtende
contract_end_date = (
verpachtung.verlaengerung
if verpachtung.verlaengerung
else verpachtung.pachtende
)
contract_start = max(verpachtung.pachtbeginn, year_start)
contract_end = min(contract_end_date or year_end, year_end)
if contract_start > contract_end:
return Decimal('0.00')
return Decimal("0.00")
days_in_year = (year_end - year_start).days + 1
days_active = (contract_end - contract_start).days + 1
proportion = Decimal(str(days_active)) / Decimal(str(days_in_year))
return Decimal(str(verpachtung.pachtzins_jaehrlich)) * proportion
def _calculate_new_rent_for_year(self, verpachtung, year):
"""Calculate rent for new LandVerpachtung for specific year"""
if not verpachtung.pachtzins_pauschal or not verpachtung.pachtbeginn:
return Decimal('0.00')
return Decimal("0.00")
year_start = date(year, 1, 1)
year_end = date(year, 12, 31)
contract_start = max(verpachtung.pachtbeginn, year_start)
contract_end = min(verpachtung.pachtende or year_end, year_end)
if contract_start > contract_end:
return Decimal('0.00')
return Decimal("0.00")
days_in_year = (year_end - year_start).days + 1
days_active = (contract_end - contract_start).days + 1
proportion = Decimal(str(days_active)) / Decimal(str(days_in_year))
return Decimal(str(verpachtung.pachtzins_pauschal)) * proportion
def _update_abrechnung(self, land, year, rent_amount, umlage_amount, source_note, force):
def _update_abrechnung(
self, land, year, rent_amount, umlage_amount, source_note, force
):
"""Update or create Abrechnung for specific land and year"""
abrechnung, created = LandAbrechnung.objects.get_or_create(
land=land,
abrechnungsjahr=year,
defaults={
'pacht_vereinnahmt': rent_amount,
'umlagen_vereinnahmt': umlage_amount,
'bemerkungen': f'[{date.today().strftime("%d.%m.%Y")}] Automatisch synchronisiert von {source_note}'
}
"pacht_vereinnahmt": rent_amount,
"umlagen_vereinnahmt": umlage_amount,
"bemerkungen": f'[{date.today().strftime("%d.%m.%Y")}] Automatisch synchronisiert von {source_note}',
},
)
updated = False
if not created and force:
# Update existing
abrechnung.pacht_vereinnahmt += rent_amount
abrechnung.umlagen_vereinnahmt += umlage_amount
sync_note = f'[{date.today().strftime("%d.%m.%Y")}] Resync: +{rent_amount:.2f}€ von {source_note}'
if abrechnung.bemerkungen:
abrechnung.bemerkungen += f'\n{sync_note}'
abrechnung.bemerkungen += f"\n{sync_note}"
else:
abrechnung.bemerkungen = sync_note
abrechnung.save()
updated = True
return created, updated

View File

@@ -1,111 +1,127 @@
import logging
from datetime import datetime
from django.core.management.base import BaseCommand
from django.db import transaction
from stiftung.models import Land, Verpachtung, Paechter, LandAbrechnung
from datetime import datetime
import logging
from stiftung.models import Land, LandAbrechnung, Paechter, Verpachtung
logger = logging.getLogger(__name__)
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):
parser.add_argument(
'--dry-run',
action='store_true',
help='Zeigt nur an, was gemacht würde, ohne Änderungen zu speichern',
"--dry-run",
action="store_true",
help="Zeigt nur an, was gemacht würde, ohne Änderungen zu speichern",
)
parser.add_argument(
'--create-abrechnungen',
action='store_true',
help='Erstellt automatisch Abrechnungen aus Verpachtungsdaten',
"--create-abrechnungen",
action="store_true",
help="Erstellt automatisch Abrechnungen aus Verpachtungsdaten",
)
def handle(self, *args, **options):
dry_run = options['dry_run']
create_abrechnungen = options['create_abrechnungen']
dry_run = options["dry_run"]
create_abrechnungen = options["create_abrechnungen"]
if dry_run:
self.stdout.write(self.style.WARNING('DRY RUN - Keine Änderungen werden gespeichert!'))
self.stdout.write(
self.style.WARNING("DRY RUN - Keine Änderungen werden gespeichert!")
)
# Schritt 1: Alle Verpachtungen analysieren
alle_verpachtungen = Verpachtung.objects.all().order_by('land', '-pachtbeginn')
self.stdout.write(f'Gefunden: {alle_verpachtungen.count()} Verpachtungen insgesamt')
alle_verpachtungen = Verpachtung.objects.all().order_by("land", "-pachtbeginn")
self.stdout.write(
f"Gefunden: {alle_verpachtungen.count()} Verpachtungen insgesamt"
)
land_updates = 0
abrechnungen_created = 0
with transaction.atomic():
current_land = None
for verpachtung in alle_verpachtungen:
land = verpachtung.land
# Für jedes Land nur die neueste aktive Verpachtung als "aktuell" setzen
if current_land != land:
current_land = land
# Prüfen ob dies die neueste aktive Verpachtung ist
if verpachtung.status == 'aktiv' and not land.aktueller_paechter:
self.stdout.write(f'Setze aktuelle Verpachtung: {land} -> {verpachtung.paechter}')
if verpachtung.status == "aktiv" and not land.aktueller_paechter:
self.stdout.write(
f"Setze aktuelle Verpachtung: {land} -> {verpachtung.paechter}"
)
if not dry_run:
# Land-Felder aktualisieren
land.aktueller_paechter = verpachtung.paechter
land.paechter_name = verpachtung.paechter.get_full_name()
land.paechter_anschrift = self._get_paechter_anschrift(verpachtung.paechter)
land.paechter_anschrift = self._get_paechter_anschrift(
verpachtung.paechter
)
land.pachtbeginn = verpachtung.pachtbeginn
land.pachtende = verpachtung.pachtende
land.verlaengerung_klausel = bool(verpachtung.verlaengerung)
land.pachtzins_pauschal = verpachtung.pachtzins_jaehrlich
# Verpachtete Fläche synchronisieren
land.verp_flaeche_aktuell = verpachtung.verpachtete_flaeche
land.save()
land_updates += 1
# Schritt 2: Abrechnungen aus Verpachtungen erstellen (optional)
if create_abrechnungen and verpachtung.status == 'aktiv':
if create_abrechnungen and verpachtung.status == "aktiv":
# Erstelle Abrechnungen für die letzten 3 Jahre
current_year = datetime.now().year
for jahr in range(current_year - 2, current_year + 1):
# Prüfen ob Abrechnung bereits existiert
existing = LandAbrechnung.objects.filter(
land=land,
abrechnungsjahr=jahr
land=land, abrechnungsjahr=jahr
).first()
if not existing:
self.stdout.write(f'Erstelle Abrechnung: {land} - {jahr}')
self.stdout.write(f"Erstelle Abrechnung: {land} - {jahr}")
if not dry_run:
abrechnung = LandAbrechnung.objects.create(
land=land,
abrechnungsjahr=jahr,
pacht_vereinnahmt=verpachtung.pachtzins_jaehrlich,
bemerkungen=f'Automatisch erstellt aus Verpachtung {verpachtung.vertragsnummer}'
bemerkungen=f"Automatisch erstellt aus Verpachtung {verpachtung.vertragsnummer}",
)
abrechnungen_created += 1
# Zusammenfassung
self.stdout.write(self.style.SUCCESS('\n=== MIGRATION ABGESCHLOSSEN ==='))
self.stdout.write(self.style.SUCCESS("\n=== MIGRATION ABGESCHLOSSEN ==="))
if dry_run:
self.stdout.write(f'DRY RUN: {land_updates} Länder würden aktualisiert')
self.stdout.write(f"DRY RUN: {land_updates} Länder würden aktualisiert")
if create_abrechnungen:
self.stdout.write(f'DRY RUN: {abrechnungen_created} Abrechnungen würden erstellt')
self.stdout.write(
f"DRY RUN: {abrechnungen_created} Abrechnungen würden erstellt"
)
else:
self.stdout.write(f'{land_updates} Länder aktualisiert')
self.stdout.write(f"{land_updates} Länder aktualisiert")
if create_abrechnungen:
self.stdout.write(f'{abrechnungen_created} Abrechnungen erstellt')
self.stdout.write(f"{abrechnungen_created} Abrechnungen erstellt")
# Empfehlungen
self.stdout.write(self.style.WARNING('\n=== NÄCHSTE SCHRITTE ==='))
self.stdout.write('1. Prüfen Sie die migrierten Daten in der Weboberfläche')
self.stdout.write('2. Alte Verpachtungs-Views können als "Legacy" markiert werden')
self.stdout.write('3. Neue Verpachtungen sollten direkt im Land-Model erstellt werden')
self.stdout.write(self.style.WARNING("\n=== NÄCHSTE SCHRITTE ==="))
self.stdout.write("1. Prüfen Sie die migrierten Daten in der Weboberfläche")
self.stdout.write(
'2. Alte Verpachtungs-Views können als "Legacy" markiert werden'
)
self.stdout.write(
"3. Neue Verpachtungen sollten direkt im Land-Model erstellt werden"
)
def _get_paechter_anschrift(self, paechter):
"""Erstellt eine Anschrift aus den Pächter-Daten"""
parts = []
@@ -115,5 +131,5 @@ class Command(BaseCommand):
parts.append(f"{paechter.plz} {paechter.ort}")
elif 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
from django.utils.deprecation import MiddlewareMixin
from django.contrib.auth.signals import user_logged_in, user_logged_out
from django.db.models.signals import post_save, post_delete, pre_save
from django.db.models.signals import post_delete, post_save, pre_save
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
_local = threading.local()
@@ -18,54 +20,54 @@ class AuditMiddleware(MiddlewareMixin):
"""
Middleware that sets up request context for audit logging
"""
def process_request(self, request):
"""Store request in thread-local storage for access in signal handlers"""
_local.request = request
_local.user_changes = {} # Store pre-save state for change tracking
return None
def process_response(self, request, response):
"""Clean up thread-local storage"""
if hasattr(_local, 'request'):
delattr(_local, 'request')
if hasattr(_local, 'user_changes'):
delattr(_local, 'user_changes')
if hasattr(_local, "request"):
delattr(_local, "request")
if hasattr(_local, "user_changes"):
delattr(_local, "user_changes")
return response
def get_current_request():
"""Get the current request from thread-local storage"""
return getattr(_local, 'request', None)
return getattr(_local, "request", None)
def get_entity_type_from_model(model):
"""Map Django model to audit entity type"""
model_name = model.__name__.lower()
mapping = {
'destinataer': 'destinataer',
'land': 'land',
'paechter': 'paechter',
'verpachtung': 'verpachtung',
'foerderung': 'foerderung',
'rentmeister': 'rentmeister',
'stiftungskonto': 'stiftungskonto',
'verwaltungskosten': 'verwaltungskosten',
'banktransaction': 'banktransaction',
'dokumentlink': 'dokumentlink',
'user': 'user',
'person': 'destinataer', # Legacy model maps to destinataer
"destinataer": "destinataer",
"land": "land",
"paechter": "paechter",
"verpachtung": "verpachtung",
"foerderung": "foerderung",
"rentmeister": "rentmeister",
"stiftungskonto": "stiftungskonto",
"verwaltungskosten": "verwaltungskosten",
"banktransaction": "banktransaction",
"dokumentlink": "dokumentlink",
"user": "user",
"person": "destinataer", # Legacy model maps to destinataer
}
return mapping.get(model_name, 'unknown')
return mapping.get(model_name, "unknown")
def get_entity_name(instance):
"""Get a human-readable name for an entity"""
if hasattr(instance, 'get_full_name') and callable(instance.get_full_name):
if hasattr(instance, "get_full_name") and callable(instance.get_full_name):
return instance.get_full_name()
elif hasattr(instance, '__str__'):
elif hasattr(instance, "__str__"):
return str(instance)
else:
return f"{instance.__class__.__name__} #{instance.pk}"
@@ -76,22 +78,22 @@ def get_entity_name(instance):
def store_pre_save_state(sender, instance, **kwargs):
"""Store the pre-save state for change tracking"""
request = get_current_request()
if not request or not hasattr(request, 'user'):
if not request or not hasattr(request, "user"):
return
# Skip if user is not authenticated
if not request.user.is_authenticated:
return
# Skip audit log entries themselves to avoid infinite loops
if sender.__name__ == 'AuditLog':
if sender.__name__ == "AuditLog":
return
# Store the current state if this is an update
if instance.pk:
try:
old_instance = sender.objects.get(pk=instance.pk)
if not hasattr(_local, 'user_changes'):
if not hasattr(_local, "user_changes"):
_local.user_changes = {}
_local.user_changes[instance.pk] = old_instance
except sender.DoesNotExist:
@@ -102,53 +104,53 @@ def store_pre_save_state(sender, instance, **kwargs):
def log_model_save(sender, instance, created, **kwargs):
"""Log model creation and updates"""
request = get_current_request()
if not request or not hasattr(request, 'user'):
if not request or not hasattr(request, "user"):
return
# Skip if user is not authenticated
if not request.user.is_authenticated:
return
# Skip audit log entries themselves to avoid infinite loops
if sender.__name__ == 'AuditLog':
if sender.__name__ == "AuditLog":
return
# Skip certain system models
if sender.__name__ in ['Session', 'LogEntry', 'ContentType', 'Permission']:
if sender.__name__ in ["Session", "LogEntry", "ContentType", "Permission"]:
return
entity_type = get_entity_type_from_model(sender)
entity_name = get_entity_name(instance)
entity_id = str(instance.pk)
if created:
# Log creation
description = f"Neue {entity_type.replace('_', ' ').title()} '{entity_name}' wurde erstellt"
log_action(
request=request,
action='create',
action="create",
entity_type=entity_type,
entity_id=entity_id,
entity_name=entity_name,
description=description
description=description,
)
else:
# Log update with 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]
changes = track_model_changes(old_instance, instance)
if changes: # Only log if there are actual changes
description = f"{entity_type.replace('_', ' ').title()} '{entity_name}' wurde aktualisiert"
log_action(
request=request,
action='update',
action="update",
entity_type=entity_type,
entity_id=entity_id,
entity_name=entity_name,
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):
"""Log model deletion"""
request = get_current_request()
if not request or not hasattr(request, 'user'):
if not request or not hasattr(request, "user"):
return
# Skip if user is not authenticated
if not request.user.is_authenticated:
return
# Skip audit log entries themselves
if sender.__name__ == 'AuditLog':
if sender.__name__ == "AuditLog":
return
# Skip certain system models
if sender.__name__ in ['Session', 'LogEntry', 'ContentType', 'Permission']:
if sender.__name__ in ["Session", "LogEntry", "ContentType", "Permission"]:
return
entity_type = get_entity_type_from_model(sender)
entity_name = get_entity_name(instance)
entity_id = str(instance.pk)
description = f"{entity_type.replace('_', ' ').title()} '{entity_name}' wurde gelöscht"
description = (
f"{entity_type.replace('_', ' ').title()} '{entity_name}' wurde gelöscht"
)
log_action(
request=request,
action='delete',
action="delete",
entity_type=entity_type,
entity_id=entity_id,
entity_name=entity_name,
description=description
description=description,
)
@@ -192,11 +196,11 @@ def log_user_login(sender, request, user, **kwargs):
"""Log user login"""
log_action(
request=request,
action='login',
entity_type='user',
action="login",
entity_type="user",
entity_id=str(user.pk),
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
log_action(
request=request,
action='logout',
entity_type='user',
action="logout",
entity_type="user",
entity_id=str(user.pk),
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
import django.db.models.deletion
import uuid
import django.db.models.deletion
from django.db import migrations, models
@@ -9,39 +10,76 @@ class Migration(migrations.Migration):
initial = True
dependencies = [
]
dependencies = []
operations = [
migrations.CreateModel(
name='DokumentLink',
name="DokumentLink",
fields=[
('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)),
('paperless_document_id', models.IntegerField()),
('kontext', models.CharField(max_length=30)),
('titel', models.CharField(max_length=255)),
(
"id",
models.UUIDField(
default=uuid.uuid4,
editable=False,
primary_key=True,
serialize=False,
),
),
("paperless_document_id", models.IntegerField()),
("kontext", models.CharField(max_length=30)),
("titel", models.CharField(max_length=255)),
],
),
migrations.CreateModel(
name='Person',
name="Person",
fields=[
('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)),
('familienzweig', models.CharField(max_length=100)),
('vorname', models.CharField(max_length=100)),
('nachname', models.CharField(max_length=100)),
('geburtsdatum', models.DateField(blank=True, null=True)),
('email', models.EmailField(blank=True, max_length=254, null=True)),
('iban', models.CharField(blank=True, max_length=34, null=True)),
(
"id",
models.UUIDField(
default=uuid.uuid4,
editable=False,
primary_key=True,
serialize=False,
),
),
("familienzweig", models.CharField(max_length=100)),
("vorname", models.CharField(max_length=100)),
("nachname", models.CharField(max_length=100)),
("geburtsdatum", models.DateField(blank=True, null=True)),
("email", models.EmailField(blank=True, max_length=254, null=True)),
("iban", models.CharField(blank=True, max_length=34, null=True)),
],
),
migrations.CreateModel(
name='Foerderung',
name="Foerderung",
fields=[
('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)),
('jahr', models.IntegerField()),
('betrag', models.DecimalField(decimal_places=2, max_digits=12)),
('verwendungsnachweis', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='stiftung.dokumentlink')),
('person', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='stiftung.person')),
(
"id",
models.UUIDField(
default=uuid.uuid4,
editable=False,
primary_key=True,
serialize=False,
),
),
("jahr", models.IntegerField()),
("betrag", models.DecimalField(decimal_places=2, max_digits=12)),
(
"verwendungsnachweis",
models.ForeignKey(
blank=True,
null=True,
on_delete=django.db.models.deletion.SET_NULL,
to="stiftung.dokumentlink",
),
),
(
"person",
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
to="stiftung.person",
),
),
],
),
]

View File

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

View File

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

View File

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

View File

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

View File

@@ -6,51 +6,71 @@ from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('stiftung', '0006_remove_paechter_familienzweig_and_more'),
("stiftung", "0006_remove_paechter_familienzweig_and_more"),
]
operations = [
migrations.RemoveField(
model_name='destinataer',
name='adresse',
model_name="destinataer",
name="adresse",
),
migrations.RemoveField(
model_name='paechter',
name='adresse',
model_name="paechter",
name="adresse",
),
migrations.AddField(
model_name='destinataer',
name='ort',
field=models.CharField(blank=True, max_length=100, null=True, verbose_name='Ort'),
model_name="destinataer",
name="ort",
field=models.CharField(
blank=True, max_length=100, null=True, verbose_name="Ort"
),
),
migrations.AddField(
model_name='destinataer',
name='plz',
field=models.CharField(blank=True, max_length=10, null=True, verbose_name='PLZ'),
model_name="destinataer",
name="plz",
field=models.CharField(
blank=True, max_length=10, null=True, verbose_name="PLZ"
),
),
migrations.AddField(
model_name='destinataer',
name='strasse',
field=models.CharField(blank=True, max_length=200, null=True, verbose_name='Straße'),
model_name="destinataer",
name="strasse",
field=models.CharField(
blank=True, max_length=200, null=True, verbose_name="Straße"
),
),
migrations.AddField(
model_name='paechter',
name='ort',
field=models.CharField(blank=True, max_length=100, null=True, verbose_name='Ort'),
model_name="paechter",
name="ort",
field=models.CharField(
blank=True, max_length=100, null=True, verbose_name="Ort"
),
),
migrations.AddField(
model_name='paechter',
name='personentyp',
field=models.CharField(choices=[('natuerlich', 'Natürliche Person'), ('gesellschaft', 'Gesellschaft (GmbH, KG, etc.)')], default='natuerlich', max_length=20, verbose_name='Typ des Pächters'),
model_name="paechter",
name="personentyp",
field=models.CharField(
choices=[
("natuerlich", "Natürliche Person"),
("gesellschaft", "Gesellschaft (GmbH, KG, etc.)"),
],
default="natuerlich",
max_length=20,
verbose_name="Typ des Pächters",
),
),
migrations.AddField(
model_name='paechter',
name='plz',
field=models.CharField(blank=True, max_length=10, null=True, verbose_name='PLZ'),
model_name="paechter",
name="plz",
field=models.CharField(
blank=True, max_length=10, null=True, verbose_name="PLZ"
),
),
migrations.AddField(
model_name='paechter',
name='strasse',
field=models.CharField(blank=True, max_length=200, null=True, verbose_name='Straße'),
model_name="paechter",
name="strasse",
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):
dependencies = [
('stiftung', '0007_remove_destinataer_adresse_remove_paechter_adresse_and_more'),
(
"stiftung",
"0007_remove_destinataer_adresse_remove_paechter_adresse_and_more",
),
]
operations = [
migrations.AddField(
model_name='dokumentlink',
name='destinataer_id',
field=models.UUIDField(blank=True, null=True, verbose_name='Destinatär ID'),
model_name="dokumentlink",
name="destinataer_id",
field=models.UUIDField(blank=True, null=True, verbose_name="Destinatär ID"),
),
migrations.AddField(
model_name='dokumentlink',
name='foerderung_id',
field=models.UUIDField(blank=True, null=True, verbose_name='Förderung ID'),
model_name="dokumentlink",
name="foerderung_id",
field=models.UUIDField(blank=True, null=True, verbose_name="Förderung ID"),
),
migrations.AddField(
model_name='dokumentlink',
name='land_id',
field=models.UUIDField(blank=True, null=True, verbose_name='Länderei ID'),
model_name="dokumentlink",
name="land_id",
field=models.UUIDField(blank=True, null=True, verbose_name="Länderei ID"),
),
migrations.AddField(
model_name='dokumentlink',
name='paechter_id',
field=models.UUIDField(blank=True, null=True, verbose_name='Pächter ID'),
model_name="dokumentlink",
name="paechter_id",
field=models.UUIDField(blank=True, null=True, verbose_name="Pächter ID"),
),
migrations.AddField(
model_name='dokumentlink',
name='verpachtung_id',
field=models.UUIDField(blank=True, null=True, verbose_name='Verpachtung ID'),
model_name="dokumentlink",
name="verpachtung_id",
field=models.UUIDField(
blank=True, null=True, verbose_name="Verpachtung ID"
),
),
migrations.AlterField(
model_name='dokumentlink',
name='kontext',
field=models.CharField(choices=[('pachtvertrag', 'Pachtvertrag'), ('antrag', 'Antrag'), ('verwendungsnachweis', 'Verwendungsnachweis'), ('rechnung', 'Rechnung'), ('vertrag', 'Vertrag'), ('bericht', 'Bericht'), ('landkarte', 'Landkarte'), ('kataster', 'Kataster'), ('anderes', 'Anderes')], default='anderes', max_length=30),
model_name="dokumentlink",
name="kontext",
field=models.CharField(
choices=[
("pachtvertrag", "Pachtvertrag"),
("antrag", "Antrag"),
("verwendungsnachweis", "Verwendungsnachweis"),
("rechnung", "Rechnung"),
("vertrag", "Vertrag"),
("bericht", "Bericht"),
("landkarte", "Landkarte"),
("kataster", "Kataster"),
("anderes", "Anderes"),
],
default="anderes",
max_length=30,
),
),
]

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -7,48 +7,74 @@ from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('stiftung', '0016_applicationpermission'),
("stiftung", "0016_applicationpermission"),
]
operations = [
migrations.AddField(
model_name='destinataer',
name='haushaltsgroesse',
field=models.PositiveIntegerField(default=1, verbose_name='Haushaltsgröße'),
model_name="destinataer",
name="haushaltsgroesse",
field=models.PositiveIntegerField(default=1, verbose_name="Haushaltsgröße"),
),
migrations.AddField(
model_name='destinataer',
name='ist_abkoemmling',
field=models.BooleanField(default=False, verbose_name='Abkömmling gem. Satzung'),
model_name="destinataer",
name="ist_abkoemmling",
field=models.BooleanField(
default=False, verbose_name="Abkömmling gem. Satzung"
),
),
migrations.AddField(
model_name='destinataer',
name='letzter_studiennachweis',
field=models.DateField(blank=True, null=True, verbose_name='Letzter Studiennachweis'),
model_name="destinataer",
name="letzter_studiennachweis",
field=models.DateField(
blank=True, null=True, verbose_name="Letzter Studiennachweis"
),
),
migrations.AddField(
model_name='destinataer',
name='monatliche_bezuege',
field=models.DecimalField(blank=True, decimal_places=2, max_digits=10, null=True, verbose_name='Monatliche Bezüge (€)'),
model_name="destinataer",
name="monatliche_bezuege",
field=models.DecimalField(
blank=True,
decimal_places=2,
max_digits=10,
null=True,
verbose_name="Monatliche Bezüge (€)",
),
),
migrations.AddField(
model_name='destinataer',
name='standard_konto',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='stiftung.stiftungskonto', verbose_name='Standard Auszahlungskonto'),
model_name="destinataer",
name="standard_konto",
field=models.ForeignKey(
blank=True,
null=True,
on_delete=django.db.models.deletion.SET_NULL,
to="stiftung.stiftungskonto",
verbose_name="Standard Auszahlungskonto",
),
),
migrations.AddField(
model_name='destinataer',
name='studiennachweis_erforderlich',
field=models.BooleanField(default=False, verbose_name='Studiennachweis erforderlich'),
model_name="destinataer",
name="studiennachweis_erforderlich",
field=models.BooleanField(
default=False, verbose_name="Studiennachweis erforderlich"
),
),
migrations.AddField(
model_name='destinataer',
name='unterstuetzung_bestaetigt',
field=models.BooleanField(default=False, verbose_name='Unterstützung bestätigt'),
model_name="destinataer",
name="unterstuetzung_bestaetigt",
field=models.BooleanField(
default=False, verbose_name="Unterstützung bestätigt"
),
),
migrations.AddField(
model_name='destinataer',
name='vermoegen',
field=models.DecimalField(blank=True, decimal_places=2, max_digits=12, null=True, verbose_name='Vermögen (€)'),
model_name="destinataer",
name="vermoegen",
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):
dependencies = [
('stiftung', '0017_destinataer_haushaltsgroesse_and_more'),
("stiftung", "0017_destinataer_haushaltsgroesse_and_more"),
]
operations = [
migrations.AddField(
model_name='destinataer',
name='vierteljaehrlicher_betrag',
field=models.DecimalField(blank=True, decimal_places=2, max_digits=12, null=True, verbose_name='Vierteljährlicher Betrag (€)'),
model_name="destinataer",
name="vierteljaehrlicher_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
import django.db.models.deletion
import uuid
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('stiftung', '0018_destinataer_vierteljaehrlicher_betrag'),
("stiftung", "0018_destinataer_vierteljaehrlicher_betrag"),
]
operations = [
migrations.CreateModel(
name='DestinataerUnterstuetzung',
name="DestinataerUnterstuetzung",
fields=[
('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)),
('betrag', models.DecimalField(decimal_places=2, max_digits=12, verbose_name='Betrag (€)')),
('faellig_am', models.DateField(verbose_name='Fällig am')),
('status', models.CharField(choices=[('geplant', 'Geplant'), ('in_bearbeitung', 'In Bearbeitung'), ('ausgezahlt', 'Ausgezahlt'), ('storniert', 'Storniert')], default='geplant', max_length=20, verbose_name='Status')),
('beschreibung', models.CharField(blank=True, max_length=255, verbose_name='Beschreibung')),
('erstellt_am', models.DateTimeField(auto_now_add=True)),
('aktualisiert_am', models.DateTimeField(auto_now=True)),
('destinataer', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='unterstuetzungen', to='stiftung.destinataer', verbose_name='Destinatär')),
('konto', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to='stiftung.stiftungskonto', verbose_name='Zahlungskonto')),
(
"id",
models.UUIDField(
default=uuid.uuid4,
editable=False,
primary_key=True,
serialize=False,
),
),
(
"betrag",
models.DecimalField(
decimal_places=2, max_digits=12, verbose_name="Betrag (€)"
),
),
("faellig_am", models.DateField(verbose_name="Fällig am")),
(
"status",
models.CharField(
choices=[
("geplant", "Geplant"),
("in_bearbeitung", "In Bearbeitung"),
("ausgezahlt", "Ausgezahlt"),
("storniert", "Storniert"),
],
default="geplant",
max_length=20,
verbose_name="Status",
),
),
(
"beschreibung",
models.CharField(
blank=True, max_length=255, verbose_name="Beschreibung"
),
),
("erstellt_am", models.DateTimeField(auto_now_add=True)),
("aktualisiert_am", models.DateTimeField(auto_now=True)),
(
"destinataer",
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
related_name="unterstuetzungen",
to="stiftung.destinataer",
verbose_name="Destinatär",
),
),
(
"konto",
models.ForeignKey(
on_delete=django.db.models.deletion.PROTECT,
to="stiftung.stiftungskonto",
verbose_name="Zahlungskonto",
),
),
],
options={
'verbose_name': 'Destinatärunterstützung',
'verbose_name_plural': 'Destinatärunterstützungen',
'ordering': ['-faellig_am', '-erstellt_am'],
'indexes': [models.Index(fields=['status', 'faellig_am'], name='stiftung_de_status_1e9799_idx'), models.Index(fields=['destinataer', 'status'], name='stiftung_de_destina_ba7286_idx')],
"verbose_name": "Destinatärunterstützung",
"verbose_name_plural": "Destinatärunterstützungen",
"ordering": ["-faellig_am", "-erstellt_am"],
"indexes": [
models.Index(
fields=["status", "faellig_am"],
name="stiftung_de_status_1e9799_idx",
),
models.Index(
fields=["destinataer", "status"],
name="stiftung_de_destina_ba7286_idx",
),
],
},
),
]

View File

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

View File

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

View File

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

View File

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

View File

@@ -6,13 +6,13 @@ from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('stiftung', '0023_remove_legacy_verpachtung'),
("stiftung", "0023_remove_legacy_verpachtung"),
]
operations = [
migrations.AddField(
model_name='dokumentlink',
name='abrechnung_id',
field=models.UUIDField(blank=True, null=True, verbose_name='Abrechnung ID'),
model_name="dokumentlink",
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
import uuid
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('stiftung', '0024_dokumentlink_abrechnung_id'),
("stiftung", "0024_dokumentlink_abrechnung_id"),
]
operations = [
migrations.CreateModel(
name='AppConfiguration',
name="AppConfiguration",
fields=[
('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)),
('key', models.CharField(max_length=100, unique=True, verbose_name='Setting Key')),
('display_name', models.CharField(max_length=200, verbose_name='Display Name')),
('description', models.TextField(blank=True, null=True, verbose_name='Description')),
('value', models.TextField(verbose_name='Value')),
('default_value', models.TextField(verbose_name='Default Value')),
('setting_type', models.CharField(choices=[('text', 'Text'), ('number', 'Number'), ('boolean', 'Boolean'), ('url', 'URL'), ('tag', 'Tag Name'), ('tag_id', 'Tag ID')], default='text', max_length=20, verbose_name='Type')),
('category', models.CharField(choices=[('paperless', 'Paperless Integration'), ('general', 'General Settings'), ('notifications', 'Notifications'), ('system', 'System Settings')], default='general', max_length=50, verbose_name='Category')),
('is_active', models.BooleanField(default=True, verbose_name='Active')),
('is_system', models.BooleanField(default=False, verbose_name='System Setting (read-only)')),
('order', models.IntegerField(default=0, verbose_name='Display Order')),
('created_at', models.DateTimeField(auto_now_add=True)),
('updated_at', models.DateTimeField(auto_now=True)),
(
"id",
models.UUIDField(
default=uuid.uuid4,
editable=False,
primary_key=True,
serialize=False,
),
),
(
"key",
models.CharField(
max_length=100, unique=True, verbose_name="Setting Key"
),
),
(
"display_name",
models.CharField(max_length=200, verbose_name="Display Name"),
),
(
"description",
models.TextField(blank=True, null=True, verbose_name="Description"),
),
("value", models.TextField(verbose_name="Value")),
("default_value", models.TextField(verbose_name="Default Value")),
(
"setting_type",
models.CharField(
choices=[
("text", "Text"),
("number", "Number"),
("boolean", "Boolean"),
("url", "URL"),
("tag", "Tag Name"),
("tag_id", "Tag ID"),
],
default="text",
max_length=20,
verbose_name="Type",
),
),
(
"category",
models.CharField(
choices=[
("paperless", "Paperless Integration"),
("general", "General Settings"),
("notifications", "Notifications"),
("system", "System Settings"),
],
default="general",
max_length=50,
verbose_name="Category",
),
),
("is_active", models.BooleanField(default=True, verbose_name="Active")),
(
"is_system",
models.BooleanField(
default=False, verbose_name="System Setting (read-only)"
),
),
("order", models.IntegerField(default=0, verbose_name="Display Order")),
("created_at", models.DateTimeField(auto_now_add=True)),
("updated_at", models.DateTimeField(auto_now=True)),
],
options={
'verbose_name': 'App Configuration',
'verbose_name_plural': 'App Configurations',
'ordering': ['category', 'order', 'display_name'],
"verbose_name": "App Configuration",
"verbose_name_plural": "App Configurations",
"ordering": ["category", "order", "display_name"],
},
),
]

View File

@@ -1,7 +1,8 @@
# Generated by Django 5.0.6 on 2025-09-01 20:03
import django.db.models.deletion
import uuid
import django.db.models.deletion
from django.conf import settings
from django.db import migrations, models
@@ -9,81 +10,192 @@ from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('stiftung', '0025_appconfiguration'),
("stiftung", "0025_appconfiguration"),
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]
operations = [
migrations.AddField(
model_name='destinataerunterstuetzung',
name='ausgezahlt_am',
field=models.DateField(blank=True, null=True, verbose_name='Ausgezahlt am'),
model_name="destinataerunterstuetzung",
name="ausgezahlt_am",
field=models.DateField(blank=True, null=True, verbose_name="Ausgezahlt am"),
),
migrations.AddField(
model_name='destinataerunterstuetzung',
name='ausgezahlt_von',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to=settings.AUTH_USER_MODEL, verbose_name='Ausgezahlt von'),
model_name="destinataerunterstuetzung",
name="ausgezahlt_von",
field=models.ForeignKey(
blank=True,
null=True,
on_delete=django.db.models.deletion.SET_NULL,
to=settings.AUTH_USER_MODEL,
verbose_name="Ausgezahlt von",
),
),
migrations.AddField(
model_name='destinataerunterstuetzung',
name='empfaenger_iban',
field=models.CharField(blank=True, max_length=34, verbose_name='Empfänger IBAN'),
model_name="destinataerunterstuetzung",
name="empfaenger_iban",
field=models.CharField(
blank=True, max_length=34, verbose_name="Empfänger IBAN"
),
),
migrations.AddField(
model_name='destinataerunterstuetzung',
name='empfaenger_name',
field=models.CharField(blank=True, max_length=200, verbose_name='Empfänger Name'),
model_name="destinataerunterstuetzung",
name="empfaenger_name",
field=models.CharField(
blank=True, max_length=200, verbose_name="Empfänger Name"
),
),
migrations.AddField(
model_name='destinataerunterstuetzung',
name='verwendungszweck',
field=models.CharField(blank=True, max_length=140, verbose_name='Verwendungszweck'),
model_name="destinataerunterstuetzung",
name="verwendungszweck",
field=models.CharField(
blank=True, max_length=140, verbose_name="Verwendungszweck"
),
),
migrations.AlterField(
model_name='destinataerunterstuetzung',
name='status',
field=models.CharField(choices=[('geplant', 'Geplant'), ('faellig', 'Fällig'), ('in_bearbeitung', 'In Bearbeitung'), ('ausgezahlt', 'Ausgezahlt'), ('storniert', 'Storniert')], default='geplant', max_length=20, verbose_name='Status'),
model_name="destinataerunterstuetzung",
name="status",
field=models.CharField(
choices=[
("geplant", "Geplant"),
("faellig", "Fällig"),
("in_bearbeitung", "In Bearbeitung"),
("ausgezahlt", "Ausgezahlt"),
("storniert", "Storniert"),
],
default="geplant",
max_length=20,
verbose_name="Status",
),
),
migrations.CreateModel(
name='UnterstuetzungWiederkehrend',
name="UnterstuetzungWiederkehrend",
fields=[
('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)),
('betrag', models.DecimalField(decimal_places=2, max_digits=12, verbose_name='Betrag (€)')),
('intervall', models.CharField(choices=[('monatlich', 'Monatlich'), ('quartalsweise', 'Vierteljährlich'), ('halbjaehrlich', 'Halbjährlich'), ('jaehrlich', 'Jährlich')], max_length=20, verbose_name='Intervall')),
('beschreibung', models.CharField(blank=True, max_length=255, verbose_name='Beschreibung')),
('empfaenger_iban', models.CharField(max_length=34, verbose_name='Empfänger IBAN')),
('empfaenger_name', models.CharField(max_length=200, verbose_name='Empfänger Name')),
('verwendungszweck', models.CharField(blank=True, max_length=140, verbose_name='Verwendungszweck')),
('erste_zahlung_am', models.DateField(verbose_name='Erste Zahlung am')),
('letzte_zahlung_am', models.DateField(blank=True, null=True, verbose_name='Letzte Zahlung am (optional)')),
('naechste_generierung', models.DateField(verbose_name='Nächste Generierung')),
('aktiv', models.BooleanField(default=True, verbose_name='Aktiv')),
('erstellt_am', models.DateTimeField(auto_now_add=True)),
('destinataer', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='wiederkehrende_unterstuetzungen', to='stiftung.destinataer', verbose_name='Destinatär')),
('erstellt_von', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to=settings.AUTH_USER_MODEL, verbose_name='Erstellt von')),
('konto', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to='stiftung.stiftungskonto', verbose_name='Zahlungskonto')),
(
"id",
models.UUIDField(
default=uuid.uuid4,
editable=False,
primary_key=True,
serialize=False,
),
),
(
"betrag",
models.DecimalField(
decimal_places=2, max_digits=12, verbose_name="Betrag (€)"
),
),
(
"intervall",
models.CharField(
choices=[
("monatlich", "Monatlich"),
("quartalsweise", "Vierteljährlich"),
("halbjaehrlich", "Halbjährlich"),
("jaehrlich", "Jährlich"),
],
max_length=20,
verbose_name="Intervall",
),
),
(
"beschreibung",
models.CharField(
blank=True, max_length=255, verbose_name="Beschreibung"
),
),
(
"empfaenger_iban",
models.CharField(max_length=34, verbose_name="Empfänger IBAN"),
),
(
"empfaenger_name",
models.CharField(max_length=200, verbose_name="Empfänger Name"),
),
(
"verwendungszweck",
models.CharField(
blank=True, max_length=140, verbose_name="Verwendungszweck"
),
),
("erste_zahlung_am", models.DateField(verbose_name="Erste Zahlung am")),
(
"letzte_zahlung_am",
models.DateField(
blank=True,
null=True,
verbose_name="Letzte Zahlung am (optional)",
),
),
(
"naechste_generierung",
models.DateField(verbose_name="Nächste Generierung"),
),
("aktiv", models.BooleanField(default=True, verbose_name="Aktiv")),
("erstellt_am", models.DateTimeField(auto_now_add=True)),
(
"destinataer",
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
related_name="wiederkehrende_unterstuetzungen",
to="stiftung.destinataer",
verbose_name="Destinatär",
),
),
(
"erstellt_von",
models.ForeignKey(
blank=True,
null=True,
on_delete=django.db.models.deletion.SET_NULL,
to=settings.AUTH_USER_MODEL,
verbose_name="Erstellt von",
),
),
(
"konto",
models.ForeignKey(
on_delete=django.db.models.deletion.PROTECT,
to="stiftung.stiftungskonto",
verbose_name="Zahlungskonto",
),
),
],
options={
'verbose_name': 'Wiederkehrende Unterstützung',
'verbose_name_plural': 'Wiederkehrende Unterstützungen',
'ordering': ['-erstellt_am'],
"verbose_name": "Wiederkehrende Unterstützung",
"verbose_name_plural": "Wiederkehrende Unterstützungen",
"ordering": ["-erstellt_am"],
},
),
migrations.AddField(
model_name='destinataerunterstuetzung',
name='wiederkehrend_von',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='stiftung.unterstuetzungwiederkehrend', verbose_name='Wiederkehrende Zahlung'),
model_name="destinataerunterstuetzung",
name="wiederkehrend_von",
field=models.ForeignKey(
blank=True,
null=True,
on_delete=django.db.models.deletion.SET_NULL,
to="stiftung.unterstuetzungwiederkehrend",
verbose_name="Wiederkehrende Zahlung",
),
),
migrations.AddIndex(
model_name='destinataerunterstuetzung',
index=models.Index(fields=['wiederkehrend_von'], name='stiftung_de_wiederk_3d5afc_idx'),
model_name="destinataerunterstuetzung",
index=models.Index(
fields=["wiederkehrend_von"], name="stiftung_de_wiederk_3d5afc_idx"
),
),
migrations.AddIndex(
model_name='unterstuetzungwiederkehrend',
index=models.Index(fields=['aktiv', 'naechste_generierung'], name='stiftung_un_aktiv_b957e5_idx'),
model_name="unterstuetzungwiederkehrend",
index=models.Index(
fields=["aktiv", "naechste_generierung"],
name="stiftung_un_aktiv_b957e5_idx",
),
),
migrations.AddIndex(
model_name='unterstuetzungwiederkehrend',
index=models.Index(fields=['destinataer', 'aktiv'], name='stiftung_un_destina_2232fc_idx'),
model_name="unterstuetzungwiederkehrend",
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
import uuid
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('stiftung', '0026_enhance_unterstuetzung_model'),
("stiftung", "0026_enhance_unterstuetzung_model"),
]
operations = [
migrations.CreateModel(
name='HelpBox',
name="HelpBox",
fields=[
('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)),
('page_key', models.CharField(choices=[('destinataer_new', 'Neuer Destinatär'), ('unterstuetzung_new', 'Neue Unterstützung'), ('foerderung_new', 'Neue Förderung'), ('paechter_new', 'Neuer Pächter'), ('laenderei_new', 'Neue Länderei'), ('verpachtung_new', 'Neue Verpachtung'), ('person_new', 'Neue Person'), ('konto_new', 'Neues Konto')], max_length=50, unique=True, verbose_name='Seite')),
('title', models.CharField(max_length=200, verbose_name='Titel der Hilfsbox')),
('content', models.TextField(help_text='Sie können Markdown verwenden: **fett**, *kursiv*, `code`, [Link](url), etc.', verbose_name='Inhalt (Markdown unterstützt)')),
('is_active', models.BooleanField(default=True, verbose_name='Aktiv')),
('created_at', models.DateTimeField(auto_now_add=True, verbose_name='Erstellt am')),
('updated_at', models.DateTimeField(auto_now=True, verbose_name='Aktualisiert am')),
('created_by', models.CharField(blank=True, max_length=100, null=True, verbose_name='Erstellt von')),
('updated_by', models.CharField(blank=True, max_length=100, null=True, verbose_name='Aktualisiert von')),
(
"id",
models.UUIDField(
default=uuid.uuid4,
editable=False,
primary_key=True,
serialize=False,
),
),
(
"page_key",
models.CharField(
choices=[
("destinataer_new", "Neuer Destinatär"),
("unterstuetzung_new", "Neue Unterstützung"),
("foerderung_new", "Neue Förderung"),
("paechter_new", "Neuer Pächter"),
("laenderei_new", "Neue Länderei"),
("verpachtung_new", "Neue Verpachtung"),
("person_new", "Neue Person"),
("konto_new", "Neues Konto"),
],
max_length=50,
unique=True,
verbose_name="Seite",
),
),
(
"title",
models.CharField(max_length=200, verbose_name="Titel der Hilfsbox"),
),
(
"content",
models.TextField(
help_text="Sie können Markdown verwenden: **fett**, *kursiv*, `code`, [Link](url), etc.",
verbose_name="Inhalt (Markdown unterstützt)",
),
),
("is_active", models.BooleanField(default=True, verbose_name="Aktiv")),
(
"created_at",
models.DateTimeField(auto_now_add=True, verbose_name="Erstellt am"),
),
(
"updated_at",
models.DateTimeField(auto_now=True, verbose_name="Aktualisiert am"),
),
(
"created_by",
models.CharField(
blank=True,
max_length=100,
null=True,
verbose_name="Erstellt von",
),
),
(
"updated_by",
models.CharField(
blank=True,
max_length=100,
null=True,
verbose_name="Aktualisiert von",
),
),
],
options={
'verbose_name': 'Hilfs-Infobox',
'verbose_name_plural': 'Hilfs-Infoboxen',
'ordering': ['page_key'],
"verbose_name": "Hilfs-Infobox",
"verbose_name_plural": "Hilfs-Infoboxen",
"ordering": ["page_key"],
},
),
migrations.AlterField(
model_name='appconfiguration',
name='category',
field=models.CharField(choices=[('paperless', 'Paperless Integration'), ('general', 'General Settings'), ('corporate', 'Corporate Identity'), ('notifications', 'Notifications'), ('system', 'System Settings')], default='general', max_length=50, verbose_name='Category'),
model_name="appconfiguration",
name="category",
field=models.CharField(
choices=[
("paperless", "Paperless Integration"),
("general", "General Settings"),
("corporate", "Corporate Identity"),
("notifications", "Notifications"),
("system", "System Settings"),
],
default="general",
max_length=50,
verbose_name="Category",
),
),
]

View File

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

File diff suppressed because it is too large Load Diff

View File

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

View File

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

View File

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

View File

@@ -1,160 +1,337 @@
from django.urls import path
from . import views
app_name = 'stiftung'
app_name = "stiftung"
urlpatterns = [
# Dashboard (Startseite)
path('', views.dashboard, name='dashboard'),
path("", views.dashboard, name="dashboard"),
# Home (für Kompatibilität mit bestehenden Templates)
path('home/', views.home, name='home'),
path("home/", views.home, name="home"),
# CSV Import URLs
path('import/', views.csv_import_list, name='csv_import_list'),
path('import/neu/', views.csv_import_create, name='csv_import_create'),
path("import/", views.csv_import_list, name="csv_import_list"),
path("import/neu/", views.csv_import_create, name="csv_import_create"),
# Destinatär URLs (Förderungsempfänger)
path('destinataere/', views.destinataer_list, name='destinataer_list'),
path('destinataere/<uuid:pk>/', views.destinataer_detail, name='destinataer_detail'),
path('destinataere/neu/', views.destinataer_create, name='destinataer_create'),
path('destinataere/<uuid:pk>/bearbeiten/', views.destinataer_update, name='destinataer_update'),
path('destinataere/<uuid:pk>/loeschen/', views.destinataer_delete, name='destinataer_delete'),
path('destinataere/<uuid:pk>/notiz/', views.destinataer_notiz_create, name='destinataer_notiz_create'),
path('destinataere/<uuid:pk>/export/', views.destinataer_export, name='destinataer_export'),
path("destinataere/", views.destinataer_list, name="destinataer_list"),
path(
"destinataere/<uuid:pk>/", views.destinataer_detail, name="destinataer_detail"
),
path("destinataere/neu/", views.destinataer_create, name="destinataer_create"),
path(
"destinataere/<uuid:pk>/bearbeiten/",
views.destinataer_update,
name="destinataer_update",
),
path(
"destinataere/<uuid:pk>/loeschen/",
views.destinataer_delete,
name="destinataer_delete",
),
path(
"destinataere/<uuid:pk>/notiz/",
views.destinataer_notiz_create,
name="destinataer_notiz_create",
),
path(
"destinataere/<uuid:pk>/export/",
views.destinataer_export,
name="destinataer_export",
),
# Paechter URLs (Landpächter)
path('paechter/', views.paechter_list, name='paechter_list'),
path('paechter/<uuid:pk>/', views.paechter_detail, name='paechter_detail'),
path('paechter/neu/', views.paechter_create, name='paechter_create'),
path('paechter/<uuid:pk>/bearbeiten/', views.paechter_update, name='paechter_update'),
path('paechter/<uuid:pk>/loeschen/', views.paechter_delete, name='paechter_delete'),
path('paechter/<uuid:pk>/export/', views.paechter_export, name='paechter_export'),
path("paechter/", views.paechter_list, name="paechter_list"),
path("paechter/<uuid:pk>/", views.paechter_detail, name="paechter_detail"),
path("paechter/neu/", views.paechter_create, name="paechter_create"),
path(
"paechter/<uuid:pk>/bearbeiten/", views.paechter_update, name="paechter_update"
),
path("paechter/<uuid:pk>/loeschen/", views.paechter_delete, name="paechter_delete"),
path("paechter/<uuid:pk>/export/", views.paechter_export, name="paechter_export"),
# Legacy Person URLs removed (Destinatäre ersetzen Personen)
# Land URLs
path('laendereien/', views.land_list, name='land_list'),
path('laendereien/<uuid:pk>/', views.land_detail, name='land_detail'),
path('laendereien/neu/', views.land_create, name='land_create'),
path('laendereien/<uuid:pk>/bearbeiten/', views.land_update, name='land_update'),
path('laendereien/<uuid:pk>/loeschen/', views.land_delete, name='land_delete'),
path('laendereien/<uuid:pk>/export/', views.land_export, name='land_export'),
path("laendereien/", views.land_list, name="land_list"),
path("laendereien/<uuid:pk>/", views.land_detail, name="land_detail"),
path("laendereien/neu/", views.land_create, name="land_create"),
path("laendereien/<uuid:pk>/bearbeiten/", views.land_update, name="land_update"),
path("laendereien/<uuid:pk>/loeschen/", views.land_delete, name="land_delete"),
path("laendereien/<uuid:pk>/export/", views.land_export, name="land_export"),
# Landabrechnung URLs
path('landabrechnungen/', views.land_abrechnung_list, name='land_abrechnung_list'),
path('landabrechnungen/<uuid:pk>/', views.land_abrechnung_detail, name='land_abrechnung_detail'),
path('landabrechnungen/neu/', views.land_abrechnung_create, name='land_abrechnung_create'),
path('landabrechnungen/<uuid:pk>/bearbeiten/', views.land_abrechnung_update, name='land_abrechnung_update'),
path('landabrechnungen/<uuid:pk>/loeschen/', views.land_abrechnung_delete, name='land_abrechnung_delete'),
path("landabrechnungen/", views.land_abrechnung_list, name="land_abrechnung_list"),
path(
"landabrechnungen/<uuid:pk>/",
views.land_abrechnung_detail,
name="land_abrechnung_detail",
),
path(
"landabrechnungen/neu/",
views.land_abrechnung_create,
name="land_abrechnung_create",
),
path(
"landabrechnungen/<uuid:pk>/bearbeiten/",
views.land_abrechnung_update,
name="land_abrechnung_update",
),
path(
"landabrechnungen/<uuid:pk>/loeschen/",
views.land_abrechnung_delete,
name="land_abrechnung_delete",
),
# Vereinheitlichte Verpachtung URLs (direkt im Land)
path('laendereien/<uuid:land_pk>/verpachtung/neu/', views.land_verpachtung_create, name='land_verpachtung_create'),
path('laendereien/<uuid:land_pk>/verpachtung/bearbeiten/', views.land_verpachtung_edit, name='land_verpachtung_edit'),
path('laendereien/<uuid:land_pk>/verpachtung/beenden/', views.land_verpachtung_end, name='land_verpachtung_end'),
path(
"laendereien/<uuid:land_pk>/verpachtung/neu/",
views.land_verpachtung_create,
name="land_verpachtung_create",
),
path(
"laendereien/<uuid:land_pk>/verpachtung/bearbeiten/",
views.land_verpachtung_edit,
name="land_verpachtung_edit",
),
path(
"laendereien/<uuid:land_pk>/verpachtung/beenden/",
views.land_verpachtung_end,
name="land_verpachtung_end",
),
# LandVerpachtung URLs (neue Verpachtungen)
path('laendereien/verpachtungen/<uuid:pk>/', views.land_verpachtung_detail, name='land_verpachtung_detail'),
path('laendereien/verpachtungen/<uuid:pk>/bearbeiten/', views.land_verpachtung_update, name='land_verpachtung_update'),
path('laendereien/verpachtungen/<uuid:pk>/beenden/', views.land_verpachtung_end_direct, name='land_verpachtung_end_direct'),
path(
"laendereien/verpachtungen/<uuid:pk>/",
views.land_verpachtung_detail,
name="land_verpachtung_detail",
),
path(
"laendereien/verpachtungen/<uuid:pk>/bearbeiten/",
views.land_verpachtung_update,
name="land_verpachtung_update",
),
path(
"laendereien/verpachtungen/<uuid:pk>/beenden/",
views.land_verpachtung_end_direct,
name="land_verpachtung_end_direct",
),
# Förderung URLs
path('foerderungen/', views.foerderung_list, name='foerderung_list'),
path('foerderungen/<uuid:pk>/', views.foerderung_detail, name='foerderung_detail'),
path('foerderungen/neu/', views.foerderung_create, name='foerderung_create'),
path('foerderungen/<uuid:pk>/bearbeiten/', views.foerderung_update, name='foerderung_update'),
path('foerderungen/<uuid:pk>/loeschen/', views.foerderung_delete, name='foerderung_delete'),
path("foerderungen/", views.foerderung_list, name="foerderung_list"),
path("foerderungen/<uuid:pk>/", views.foerderung_detail, name="foerderung_detail"),
path("foerderungen/neu/", views.foerderung_create, name="foerderung_create"),
path(
"foerderungen/<uuid:pk>/bearbeiten/",
views.foerderung_update,
name="foerderung_update",
),
path(
"foerderungen/<uuid:pk>/loeschen/",
views.foerderung_delete,
name="foerderung_delete",
),
# Dokumente URLs
path('dokumente/', views.dokument_list, name='dokument_list'),
path('dokumente/<uuid:pk>/', views.dokument_detail, name='dokument_detail'),
path('dokumente/neu/', views.dokument_create, name='dokument_create'),
path('dokumente/<uuid:pk>/bearbeiten/', views.dokument_update, name='dokument_update'),
path('dokumente/<uuid:pk>/loeschen/', views.dokument_delete, name='dokument_delete'),
path("dokumente/", views.dokument_list, name="dokument_list"),
path("dokumente/<uuid:pk>/", views.dokument_detail, name="dokument_detail"),
path("dokumente/neu/", views.dokument_create, name="dokument_create"),
path(
"dokumente/<uuid:pk>/bearbeiten/", views.dokument_update, name="dokument_update"
),
path(
"dokumente/<uuid:pk>/loeschen/", views.dokument_delete, name="dokument_delete"
),
# Dokumentenverwaltung (Paperless-Integration, Verwaltung & Verknüpfung)
path('dokumente/verwaltung/', views.dokument_management, name='dokument_management'),
path(
"dokumente/verwaltung/", views.dokument_management, name="dokument_management"
),
# Legacy document URLs removed - use dokument_management instead
# Dokument-Verknüpfung
path('api/link-document/search/', views.link_document_search, name='link_document_search'),
path('api/link-document/create/', views.link_document_create, name='link_document_create'),
path('api/link-document/list/', views.link_document_list, name='link_document_list'),
path('api/link-document/update/', views.link_document_update, name='link_document_update'),
path('api/link-document/delete/<uuid:link_id>/', views.link_document_delete, name='link_document_delete'),
path(
"api/link-document/search/",
views.link_document_search,
name="link_document_search",
),
path(
"api/link-document/create/",
views.link_document_create,
name="link_document_create",
),
path(
"api/link-document/list/", views.link_document_list, name="link_document_list"
),
path(
"api/link-document/update/",
views.link_document_update,
name="link_document_update",
),
path(
"api/link-document/delete/<uuid:link_id>/",
views.link_document_delete,
name="link_document_delete",
),
# Legacy dokument_verknuepfung URL removed - use dokument_management instead
# Jahresbericht URLs
path('berichte/', views.bericht_list, name='bericht_list'),
path('berichte/jahresbericht/', views.jahresbericht_generate_redirect, name='jahresbericht_generate_redirect'),
path('berichte/jahresbericht/<int:jahr>/', views.jahresbericht_generate, name='jahresbericht_generate'),
path('berichte/jahresbericht/<int:jahr>/pdf/', views.jahresbericht_pdf, name='jahresbericht_pdf'),
path("berichte/", views.bericht_list, name="bericht_list"),
path(
"berichte/jahresbericht/",
views.jahresbericht_generate_redirect,
name="jahresbericht_generate_redirect",
),
path(
"berichte/jahresbericht/<int:jahr>/",
views.jahresbericht_generate,
name="jahresbericht_generate",
),
path(
"berichte/jahresbericht/<int:jahr>/pdf/",
views.jahresbericht_pdf,
name="jahresbericht_pdf",
),
# Geschäftsführung URLs
path('geschaeftsfuehrung/', views.geschaeftsfuehrung, name='geschaeftsfuehrung'),
path('geschaeftsfuehrung/konten/', views.konto_list, name='konto_list'),
path('geschaeftsfuehrung/konten/neu/', views.konto_create, name='konto_create'),
path('geschaeftsfuehrung/konten/<uuid:pk>/', views.konto_detail, name='konto_detail'),
path('geschaeftsfuehrung/konten/<uuid:pk>/bearbeiten/', views.konto_edit, name='konto_edit'),
path('geschaeftsfuehrung/verwaltungskosten/', views.verwaltungskosten_list, name='verwaltungskosten_list'),
path('geschaeftsfuehrung/verwaltungskosten/neu/', views.verwaltungskosten_create, name='verwaltungskosten_create'),
path('geschaeftsfuehrung/verwaltungskosten/<uuid:pk>/bearbeiten/', views.verwaltungskosten_edit, name='verwaltungskosten_edit'),
path('verwaltungskosten/mark-paid/', views.mark_expense_paid, name='mark_expense_paid'),
path('geschaeftsfuehrung/rentmeister/', views.rentmeister_list, name='rentmeister_list'),
path('geschaeftsfuehrung/rentmeister/neu/', views.rentmeister_create, name='rentmeister_create'),
path('geschaeftsfuehrung/rentmeister/<uuid:pk>/', views.rentmeister_detail, name='rentmeister_detail'),
path('geschaeftsfuehrung/rentmeister/<uuid:pk>/bearbeiten/', views.rentmeister_edit, name='rentmeister_edit'),
path('geschaeftsfuehrung/rentmeister/<uuid:pk>/ausgaben/', views.rentmeister_ausgaben, name='rentmeister_ausgaben'),
path("geschaeftsfuehrung/", views.geschaeftsfuehrung, name="geschaeftsfuehrung"),
path("geschaeftsfuehrung/konten/", views.konto_list, name="konto_list"),
path("geschaeftsfuehrung/konten/neu/", views.konto_create, name="konto_create"),
path(
"geschaeftsfuehrung/konten/<uuid:pk>/", views.konto_detail, name="konto_detail"
),
path(
"geschaeftsfuehrung/konten/<uuid:pk>/bearbeiten/",
views.konto_edit,
name="konto_edit",
),
path(
"geschaeftsfuehrung/verwaltungskosten/",
views.verwaltungskosten_list,
name="verwaltungskosten_list",
),
path(
"geschaeftsfuehrung/verwaltungskosten/neu/",
views.verwaltungskosten_create,
name="verwaltungskosten_create",
),
path(
"geschaeftsfuehrung/verwaltungskosten/<uuid:pk>/bearbeiten/",
views.verwaltungskosten_edit,
name="verwaltungskosten_edit",
),
path(
"verwaltungskosten/mark-paid/",
views.mark_expense_paid,
name="mark_expense_paid",
),
path(
"geschaeftsfuehrung/rentmeister/",
views.rentmeister_list,
name="rentmeister_list",
),
path(
"geschaeftsfuehrung/rentmeister/neu/",
views.rentmeister_create,
name="rentmeister_create",
),
path(
"geschaeftsfuehrung/rentmeister/<uuid:pk>/",
views.rentmeister_detail,
name="rentmeister_detail",
),
path(
"geschaeftsfuehrung/rentmeister/<uuid:pk>/bearbeiten/",
views.rentmeister_edit,
name="rentmeister_edit",
),
path(
"geschaeftsfuehrung/rentmeister/<uuid:pk>/ausgaben/",
views.rentmeister_ausgaben,
name="rentmeister_ausgaben",
),
# Administration URLs
path('administration/', views.administration, name='administration'),
path('administration/settings/', views.app_settings, name='app_settings'),
path('administration/audit-log/', views.audit_log_list, name='audit_log_list'),
path('administration/backup/', views.backup_management, name='backup_management'),
path('administration/backup/<uuid:backup_id>/download/', views.backup_download, name='backup_download'),
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'),
path("administration/", views.administration, name="administration"),
path("administration/settings/", views.app_settings, name="app_settings"),
path("administration/audit-log/", views.audit_log_list, name="audit_log_list"),
path("administration/backup/", views.backup_management, name="backup_management"),
path(
"administration/backup/<uuid:backup_id>/download/",
views.backup_download,
name="backup_download",
),
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)
path('unterstuetzungen/', views.unterstuetzungen_all, name='unterstuetzungen_all'),
path('unterstuetzungen/neu/', views.unterstuetzung_create, name='unterstuetzung_create'),
path('unterstuetzungen/<uuid:pk>/', views.unterstuetzung_detail, name='unterstuetzung_detail'),
path('unterstuetzungen/<uuid:pk>/bezahlt/', views.unterstuetzung_mark_paid, name='unterstuetzung_mark_paid'),
path('unterstuetzungen/wiederkehrend/', views.wiederkehrende_unterstuetzungen, name='wiederkehrende_unterstuetzungen'),
path("unterstuetzungen/", views.unterstuetzungen_all, name="unterstuetzungen_all"),
path(
"unterstuetzungen/neu/",
views.unterstuetzung_create,
name="unterstuetzung_create",
),
path(
"unterstuetzungen/<uuid:pk>/",
views.unterstuetzung_detail,
name="unterstuetzung_detail",
),
path(
"unterstuetzungen/<uuid:pk>/bezahlt/",
views.unterstuetzung_mark_paid,
name="unterstuetzung_mark_paid",
),
path(
"unterstuetzungen/wiederkehrend/",
views.wiederkehrende_unterstuetzungen,
name="wiederkehrende_unterstuetzungen",
),
# AJAX endpoints
path('api/destinataer/<uuid:destinataer_id>/info/', views.get_destinataer_info, name='get_destinataer_info'),
path(
"api/destinataer/<uuid:destinataer_id>/info/",
views.get_destinataer_info,
name="get_destinataer_info",
),
# Authentication URLs
path('login/', views.user_login, name='login'),
path('logout/', views.user_logout, name='logout'),
path("login/", views.user_login, name="login"),
path("logout/", views.user_logout, name="logout"),
# User Management URLs
path('administration/users/', views.user_management, name='user_management'),
path('administration/users/create/', views.user_create, name='user_create'),
path('administration/users/<int:pk>/', views.user_detail, name='user_detail'),
path('administration/users/<int:pk>/edit/', views.user_edit, name='user_edit'),
path('administration/users/<int:pk>/password/', views.user_change_password, name='user_change_password'),
path('administration/users/<int:pk>/permissions/', views.user_permissions, name='user_permissions'),
path('administration/users/<int:pk>/delete/', views.user_delete, name='user_delete'),
path("administration/users/", views.user_management, name="user_management"),
path("administration/users/create/", views.user_create, name="user_create"),
path("administration/users/<int:pk>/", views.user_detail, name="user_detail"),
path("administration/users/<int:pk>/edit/", views.user_edit, name="user_edit"),
path(
"administration/users/<int:pk>/password/",
views.user_change_password,
name="user_change_password",
),
path(
"administration/users/<int:pk>/permissions/",
views.user_permissions,
name="user_permissions",
),
path(
"administration/users/<int:pk>/delete/", views.user_delete, name="user_delete"
),
# Hilfsbox URLs
path('help-box/edit/', views.edit_help_box, name='edit_help_box'),
path('help-box/admin/', views.edit_help_box, name='help_boxes_admin'),
path("help-box/edit/", views.edit_help_box, name="edit_help_box"),
path("help-box/admin/", views.edit_help_box, name="help_boxes_admin"),
# API URLs
path('api/land-stats/', views.land_stats_api, name='land_stats_api'),
path('api/health/', views.health_check, name='health_check'),
path('api/paperless/ping/', views.paperless_ping, name='paperless_ping'),
path('api/paperless/documents/', views.paperless_documents, name='paperless_documents'),
path('api/paperless/tags/', views.paperless_tags_only, name='paperless_tags_only'),
path('api/paperless/debug/', views.paperless_debug, name='paperless_debug'),
path('api/paperless/documents/<int:doc_id>/', views.paperless_document_redirect, name='paperless_document_redirect'),
path("api/land-stats/", views.land_stats_api, name="land_stats_api"),
path("api/health/", views.health_check, name="health_check"),
path("api/paperless/ping/", views.paperless_ping, name="paperless_ping"),
path(
"api/paperless/documents/",
views.paperless_documents,
name="paperless_documents",
),
path("api/paperless/tags/", views.paperless_tags_only, name="paperless_tags_only"),
path("api/paperless/debug/", views.paperless_debug, name="paperless_debug"),
path(
"api/paperless/documents/<int:doc_id>/",
views.paperless_document_redirect,
name="paperless_document_redirect",
),
# Gramps integration (probe)
path('api/gramps/search/', views.gramps_search_api, name='gramps_search_api'),
path('api/gramps/debug/', views.gramps_debug_api, name='gramps_debug_api'),
path("api/gramps/search/", views.gramps_search_api, name="gramps_search_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
"""
from django.conf import settings
from stiftung.models import AppConfiguration
def get_config(key, default=None, fallback_to_settings=True):
"""
Get a configuration value from the database or fall back to Django settings
Args:
key: The configuration key
default: Default value if not found
fallback_to_settings: If True, try to get from Django settings using the key in uppercase
Returns:
The configuration value
"""
# Try to get from AppConfiguration first
value = AppConfiguration.get_setting(key, None)
# Fall back to Django settings if value is None or empty string
if not value and fallback_to_settings:
settings_key = key.upper()
return getattr(settings, settings_key, default)
return value if value is not None else default
def get_paperless_config():
"""
Get all Paperless-related configuration values
Returns:
dict: Dictionary containing all Paperless configuration
"""
return {
'api_url': get_config('paperless_api_url', 'http://192.168.178.167:30070'),
'api_token': get_config('paperless_api_token', ''),
'destinataere_tag': get_config('paperless_destinataere_tag', 'Stiftung_Destinatäre'),
'destinataere_tag_id': get_config('paperless_destinataere_tag_id', '210'),
'land_tag': get_config('paperless_land_tag', 'Stiftung_Land_und_Pächter'),
'land_tag_id': get_config('paperless_land_tag_id', '204'),
'admin_tag': get_config('paperless_admin_tag', 'Stiftung_Administration'),
'admin_tag_id': get_config('paperless_admin_tag_id', '216'),
"api_url": get_config("paperless_api_url", "http://192.168.178.167:30070"),
"api_token": get_config("paperless_api_token", ""),
"destinataere_tag": get_config(
"paperless_destinataere_tag", "Stiftung_Destinatäre"
),
"destinataere_tag_id": get_config("paperless_destinataere_tag_id", "210"),
"land_tag": get_config("paperless_land_tag", "Stiftung_Land_und_Pächter"),
"land_tag_id": get_config("paperless_land_tag_id", "204"),
"admin_tag": get_config("paperless_admin_tag", "Stiftung_Administration"),
"admin_tag_id": get_config("paperless_admin_tag_id", "216"),
}
def set_config(key, value, **kwargs):
"""
Set a configuration value
Args:
key: The configuration key
value: The value to set
**kwargs: Additional parameters for AppConfiguration.set_setting
Returns:
AppConfiguration: The configuration object
"""
@@ -65,9 +69,9 @@ def set_config(key, value, **kwargs):
def is_paperless_configured():
"""
Check if Paperless is properly configured
Returns:
bool: True if API URL and token are configured
"""
config = get_paperless_config()
return bool(config['api_url'] and config['api_token'])
return bool(config["api_url"] and config["api_token"])

View File

@@ -1,18 +1,21 @@
"""
PDF generation utilities with corporate identity support
"""
import os
import base64
import os
from io import BytesIO
from django.conf import settings
from django.template.loader import render_to_string
from django.http import HttpResponse
from django.template.loader import render_to_string
from django.utils import timezone
# Try to import WeasyPrint, fall back gracefully if not available
try:
from weasyprint import HTML, CSS
from weasyprint import CSS, HTML
from weasyprint.text.fonts import FontConfiguration
WEASYPRINT_AVAILABLE = True
IMPORT_ERROR = None
except ImportError as e:
@@ -35,72 +38,84 @@ from stiftung.models import AppConfiguration
class PDFGenerator:
"""Corporate identity PDF generator"""
def __init__(self):
if WEASYPRINT_AVAILABLE:
self.font_config = FontConfiguration()
else:
self.font_config = None
def is_available(self):
"""Check if PDF generation is available"""
return WEASYPRINT_AVAILABLE
def get_corporate_settings(self):
"""Get corporate identity settings from configuration"""
return {
'stiftung_name': AppConfiguration.get_setting('corporate_stiftung_name', 'Stiftung'),
'logo_path': AppConfiguration.get_setting('corporate_logo_path', ''),
'primary_color': AppConfiguration.get_setting('corporate_primary_color', '#2c3e50'),
'secondary_color': AppConfiguration.get_setting('corporate_secondary_color', '#3498db'),
'address_line1': AppConfiguration.get_setting('corporate_address_line1', ''),
'address_line2': AppConfiguration.get_setting('corporate_address_line2', ''),
'phone': AppConfiguration.get_setting('corporate_phone', ''),
'email': AppConfiguration.get_setting('corporate_email', ''),
'website': AppConfiguration.get_setting('corporate_website', ''),
'footer_text': AppConfiguration.get_setting('corporate_footer_text', 'Dieser Bericht wurde automatisch generiert.'),
"stiftung_name": AppConfiguration.get_setting(
"corporate_stiftung_name", "Stiftung"
),
"logo_path": AppConfiguration.get_setting("corporate_logo_path", ""),
"primary_color": AppConfiguration.get_setting(
"corporate_primary_color", "#2c3e50"
),
"secondary_color": AppConfiguration.get_setting(
"corporate_secondary_color", "#3498db"
),
"address_line1": AppConfiguration.get_setting(
"corporate_address_line1", ""
),
"address_line2": AppConfiguration.get_setting(
"corporate_address_line2", ""
),
"phone": AppConfiguration.get_setting("corporate_phone", ""),
"email": AppConfiguration.get_setting("corporate_email", ""),
"website": AppConfiguration.get_setting("corporate_website", ""),
"footer_text": AppConfiguration.get_setting(
"corporate_footer_text", "Dieser Bericht wurde automatisch generiert."
),
}
def get_logo_base64(self, logo_path):
"""Convert logo to base64 for embedding in PDF"""
if not logo_path:
return None
# Try different possible paths
possible_paths = [
logo_path,
os.path.join(settings.MEDIA_ROOT, logo_path),
os.path.join(settings.STATIC_ROOT or '', logo_path),
os.path.join(settings.BASE_DIR, 'static', logo_path),
os.path.join(settings.STATIC_ROOT or "", logo_path),
os.path.join(settings.BASE_DIR, "static", logo_path),
]
for path in possible_paths:
if os.path.exists(path):
try:
with open(path, 'rb') as img_file:
img_data = base64.b64encode(img_file.read()).decode('utf-8')
with open(path, "rb") as img_file:
img_data = base64.b64encode(img_file.read()).decode("utf-8")
# Determine MIME type
ext = os.path.splitext(path)[1].lower()
if ext in ['.jpg', '.jpeg']:
mime_type = 'image/jpeg'
elif ext == '.png':
mime_type = 'image/png'
elif ext == '.svg':
mime_type = 'image/svg+xml'
if ext in [".jpg", ".jpeg"]:
mime_type = "image/jpeg"
elif ext == ".png":
mime_type = "image/png"
elif ext == ".svg":
mime_type = "image/svg+xml"
else:
mime_type = 'image/png' # default
mime_type = "image/png" # default
return f"data:{mime_type};base64,{img_data}"
except Exception:
continue
return None
def get_base_css(self, corporate_settings):
"""Generate base CSS for corporate identity"""
primary_color = corporate_settings.get('primary_color', '#2c3e50')
secondary_color = corporate_settings.get('secondary_color', '#3498db')
primary_color = corporate_settings.get("primary_color", "#2c3e50")
secondary_color = corporate_settings.get("secondary_color", "#3498db")
return f"""
@page {{
size: A4;
@@ -291,7 +306,7 @@ class PDFGenerator:
page-break-before: always;
}}
"""
def generate_pdf_response(self, html_content, filename, css_content=None):
"""Generate PDF response from HTML content"""
if not WEASYPRINT_AVAILABLE:
@@ -320,27 +335,30 @@ class PDFGenerator:
</body>
</html>
"""
response = HttpResponse(error_html, content_type='text/html')
response['Content-Disposition'] = f'inline; filename="{filename.replace(".pdf", "_preview.html")}"'
response = HttpResponse(error_html, content_type="text/html")
response["Content-Disposition"] = (
f'inline; filename="{filename.replace(".pdf", "_preview.html")}"'
)
return response
try:
# Create CSS string
if css_content:
css = CSS(string=css_content, font_config=self.font_config)
else:
css = None
# Generate PDF
html_doc = HTML(string=html_content)
pdf_bytes = html_doc.write_pdf(stylesheets=[css] if css else None,
font_config=self.font_config)
pdf_bytes = html_doc.write_pdf(
stylesheets=[css] if css else None, font_config=self.font_config
)
# Create response
response = HttpResponse(pdf_bytes, content_type='application/pdf')
response['Content-Disposition'] = f'attachment; filename="{filename}"'
response = HttpResponse(pdf_bytes, content_type="application/pdf")
response["Content-Disposition"] = f'attachment; filename="{filename}"'
return response
except Exception as e:
# Fallback: return error message as HTML
error_html = f"""
@@ -368,15 +386,19 @@ class PDFGenerator:
</body>
</html>
"""
response = HttpResponse(error_html, content_type='text/html')
response['Content-Disposition'] = f'inline; filename="error_{filename.replace(".pdf", ".html")}"'
response = HttpResponse(error_html, content_type="text/html")
response["Content-Disposition"] = (
f'inline; filename="error_{filename.replace(".pdf", ".html")}"'
)
return response
def export_data_list_pdf(self, data, fields_config, title, filename_prefix, request_user=None):
def export_data_list_pdf(
self, data, fields_config, title, filename_prefix, request_user=None
):
"""
Export a list of data as formatted PDF
Args:
data: QuerySet or list of model instances
fields_config: dict with field names as keys and display names as values
@@ -385,34 +407,39 @@ class PDFGenerator:
request_user: User making the request (for audit purposes)
"""
corporate_settings = self.get_corporate_settings()
logo_base64 = self.get_logo_base64(corporate_settings.get('logo_path', ''))
logo_base64 = self.get_logo_base64(corporate_settings.get("logo_path", ""))
# Prepare context
context = {
'corporate_settings': corporate_settings,
'logo_base64': logo_base64,
'title': title,
'data': data,
'fields_config': fields_config,
'generation_date': timezone.now(),
'generated_by': (request_user.get_full_name()
if hasattr(request_user, 'get_full_name') and request_user.get_full_name()
else request_user.username
if hasattr(request_user, 'username') and request_user.username
else 'System'),
'total_count': len(data) if hasattr(data, '__len__') else data.count(),
"corporate_settings": corporate_settings,
"logo_base64": logo_base64,
"title": title,
"data": data,
"fields_config": fields_config,
"generation_date": timezone.now(),
"generated_by": (
request_user.get_full_name()
if hasattr(request_user, "get_full_name")
and request_user.get_full_name()
else (
request_user.username
if hasattr(request_user, "username") and request_user.username
else "System"
)
),
"total_count": len(data) if hasattr(data, "__len__") else data.count(),
}
# Render HTML
html_content = render_to_string('pdf/data_list.html', context)
html_content = render_to_string("pdf/data_list.html", context)
# Generate CSS
css_content = self.get_base_css(corporate_settings)
# Generate filename
timestamp = timezone.now().strftime('%Y%m%d_%H%M%S')
timestamp = timezone.now().strftime("%Y%m%d_%H%M%S")
filename = f"{filename_prefix}_{timestamp}.pdf"
return self.generate_pdf_response(html_content, filename, css_content)

File diff suppressed because it is too large Load Diff