Format code with Black and isort for CI/CD compliance

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

View File

@@ -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 ""