- 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
269 lines
11 KiB
Python
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}m²'
|
|
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) |