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:
@@ -4,6 +4,8 @@ from io import StringIO
|
||||
from django.db import models
|
||||
from django.core.validators import MinValueValidator, MaxValueValidator
|
||||
from django.utils import timezone
|
||||
from dateutil.relativedelta import relativedelta
|
||||
from stiftung.utils.date_utils import ensure_date, get_year_from_date
|
||||
|
||||
class CSVImport(models.Model):
|
||||
"""Track CSV import operations for audit purposes"""
|
||||
@@ -129,18 +131,18 @@ class Paechter(models.Model):
|
||||
|
||||
def get_aktive_verpachtungen(self):
|
||||
"""Get all active leases for this tenant"""
|
||||
return self.verpachtung_set.filter(status='aktiv')
|
||||
return self.neue_verpachtungen.filter(status='aktiv')
|
||||
|
||||
def get_gesamt_pachtflaeche(self):
|
||||
"""Calculate total leased area"""
|
||||
return self.verpachtung_set.filter(status='aktiv').aggregate(
|
||||
return self.neue_verpachtungen.filter(status='aktiv').aggregate(
|
||||
total=models.Sum('verpachtete_flaeche')
|
||||
)['total'] or 0
|
||||
|
||||
def get_gesamt_pachtzins(self):
|
||||
"""Calculate total annual rent"""
|
||||
return self.verpachtung_set.filter(status='aktiv').aggregate(
|
||||
total=models.Sum('pachtzins_jaehrlich')
|
||||
return self.neue_verpachtungen.filter(status='aktiv').aggregate(
|
||||
total=models.Sum('pachtzins_pauschal')
|
||||
)['total'] or 0
|
||||
|
||||
class Destinataer(models.Model):
|
||||
@@ -505,12 +507,8 @@ class Land(models.Model):
|
||||
if self.verp_flaeche_aktuell and self.verp_flaeche_aktuell > 0:
|
||||
return self.verp_flaeche_aktuell
|
||||
|
||||
# Fallback: Legacy Verpachtungen (für Rückwärtskompatibilität)
|
||||
legacy_total = self.verpachtung_set.filter(status='aktiv').aggregate(
|
||||
total=Sum('verpachtete_flaeche')
|
||||
)['total'] or 0
|
||||
|
||||
return legacy_total
|
||||
# No legacy system - return neue_total (could be 0)
|
||||
return neue_total
|
||||
|
||||
def get_verfuegbare_flaeche(self):
|
||||
"""Berechnet die noch verfügbare Fläche für neue Verpachtungen"""
|
||||
@@ -690,17 +688,29 @@ class LandVerpachtung(models.Model):
|
||||
def is_aktiv(self):
|
||||
"""Prüft ob der Vertrag noch aktiv ist"""
|
||||
from datetime import date
|
||||
|
||||
|
||||
heute = date.today()
|
||||
if self.pachtende:
|
||||
return self.pachtbeginn <= heute <= self.pachtende
|
||||
return self.pachtbeginn <= heute # Unbefristet
|
||||
pachtbeginn_date = ensure_date(self.pachtbeginn)
|
||||
pachtende_date = ensure_date(self.pachtende)
|
||||
|
||||
if not pachtbeginn_date:
|
||||
return False
|
||||
|
||||
if pachtende_date:
|
||||
return pachtbeginn_date <= heute <= pachtende_date
|
||||
return pachtbeginn_date <= heute # Unbefristet
|
||||
|
||||
def get_restlaufzeit_tage(self):
|
||||
"""Berechnet die Restlaufzeit in Tagen"""
|
||||
from datetime import date
|
||||
|
||||
|
||||
heute = date.today()
|
||||
if self.pachtende and self.pachtende > heute:
|
||||
return (self.pachtende - heute).days
|
||||
pachtende_date = ensure_date(self.pachtende)
|
||||
|
||||
if pachtende_date and pachtende_date > heute:
|
||||
return (pachtende_date - heute).days
|
||||
return None # Unbefristet
|
||||
|
||||
@property
|
||||
@@ -708,10 +718,199 @@ class LandVerpachtung(models.Model):
|
||||
"""Berechnet die USt auf Pacht (falls optiert)"""
|
||||
from decimal import Decimal, ROUND_HALF_UP
|
||||
if self.ust_option and self.pachtzins_pauschal:
|
||||
ust = self.pachtzins_pauschal * (self.ust_satz / Decimal('100'))
|
||||
return ust.quantize(Decimal('0.01'), rounding=ROUND_HALF_UP)
|
||||
ust_betrag = Decimal(str(self.pachtzins_pauschal)) * Decimal(str(self.ust_satz)) / Decimal('100')
|
||||
return ust_betrag.quantize(Decimal('0.01'), rounding=ROUND_HALF_UP)
|
||||
return Decimal('0.00')
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
"""Override save to trigger Abrechnung updates"""
|
||||
is_new = self.pk is None
|
||||
old_instance = None
|
||||
|
||||
if not is_new:
|
||||
try:
|
||||
old_instance = LandVerpachtung.objects.get(pk=self.pk)
|
||||
except LandVerpachtung.DoesNotExist:
|
||||
old_instance = None
|
||||
|
||||
super().save(*args, **kwargs)
|
||||
|
||||
# Update Abrechnungen after save
|
||||
self._update_abrechnungen(old_instance, is_new)
|
||||
|
||||
def _update_abrechnungen(self, old_instance, is_new):
|
||||
"""Update LandAbrechnung records when Verpachtung changes"""
|
||||
from datetime import date
|
||||
|
||||
|
||||
# Determine affected years
|
||||
years_to_update = set()
|
||||
|
||||
pachtbeginn_year = get_year_from_date(self.pachtbeginn)
|
||||
if pachtbeginn_year:
|
||||
years_to_update.add(pachtbeginn_year)
|
||||
|
||||
pachtende_year = get_year_from_date(self.pachtende)
|
||||
if pachtende_year:
|
||||
years_to_update.add(pachtende_year)
|
||||
|
||||
# If updated, check old dates too
|
||||
if old_instance:
|
||||
old_pachtbeginn_year = get_year_from_date(old_instance.pachtbeginn)
|
||||
if old_pachtbeginn_year:
|
||||
years_to_update.add(old_pachtbeginn_year)
|
||||
|
||||
old_pachtende_year = get_year_from_date(old_instance.pachtende)
|
||||
if old_pachtende_year:
|
||||
years_to_update.add(old_pachtende_year)
|
||||
|
||||
# Add current year if contract is active
|
||||
if self.is_aktiv():
|
||||
years_to_update.add(date.today().year)
|
||||
|
||||
# Update each affected year
|
||||
for year in years_to_update:
|
||||
self._update_abrechnung_for_year(year, old_instance, is_new)
|
||||
|
||||
def _update_abrechnung_for_year(self, year, old_instance, is_new):
|
||||
"""Update or create LandAbrechnung for specific year"""
|
||||
from decimal import Decimal
|
||||
from datetime import date
|
||||
|
||||
# Get or create Abrechnung for this year
|
||||
abrechnung, created = LandAbrechnung.objects.get_or_create(
|
||||
land=self.land,
|
||||
abrechnungsjahr=year,
|
||||
defaults={
|
||||
'pacht_vereinnahmt': Decimal('0.00'),
|
||||
'umlagen_vereinnahmt': Decimal('0.00'),
|
||||
'bemerkungen': f'Automatisch erstellt für {self.vertragsnummer}'
|
||||
}
|
||||
)
|
||||
|
||||
# Calculate rent for this year
|
||||
rent_for_year = self._calculate_rent_for_year(year)
|
||||
umlage_for_year = self._calculate_umlage_for_year(year)
|
||||
|
||||
# Update or add to existing amounts
|
||||
if created or is_new:
|
||||
# New Abrechnung or new Verpachtung
|
||||
abrechnung.pacht_vereinnahmt += rent_for_year
|
||||
abrechnung.umlagen_vereinnahmt += umlage_for_year
|
||||
change_note = f"Neue Verpachtung {self.vertragsnummer} hinzugefügt"
|
||||
else:
|
||||
# Update existing - calculate difference
|
||||
old_rent = old_instance._calculate_rent_for_year(year) if old_instance else Decimal('0.00')
|
||||
old_umlage = old_instance._calculate_umlage_for_year(year) if old_instance else Decimal('0.00')
|
||||
|
||||
rent_diff = rent_for_year - old_rent
|
||||
umlage_diff = umlage_for_year - old_umlage
|
||||
|
||||
abrechnung.pacht_vereinnahmt += rent_diff
|
||||
abrechnung.umlagen_vereinnahmt += umlage_diff
|
||||
|
||||
if rent_diff != 0 or umlage_diff != 0:
|
||||
change_note = f"Verpachtung {self.vertragsnummer} geändert: Pacht {rent_diff:+.2f}€, Umlagen {umlage_diff:+.2f}€"
|
||||
else:
|
||||
change_note = f"Verpachtung {self.vertragsnummer} aktualisiert (keine Betragsänderung)"
|
||||
|
||||
# Add change tracking to bemerkungen (if significant change)
|
||||
if change_note and ('hinzugefügt' in change_note or 'geändert' in change_note):
|
||||
if abrechnung.bemerkungen:
|
||||
abrechnung.bemerkungen += f"\n[{date.today().strftime('%d.%m.%Y')}] {change_note}"
|
||||
else:
|
||||
abrechnung.bemerkungen = f"[{date.today().strftime('%d.%m.%Y')}] {change_note}"
|
||||
|
||||
abrechnung.save()
|
||||
|
||||
def _calculate_rent_for_year(self, year):
|
||||
"""Calculate rent amount for specific year"""
|
||||
from decimal import Decimal
|
||||
from datetime import date
|
||||
from django.utils.dateparse import parse_date
|
||||
|
||||
# Helper function to convert date strings to date objects
|
||||
def ensure_date(date_value):
|
||||
if not date_value:
|
||||
return None
|
||||
if isinstance(date_value, str):
|
||||
return parse_date(date_value)
|
||||
return date_value
|
||||
|
||||
if not self.pachtzins_pauschal or not self.pachtbeginn:
|
||||
return Decimal('0.00')
|
||||
|
||||
# Check if contract is active in this year
|
||||
year_start = date(year, 1, 1)
|
||||
year_end = date(year, 12, 31)
|
||||
|
||||
# Convert dates to ensure they are date objects
|
||||
pachtbeginn_date = ensure_date(self.pachtbeginn)
|
||||
pachtende_date = ensure_date(self.pachtende)
|
||||
|
||||
if not pachtbeginn_date:
|
||||
return Decimal('0.00')
|
||||
|
||||
contract_start = max(pachtbeginn_date, year_start)
|
||||
contract_end = min(pachtende_date or year_end, year_end)
|
||||
|
||||
if contract_start > contract_end:
|
||||
return Decimal('0.00') # No overlap
|
||||
|
||||
# Calculate proportion of year
|
||||
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(self.pachtzins_pauschal)) * proportion
|
||||
|
||||
def _calculate_umlage_for_year(self, year):
|
||||
"""Calculate Umlage amount for specific year based on what can be passed through"""
|
||||
from decimal import Decimal
|
||||
# This would need to be calculated based on actual costs and what's umlagefähig
|
||||
# For now, return 0 - this can be enhanced later with actual cost calculation
|
||||
return Decimal('0.00')
|
||||
|
||||
def delete(self, *args, **kwargs):
|
||||
"""Override delete to update Abrechnungen when Verpachtung is removed"""
|
||||
|
||||
|
||||
# Calculate what needs to be removed from Abrechnungen
|
||||
years_to_update = set()
|
||||
|
||||
pachtbeginn_year = get_year_from_date(self.pachtbeginn)
|
||||
if pachtbeginn_year:
|
||||
years_to_update.add(pachtbeginn_year)
|
||||
|
||||
pachtende_year = get_year_from_date(self.pachtende)
|
||||
if pachtende_year:
|
||||
years_to_update.add(pachtende_year)
|
||||
|
||||
# Remove from Abrechnungen before deleting
|
||||
for year in years_to_update:
|
||||
try:
|
||||
abrechnung = LandAbrechnung.objects.get(land=self.land, abrechnungsjahr=year)
|
||||
|
||||
rent_to_remove = self._calculate_rent_for_year(year)
|
||||
umlage_to_remove = self._calculate_umlage_for_year(year)
|
||||
|
||||
abrechnung.pacht_vereinnahmt -= rent_to_remove
|
||||
abrechnung.umlagen_vereinnahmt -= umlage_to_remove
|
||||
|
||||
# Add deletion note
|
||||
from datetime import date
|
||||
change_note = f"Verpachtung {self.vertragsnummer} gelöscht: Pacht -{rent_to_remove:.2f}€, Umlagen -{umlage_to_remove:.2f}€"
|
||||
if abrechnung.bemerkungen:
|
||||
abrechnung.bemerkungen += f"\n[{date.today().strftime('%d.%m.%Y')}] {change_note}"
|
||||
else:
|
||||
abrechnung.bemerkungen = f"[{date.today().strftime('%d.%m.%Y')}] {change_note}"
|
||||
|
||||
abrechnung.save()
|
||||
except LandAbrechnung.DoesNotExist:
|
||||
pass # No Abrechnung to update
|
||||
|
||||
super().delete(*args, **kwargs)
|
||||
|
||||
|
||||
class LandAbrechnung(models.Model):
|
||||
"""Jahresabrechnung für Ländereien"""
|
||||
@@ -877,99 +1076,6 @@ class LandAbrechnung(models.Model):
|
||||
return ust.quantize(Decimal('0.01'), rounding=ROUND_HALF_UP)
|
||||
return Decimal('0.00')
|
||||
|
||||
|
||||
class Verpachtung(models.Model):
|
||||
"""Verpachtungsverträge für Ländereien"""
|
||||
|
||||
STATUS_CHOICES = [
|
||||
('aktiv', 'Aktiv'),
|
||||
('beendet', 'Beendet'),
|
||||
('gekuendigt', 'Gekündigt'),
|
||||
('verlängert', 'Verlängert'),
|
||||
]
|
||||
|
||||
id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
|
||||
|
||||
# Verpachtete Ländereien
|
||||
land = models.ForeignKey(Land, on_delete=models.CASCADE, verbose_name="Land")
|
||||
paechter = models.ForeignKey(Paechter, on_delete=models.CASCADE, verbose_name="Pächter")
|
||||
|
||||
# Vertragsdaten
|
||||
vertragsnummer = models.CharField(max_length=50, unique=True, verbose_name="Vertragsnummer")
|
||||
pachtbeginn = models.DateField(verbose_name="Pachtbeginn")
|
||||
pachtende = models.DateField(verbose_name="Pachtende")
|
||||
verlaengerung = models.DateField(null=True, blank=True, verbose_name="Verlängerung bis")
|
||||
|
||||
# Finanzielle Bedingungen
|
||||
pachtzins_pro_qm = models.DecimalField(
|
||||
max_digits=8,
|
||||
decimal_places=4,
|
||||
verbose_name="Pachtzins pro qm (€)"
|
||||
)
|
||||
pachtzins_jaehrlich = models.DecimalField(
|
||||
max_digits=12,
|
||||
decimal_places=2,
|
||||
verbose_name="Jährlicher Pachtzins (€)"
|
||||
)
|
||||
|
||||
# Flächenangaben
|
||||
verpachtete_flaeche = models.DecimalField(
|
||||
max_digits=12,
|
||||
decimal_places=2,
|
||||
verbose_name="Verpachtete Fläche (qm)"
|
||||
)
|
||||
|
||||
# Status
|
||||
status = models.CharField(max_length=20, choices=STATUS_CHOICES, default='aktiv')
|
||||
|
||||
# Dokumentation
|
||||
verwendungsnachweis = models.ForeignKey('DokumentLink', null=True, blank=True, on_delete=models.SET_NULL, verbose_name="Verwendungsnachweis")
|
||||
bemerkungen = models.TextField(null=True, blank=True, verbose_name="Ergänzende Kommentare")
|
||||
|
||||
# Zeitstempel
|
||||
erstellt_am = models.DateTimeField(auto_now_add=True)
|
||||
aktualisiert_am = models.DateTimeField(auto_now=True)
|
||||
|
||||
class Meta:
|
||||
verbose_name = "Verpachtung"
|
||||
verbose_name_plural = "Verpachtungen"
|
||||
ordering = ['-pachtbeginn']
|
||||
|
||||
def __str__(self):
|
||||
return f"{self.land} - {self.paechter} ({self.vertragsnummer})"
|
||||
|
||||
def get_vertragsdauer_tage(self):
|
||||
"""Berechnet die Vertragsdauer in Tagen"""
|
||||
from datetime import date
|
||||
if self.pachtende:
|
||||
return (self.pachtende - self.pachtbeginn).days
|
||||
return 0
|
||||
|
||||
def get_restlaufzeit_tage(self):
|
||||
"""Berechnet die Restlaufzeit in Tagen"""
|
||||
from datetime import date
|
||||
heute = date.today()
|
||||
if self.pachtende and self.pachtende > heute:
|
||||
return (self.pachtende - heute).days
|
||||
return 0
|
||||
|
||||
def is_aktiv(self):
|
||||
"""Prüft ob der Vertrag noch aktiv ist"""
|
||||
from datetime import date
|
||||
heute = date.today()
|
||||
return self.pachtbeginn <= heute <= self.pachtende
|
||||
|
||||
@property
|
||||
def verpachtete_flaeche_hektar(self):
|
||||
"""Berechnet die verpachtete Fläche in Hektar"""
|
||||
from decimal import Decimal, ROUND_HALF_UP
|
||||
if self.verpachtete_flaeche and self.verpachtete_flaeche > 0:
|
||||
# Umrechnung: 1 Hektar = 10.000 qm
|
||||
hektar = Decimal(str(self.verpachtete_flaeche)) / Decimal('10000')
|
||||
# Runden auf 2 Nachkommastellen
|
||||
return hektar.quantize(Decimal('0.01'), rounding=ROUND_HALF_UP)
|
||||
return Decimal('0.00')
|
||||
|
||||
class DokumentLink(models.Model):
|
||||
KONTEXT_CHOICES = [
|
||||
('pachtvertrag', 'Pachtvertrag'),
|
||||
@@ -997,6 +1103,7 @@ class DokumentLink(models.Model):
|
||||
destinataer_id = models.UUIDField(null=True, blank=True, verbose_name="Destinatär ID")
|
||||
foerderung_id = models.UUIDField(null=True, blank=True, verbose_name="Förderung ID")
|
||||
rentmeister_id = models.UUIDField(null=True, blank=True, verbose_name="Rentmeister ID")
|
||||
abrechnung_id = models.UUIDField(null=True, blank=True, verbose_name="Abrechnung ID")
|
||||
|
||||
class Meta:
|
||||
verbose_name = "Dokument"
|
||||
@@ -1007,17 +1114,14 @@ class DokumentLink(models.Model):
|
||||
return f"{self.titel} ({self.get_kontext_display()})"
|
||||
|
||||
def get_paperless_url(self):
|
||||
"""Gibt die URL zum Dokument in Paperless zurück"""
|
||||
from django.conf import settings
|
||||
if settings.PAPERLESS_API_URL:
|
||||
return f"{settings.PAPERLESS_API_URL}/documents/{self.paperless_document_id}/"
|
||||
return None
|
||||
"""Gibt die URL zum Dokument in Paperless zurück (über Django Redirect)"""
|
||||
return f"/api/paperless/documents/{self.paperless_document_id}/"
|
||||
|
||||
def get_paperless_thumbnail_url(self):
|
||||
"""Gibt die URL zum Thumbnail in Paperless zurück"""
|
||||
from django.conf import settings
|
||||
if settings.PAPERLESS_API_URL:
|
||||
return f"{settings.PAPERLESS_API_URL}/api/documents/{self.paperless_document_id}/thumb/"
|
||||
return f"{settings.PAPERLESS_API_URL}/api/paperless/documents/{self.paperless_document_id}/thumb/"
|
||||
return None
|
||||
|
||||
def get_verpachtung(self):
|
||||
@@ -1135,6 +1239,7 @@ class DestinataerUnterstuetzung(models.Model):
|
||||
"""Geplante/ausgeführte Unterstützungszahlungen an Destinatäre"""
|
||||
STATUS_CHOICES = [
|
||||
('geplant', 'Geplant'),
|
||||
('faellig', 'Fällig'),
|
||||
('in_bearbeitung', 'In Bearbeitung'),
|
||||
('ausgezahlt', 'Ausgezahlt'),
|
||||
('storniert', 'Storniert'),
|
||||
@@ -1147,6 +1252,17 @@ class DestinataerUnterstuetzung(models.Model):
|
||||
faellig_am = models.DateField(verbose_name='Fällig am')
|
||||
status = models.CharField(max_length=20, choices=STATUS_CHOICES, default='geplant', verbose_name='Status')
|
||||
beschreibung = models.CharField(max_length=255, blank=True, verbose_name='Beschreibung')
|
||||
|
||||
# Enhanced fields for recurrent payments and IBAN tracking
|
||||
empfaenger_iban = models.CharField(max_length=34, blank=True, verbose_name='Empfänger IBAN')
|
||||
empfaenger_name = models.CharField(max_length=200, blank=True, verbose_name='Empfänger Name')
|
||||
verwendungszweck = models.CharField(max_length=140, blank=True, verbose_name='Verwendungszweck')
|
||||
ausgezahlt_am = models.DateField(null=True, blank=True, verbose_name='Ausgezahlt am')
|
||||
ausgezahlt_von = models.ForeignKey('auth.User', on_delete=models.SET_NULL, null=True, blank=True, verbose_name='Ausgezahlt von')
|
||||
|
||||
# Link to recurrent payment template if this was auto-generated
|
||||
wiederkehrend_von = models.ForeignKey('UnterstuetzungWiederkehrend', on_delete=models.SET_NULL, null=True, blank=True, verbose_name='Wiederkehrende Zahlung')
|
||||
|
||||
erstellt_am = models.DateTimeField(auto_now_add=True)
|
||||
aktualisiert_am = models.DateTimeField(auto_now=True)
|
||||
|
||||
@@ -1157,10 +1273,106 @@ class DestinataerUnterstuetzung(models.Model):
|
||||
indexes = [
|
||||
models.Index(fields=['status', 'faellig_am']),
|
||||
models.Index(fields=['destinataer', 'status']),
|
||||
models.Index(fields=['wiederkehrend_von']),
|
||||
]
|
||||
|
||||
def __str__(self):
|
||||
return f"{self.destinataer.get_full_name()} – €{self.betrag} am {self.faellig_am} ({self.get_status_display()})"
|
||||
|
||||
def is_overdue(self):
|
||||
"""Check if payment is overdue"""
|
||||
from django.utils import timezone
|
||||
return self.faellig_am < timezone.now().date() and self.status in ['geplant', 'faellig']
|
||||
|
||||
def can_be_marked_paid(self):
|
||||
"""Check if payment can be marked as paid"""
|
||||
return self.status in ['geplant', 'faellig', 'in_bearbeitung']
|
||||
|
||||
|
||||
class UnterstuetzungWiederkehrend(models.Model):
|
||||
"""Template for recurring support payments"""
|
||||
INTERVALL_CHOICES = [
|
||||
('monatlich', 'Monatlich'),
|
||||
('quartalsweise', 'Vierteljährlich'),
|
||||
('halbjaehrlich', 'Halbjährlich'),
|
||||
('jaehrlich', 'Jährlich'),
|
||||
]
|
||||
|
||||
id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
|
||||
destinataer = models.ForeignKey('Destinataer', on_delete=models.CASCADE, related_name='wiederkehrende_unterstuetzungen', verbose_name='Destinatär')
|
||||
konto = models.ForeignKey('StiftungsKonto', on_delete=models.PROTECT, verbose_name='Zahlungskonto')
|
||||
betrag = models.DecimalField(max_digits=12, decimal_places=2, verbose_name='Betrag (€)')
|
||||
intervall = models.CharField(max_length=20, choices=INTERVALL_CHOICES, verbose_name='Intervall')
|
||||
beschreibung = models.CharField(max_length=255, blank=True, verbose_name='Beschreibung')
|
||||
|
||||
# IBAN and payment details
|
||||
empfaenger_iban = models.CharField(max_length=34, verbose_name='Empfänger IBAN')
|
||||
empfaenger_name = models.CharField(max_length=200, verbose_name='Empfänger Name')
|
||||
verwendungszweck = models.CharField(max_length=140, blank=True, verbose_name='Verwendungszweck')
|
||||
|
||||
# Schedule settings
|
||||
erste_zahlung_am = models.DateField(verbose_name='Erste Zahlung am')
|
||||
letzte_zahlung_am = models.DateField(null=True, blank=True, verbose_name='Letzte Zahlung am (optional)')
|
||||
naechste_generierung = models.DateField(verbose_name='Nächste Generierung')
|
||||
|
||||
aktiv = models.BooleanField(default=True, verbose_name='Aktiv')
|
||||
erstellt_am = models.DateTimeField(auto_now_add=True)
|
||||
erstellt_von = models.ForeignKey('auth.User', on_delete=models.SET_NULL, null=True, blank=True, verbose_name='Erstellt von')
|
||||
|
||||
class Meta:
|
||||
verbose_name = 'Wiederkehrende Unterstützung'
|
||||
verbose_name_plural = 'Wiederkehrende Unterstützungen'
|
||||
ordering = ['-erstellt_am']
|
||||
indexes = [
|
||||
models.Index(fields=['aktiv', 'naechste_generierung']),
|
||||
models.Index(fields=['destinataer', 'aktiv']),
|
||||
]
|
||||
|
||||
def __str__(self):
|
||||
return f"{self.destinataer.get_full_name()} – {self.get_intervall_display()} €{self.betrag}"
|
||||
|
||||
def generiere_naechste_zahlung(self):
|
||||
"""Generate the next scheduled payment"""
|
||||
from datetime import timedelta
|
||||
from dateutil.relativedelta import relativedelta
|
||||
|
||||
if not self.aktiv:
|
||||
return None
|
||||
|
||||
heute = timezone.now().date()
|
||||
if self.naechste_generierung > heute:
|
||||
return None # Not yet time to generate
|
||||
|
||||
# Check if we've reached the end date
|
||||
if self.letzte_zahlung_am and self.naechste_generierung > self.letzte_zahlung_am:
|
||||
return None
|
||||
|
||||
# Create the next payment
|
||||
neue_zahlung = DestinataerUnterstuetzung.objects.create(
|
||||
destinataer=self.destinataer,
|
||||
konto=self.konto,
|
||||
betrag=self.betrag,
|
||||
faellig_am=self.naechste_generierung,
|
||||
beschreibung=self.beschreibung or f"{self.get_intervall_display()} Unterstützung",
|
||||
empfaenger_iban=self.empfaenger_iban,
|
||||
empfaenger_name=self.empfaenger_name,
|
||||
verwendungszweck=self.verwendungszweck,
|
||||
wiederkehrend_von=self,
|
||||
status='geplant'
|
||||
)
|
||||
|
||||
# Calculate next generation date
|
||||
if self.intervall == 'monatlich':
|
||||
self.naechste_generierung = self.naechste_generierung + relativedelta(months=1)
|
||||
elif self.intervall == 'quartalsweise':
|
||||
self.naechste_generierung = self.naechste_generierung + relativedelta(months=3)
|
||||
elif self.intervall == 'halbjaehrlich':
|
||||
self.naechste_generierung = self.naechste_generierung + relativedelta(months=6)
|
||||
elif self.intervall == 'jaehrlich':
|
||||
self.naechste_generierung = self.naechste_generierung + relativedelta(years=1)
|
||||
|
||||
self.save()
|
||||
return neue_zahlung
|
||||
|
||||
|
||||
class DestinataerNotiz(models.Model):
|
||||
@@ -1665,3 +1877,153 @@ class BackupJob(models.Model):
|
||||
return f"{size:.1f} {unit}"
|
||||
size /= 1024
|
||||
return f"{size:.1f} TB"
|
||||
|
||||
|
||||
class AppConfiguration(models.Model):
|
||||
"""Application configuration settings that can be managed through the admin interface"""
|
||||
|
||||
SETTING_TYPE_CHOICES = [
|
||||
('text', 'Text'),
|
||||
('number', 'Number'),
|
||||
('boolean', 'Boolean'),
|
||||
('url', 'URL'),
|
||||
('tag', 'Tag Name'),
|
||||
('tag_id', 'Tag ID'),
|
||||
]
|
||||
|
||||
CATEGORY_CHOICES = [
|
||||
('paperless', 'Paperless Integration'),
|
||||
('general', 'General Settings'),
|
||||
('corporate', 'Corporate Identity'),
|
||||
('notifications', 'Notifications'),
|
||||
('system', 'System Settings'),
|
||||
]
|
||||
|
||||
id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
|
||||
key = models.CharField(max_length=100, unique=True, verbose_name="Setting Key")
|
||||
display_name = models.CharField(max_length=200, verbose_name="Display Name")
|
||||
description = models.TextField(blank=True, null=True, verbose_name="Description")
|
||||
value = models.TextField(verbose_name="Value")
|
||||
default_value = models.TextField(verbose_name="Default Value")
|
||||
setting_type = models.CharField(max_length=20, choices=SETTING_TYPE_CHOICES, default='text', verbose_name="Type")
|
||||
category = models.CharField(max_length=50, choices=CATEGORY_CHOICES, default='general', verbose_name="Category")
|
||||
is_active = models.BooleanField(default=True, verbose_name="Active")
|
||||
is_system = models.BooleanField(default=False, verbose_name="System Setting (read-only)")
|
||||
order = models.IntegerField(default=0, verbose_name="Display Order")
|
||||
|
||||
# Metadata
|
||||
created_at = models.DateTimeField(auto_now_add=True)
|
||||
updated_at = models.DateTimeField(auto_now=True)
|
||||
|
||||
class Meta:
|
||||
verbose_name = "App Configuration"
|
||||
verbose_name_plural = "App Configurations"
|
||||
ordering = ['category', 'order', 'display_name']
|
||||
|
||||
def __str__(self):
|
||||
return f"{self.display_name} ({self.key})"
|
||||
|
||||
def get_typed_value(self):
|
||||
"""Return the value converted to the appropriate type"""
|
||||
if self.setting_type == 'boolean':
|
||||
return self.value.lower() in ('true', '1', 'yes', 'on')
|
||||
elif self.setting_type == 'number':
|
||||
try:
|
||||
if '.' in self.value:
|
||||
return float(self.value)
|
||||
return int(self.value)
|
||||
except (ValueError, TypeError):
|
||||
return 0
|
||||
return self.value
|
||||
|
||||
@classmethod
|
||||
def get_setting(cls, key, default=None):
|
||||
"""Get a setting value by key"""
|
||||
try:
|
||||
setting = cls.objects.get(key=key, is_active=True)
|
||||
return setting.get_typed_value()
|
||||
except cls.DoesNotExist:
|
||||
return default
|
||||
|
||||
@classmethod
|
||||
def set_setting(cls, key, value, display_name=None, description=None, setting_type='text', category='general'):
|
||||
"""Set or update a setting value"""
|
||||
setting, created = cls.objects.get_or_create(
|
||||
key=key,
|
||||
defaults={
|
||||
'display_name': display_name or key,
|
||||
'description': description,
|
||||
'value': str(value),
|
||||
'default_value': str(value),
|
||||
'setting_type': setting_type,
|
||||
'category': category,
|
||||
}
|
||||
)
|
||||
if not created:
|
||||
setting.value = str(value)
|
||||
setting.save()
|
||||
return setting
|
||||
|
||||
|
||||
class HelpBox(models.Model):
|
||||
"""Editierbare Hilfe-Infoboxen für Formulare"""
|
||||
|
||||
PAGE_CHOICES = [
|
||||
('destinataer_new', 'Neuer Destinatär'),
|
||||
('unterstuetzung_new', 'Neue Unterstützung'),
|
||||
('foerderung_new', 'Neue Förderung'),
|
||||
('paechter_new', 'Neuer Pächter'),
|
||||
('laenderei_new', 'Neue Länderei'),
|
||||
('verpachtung_new', 'Neue Verpachtung'),
|
||||
('land_abrechnung_new', 'Neue Landabrechnung'),
|
||||
('person_new', 'Neue Person'),
|
||||
('konto_new', 'Neues Konto'),
|
||||
('verwaltungskosten_new', 'Neue Verwaltungskosten'),
|
||||
('rentmeister_new', 'Neuer Rentmeister'),
|
||||
('dokument_new', 'Neues Dokument'),
|
||||
('user_new', 'Neuer Benutzer'),
|
||||
('csv_import_new', 'CSV Import'),
|
||||
('destinataer_notiz_new', 'Destinatär Notiz'),
|
||||
]
|
||||
|
||||
id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
|
||||
page_key = models.CharField(
|
||||
max_length=50,
|
||||
choices=PAGE_CHOICES,
|
||||
unique=True,
|
||||
verbose_name="Seite"
|
||||
)
|
||||
title = models.CharField(
|
||||
max_length=200,
|
||||
verbose_name="Titel der Hilfsbox"
|
||||
)
|
||||
content = models.TextField(
|
||||
verbose_name="Inhalt (Markdown unterstützt)",
|
||||
help_text="Sie können Markdown verwenden: **fett**, *kursiv*, `code`, [Link](url), etc."
|
||||
)
|
||||
is_active = models.BooleanField(
|
||||
default=True,
|
||||
verbose_name="Aktiv"
|
||||
)
|
||||
|
||||
# Metadata
|
||||
created_at = models.DateTimeField(auto_now_add=True, verbose_name="Erstellt am")
|
||||
updated_at = models.DateTimeField(auto_now=True, verbose_name="Aktualisiert am")
|
||||
created_by = models.CharField(max_length=100, null=True, blank=True, verbose_name="Erstellt von")
|
||||
updated_by = models.CharField(max_length=100, null=True, blank=True, verbose_name="Aktualisiert von")
|
||||
|
||||
class Meta:
|
||||
verbose_name = "Hilfs-Infobox"
|
||||
verbose_name_plural = "Hilfs-Infoboxen"
|
||||
ordering = ['page_key']
|
||||
|
||||
def __str__(self):
|
||||
return f"{self.get_page_key_display()}: {self.title}"
|
||||
|
||||
@classmethod
|
||||
def get_help_for_page(cls, page_key):
|
||||
"""Hole die aktive Hilfs-Infobox für eine bestimmte Seite"""
|
||||
try:
|
||||
return cls.objects.get(page_key=page_key, is_active=True)
|
||||
except cls.DoesNotExist:
|
||||
return None
|
||||
|
||||
Reference in New Issue
Block a user