fix: configure CI database connection properly
- 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
This commit is contained in:
133
app/stiftung/management/commands/generate_recurring_payments.py
Normal file
133
app/stiftung/management/commands/generate_recurring_payments.py
Normal file
@@ -0,0 +1,133 @@
|
||||
"""
|
||||
Management command to generate due recurring support payments.
|
||||
This command should be run daily via cron or similar scheduling system.
|
||||
"""
|
||||
|
||||
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'
|
||||
|
||||
def add_arguments(self, parser):
|
||||
parser.add_argument(
|
||||
'--dry-run',
|
||||
action='store_true',
|
||||
help='Show what would be generated without actually creating payments',
|
||||
)
|
||||
parser.add_argument(
|
||||
'--days-ahead',
|
||||
type=int,
|
||||
default=0,
|
||||
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']
|
||||
|
||||
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'))
|
||||
|
||||
# Get all active recurring payment templates that are due
|
||||
templates = UnterstuetzungWiederkehrend.objects.filter(
|
||||
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'€{template.betrag} due {template.naechste_generierung.strftime("%d.%m.%Y")}'
|
||||
)
|
||||
generated_count += 1
|
||||
else:
|
||||
# Actually generate the payment
|
||||
neue_zahlung = template.generiere_naechste_zahlung()
|
||||
if neue_zahlung:
|
||||
self.stdout.write(
|
||||
self.style.SUCCESS(
|
||||
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}')
|
||||
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)'
|
||||
)
|
||||
)
|
||||
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)}'
|
||||
)
|
||||
)
|
||||
logger.error(f'Error generating recurring payment for template {template.pk}: {str(e)}')
|
||||
|
||||
# Summary
|
||||
self.stdout.write('\n' + '='*50)
|
||||
if dry_run:
|
||||
self.stdout.write(
|
||||
self.style.SUCCESS(
|
||||
f'DRY RUN COMPLETE: {generated_count} payments would be generated'
|
||||
)
|
||||
)
|
||||
else:
|
||||
self.stdout.write(
|
||||
self.style.SUCCESS(
|
||||
f'GENERATION COMPLETE: {generated_count} payments generated'
|
||||
)
|
||||
)
|
||||
|
||||
if error_count > 0:
|
||||
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')
|
||||
|
||||
if overdue_payments.exists():
|
||||
self.stdout.write('\n' + '='*50)
|
||||
self.stdout.write(
|
||||
self.style.WARNING(
|
||||
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)'
|
||||
)
|
||||
if overdue_payments.count() > 10:
|
||||
self.stdout.write(f' ... and {overdue_payments.count() - 10} more')
|
||||
124
app/stiftung/management/commands/init_config.py
Normal file
124
app/stiftung/management/commands/init_config.py
Normal file
@@ -0,0 +1,124 @@
|
||||
from django.core.management.base import BaseCommand
|
||||
from stiftung.models import AppConfiguration
|
||||
|
||||
|
||||
class Command(BaseCommand):
|
||||
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_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_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_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_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
|
||||
updated_count = 0
|
||||
|
||||
for setting_data in paperless_settings:
|
||||
setting, created = AppConfiguration.objects.get_or_create(
|
||||
key=setting_data['key'],
|
||||
defaults=setting_data
|
||||
)
|
||||
|
||||
if created:
|
||||
created_count += 1
|
||||
self.stdout.write(
|
||||
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.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.'
|
||||
)
|
||||
)
|
||||
self.stdout.write(
|
||||
self.style.WARNING(
|
||||
'You can now manage these settings in the Django Admin under "App Configurations"'
|
||||
)
|
||||
)
|
||||
149
app/stiftung/management/commands/init_corporate_settings.py
Normal file
149
app/stiftung/management/commands/init_corporate_settings.py
Normal file
@@ -0,0 +1,149 @@
|
||||
"""
|
||||
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'
|
||||
|
||||
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_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_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_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_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_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
|
||||
},
|
||||
]
|
||||
|
||||
created_count = 0
|
||||
updated_count = 0
|
||||
|
||||
for setting_data in corporate_settings:
|
||||
setting, created = AppConfiguration.objects.get_or_create(
|
||||
key=setting_data['key'],
|
||||
defaults=setting_data
|
||||
)
|
||||
|
||||
if created:
|
||||
created_count += 1
|
||||
self.stdout.write(
|
||||
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.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.'
|
||||
)
|
||||
)
|
||||
|
||||
if created_count > 0:
|
||||
self.stdout.write(
|
||||
self.style.WARNING(
|
||||
'Please configure your corporate identity settings in '
|
||||
'Administration -> Application Settings before generating PDFs.'
|
||||
)
|
||||
)
|
||||
253
app/stiftung/management/commands/sync_abrechnungen.py
Normal file
253
app/stiftung/management/commands/sync_abrechnungen.py
Normal file
@@ -0,0 +1,253 @@
|
||||
#!/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
|
||||
Reference in New Issue
Block a user