Files
stiftung-management-system/app/stiftung/management/commands/sync_abrechnungen.py
Stiftung Development e0c7d0e351 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
2025-09-06 21:04:07 +02:00

272 lines
10 KiB
Python

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