Files
stiftung-management-system/app/stiftung/services/calendar_service.py
Jan Remmer Siebels c289cc3c58 Fix payment system balance integration and add calendar functionality
- Implement automated payment tracking with Django signals
- Fix duplicate transaction creation with unique referenz system
- Add calendar system with CRUD operations and event management
- Reorganize navigation menu (rename sections, move admin functions)
- Replace Geschichte editor with EasyMDE markdown editor
- Add management commands for balance reconciliation
- Create missing transactions for previously paid payments
- Ensure account balances accurately reflect all payment activity

Features added:
- Calendar entries creation and administration via menu
- Payment status tracking with automatic balance updates
- Duplicate prevention for payment transactions
- Markdown editor with live preview for Geschichte pages
- Database reconciliation tools for payment/balance sync

Bug fixes:
- Resolved IntegrityError on payment status changes
- Fixed missing account balance updates for paid payments
- Prevented duplicate balance deductions on re-saves
- Corrected menu structure and admin function placement
2025-10-05 00:38:18 +02:00

269 lines
11 KiB
Python

"""
Calendar service for aggregating date-based events from the foundation management system
"""
from datetime import date, timedelta
import calendar as cal
try:
from django.utils import timezone
except ImportError:
from datetime import datetime as timezone
timezone.now = datetime.now
try:
from stiftung.models import (
DestinataerUnterstuetzung,
LandVerpachtung,
Destinataer,
StiftungsKalenderEintrag
)
except ImportError:
# Fallback for when Django apps aren't ready
DestinataerUnterstuetzung = None
LandVerpachtung = None
Destinataer = None
StiftungsKalenderEintrag = None
class StiftungsKalenderService:
"""Service to aggregate calendar events from various models"""
def __init__(self):
self.today = timezone.now().date()
def get_calendar_events(self, start_date=None, end_date=None):
"""
Get custom calendar entries only within date range
Returns list of event objects
"""
if not start_date:
start_date = self.today
if not end_date:
end_date = start_date + timedelta(days=30)
return self._get_custom_calendar_events(start_date, end_date)
def get_all_events(self, start_date=None, end_date=None):
"""
Get all calendar events from all sources within date range
Returns list of event dictionaries
"""
if not start_date:
start_date = self.today
if not end_date:
end_date = start_date + timedelta(days=30)
events = []
# Add support payment due dates
events.extend(self.get_support_payment_events(start_date, end_date))
# Add lease expiration dates
events.extend(self.get_lease_events(start_date, end_date))
# Add birthdays
events.extend(self.get_birthday_events(start_date, end_date))
# Add custom calendar entries
events.extend(self.get_calendar_events(start_date, end_date))
# Sort by date
events.sort(key=lambda x: (x.date, getattr(x, 'time', '00:00')))
return events
def get_support_payment_events(self, start_date, end_date):
"""Get support payment due dates"""
if not DestinataerUnterstuetzung:
return []
events = []
payments = DestinataerUnterstuetzung.objects.filter(
faellig_am__range=[start_date, end_date],
status__in=['geplant', 'faellig']
).select_related('destinataer')
for payment in payments:
is_overdue = payment.is_overdue() if hasattr(payment, 'is_overdue') else False
class PaymentEvent:
def __init__(self, payment_obj, overdue):
self.id = f'payment_{payment_obj.id}'
self.title = f'Zahlung an {payment_obj.destinataer.get_full_name()}'
self.description = f'{payment_obj.betrag} - {payment_obj.beschreibung}'
self.date = payment_obj.faellig_am
self.time = None
self.category = 'zahlung'
self.category_display = 'Zahlung'
self.priority = 'hoch' if overdue else 'normal'
self.priority_display = 'Hoch' if overdue else 'Normal'
self.icon = 'fas fa-euro-sign'
self.color = 'danger' if overdue else 'warning'
self.source = 'payment'
self.overdue = overdue
self.completed = False
events.append(PaymentEvent(payment, is_overdue))
return events
def get_lease_events(self, start_date, end_date):
"""Get lease start/end dates"""
if not LandVerpachtung:
return []
events = []
# Lease expirations
leases = LandVerpachtung.objects.filter(
pachtende__range=[start_date, end_date],
status='aktiv'
).select_related('paechter', 'land')
for lease in leases:
# Check if expiring soon (within 90 days)
days_until_expiry = (lease.pachtende - self.today).days if lease.pachtende else 999
priority = 'hoch' if days_until_expiry <= 30 else 'mittel' if days_until_expiry <= 90 else 'normal'
class LeaseEvent:
def __init__(self, lease_obj, priority_val):
self.id = f'lease_end_{lease_obj.id}'
self.title = f'Pachtende: {lease_obj.paechter.get_full_name()}'
self.description = f'{lease_obj.land.bezeichnung} - {lease_obj.verpachtete_flaeche}'
self.date = lease_obj.pachtende
self.time = None
self.category = 'vertrag'
self.category_display = 'Vertrag'
self.priority = priority_val
self.priority_display = 'Hoch' if priority_val == 'hoch' else 'Mittel' if priority_val == 'mittel' else 'Normal'
self.icon = 'fas fa-file-contract'
self.color = 'danger' if priority_val == 'hoch' else 'warning' if priority_val == 'mittel' else 'info'
self.source = 'lease'
self.overdue = False
self.completed = False
events.append(LeaseEvent(lease, priority))
return events
def get_birthday_events(self, start_date, end_date):
"""Get birthdays within date range"""
if not Destinataer:
return []
events = []
# Get all destinatäre with birth dates
destinataere = Destinataer.objects.filter(
geburtsdatum__isnull=False
)
for destinataer in destinataere:
# Calculate birthday for each year in range
birth_date = destinataer.geburtsdatum
for year in range(start_date.year, end_date.year + 1):
try:
birthday_this_year = birth_date.replace(year=year)
if start_date <= birthday_this_year <= end_date:
age = year - birth_date.year
class BirthdayEvent:
def __init__(self, person, birthday_date, age_val):
self.id = f'birthday_{person.id}_{year}'
self.title = f'🎂 {person.get_full_name()}'
self.description = f'{age_val}. Geburtstag'
self.date = birthday_date
self.time = None
self.category = 'geburtstag'
self.category_display = 'Geburtstag'
self.priority = 'normal'
self.priority_display = 'Normal'
self.icon = 'fas fa-birthday-cake'
self.color = 'success'
self.source = 'birthday'
self.overdue = False
self.completed = False
events.append(BirthdayEvent(destinataer, birthday_this_year, age))
except ValueError:
# Handle leap year edge case (Feb 29)
pass
return events
def _get_custom_calendar_events(self, start_date, end_date):
"""Get custom calendar entries"""
if not StiftungsKalenderEintrag:
return []
events = []
calendar_entries = StiftungsKalenderEintrag.objects.filter(
datum__range=[start_date, end_date]
).select_related('destinataer', 'verpachtung')
for entry in calendar_entries:
class CustomEvent:
def __init__(self, entry_obj, today_date):
self.id = entry_obj.id
self.title = entry_obj.titel
self.description = entry_obj.beschreibung
self.date = entry_obj.datum
self.time = entry_obj.uhrzeit
self.category = entry_obj.kategorie
self.category_display = entry_obj.get_kategorie_display()
self.priority = entry_obj.prioritaet
self.priority_display = entry_obj.get_prioritaet_display()
self.icon = self._get_kategorie_icon(entry_obj.kategorie)
self.color = self._get_prioritaet_color(entry_obj.prioritaet)
self.source = 'custom'
self.completed = entry_obj.erledigt
self.overdue = entry_obj.datum < today_date if entry_obj.datum else False
def _get_kategorie_icon(self, kategorie):
icons = {
'termin': 'fas fa-calendar-alt',
'zahlung': 'fas fa-euro-sign',
'deadline': 'fas fa-exclamation-triangle',
'geburtstag': 'fas fa-birthday-cake',
'vertrag': 'fas fa-file-contract',
'pruefung': 'fas fa-search',
}
return icons.get(kategorie, 'fas fa-calendar')
def _get_prioritaet_color(self, prioritaet):
colors = {
'niedrig': 'secondary',
'normal': 'primary',
'mittel': 'warning',
'hoch': 'danger',
}
return colors.get(prioritaet, 'primary')
events.append(CustomEvent(entry, self.today))
return events
def get_upcoming_events(self, days=7):
"""Get upcoming events within specified days"""
end_date = self.today + timedelta(days=days)
return self.get_calendar_events(self.today, end_date)
def get_overdue_events(self):
"""Get overdue/past due events"""
events = self.get_calendar_events(
start_date=self.today - timedelta(days=30),
end_date=self.today - timedelta(days=1)
)
return [event for event in events if event.get('overdue', False)]
def get_events_for_month(self, year, month):
"""Get all events for a specific month"""
from calendar import monthrange
start_date = date(year, month, 1)
_, last_day = monthrange(year, month)
end_date = date(year, month, last_day)
return self.get_calendar_events(start_date, end_date)