- Add dotenv loading to Django settings - Update CI workflow to use correct environment variables - Set POSTGRES_* variables instead of DATABASE_URL - Add environment variables to all Django management commands - Fixes CI test failures due to database connection issues
254 lines
10 KiB
Python
254 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 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
|