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