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
This commit is contained in:
269
app/stiftung/services/calendar_service.py
Normal file
269
app/stiftung/services/calendar_service.py
Normal file
@@ -0,0 +1,269 @@
|
||||
"""
|
||||
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)
|
||||
Reference in New Issue
Block a user