#!/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