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:
@@ -3,7 +3,7 @@ import io
|
||||
import json
|
||||
import os
|
||||
import time
|
||||
from datetime import datetime, timedelta
|
||||
from datetime import datetime, timedelta, date
|
||||
from decimal import Decimal
|
||||
|
||||
import qrcode
|
||||
@@ -225,11 +225,38 @@ from .forms import (DestinataerForm, DestinataerNotizForm,
|
||||
|
||||
def home(request):
|
||||
"""Home page for the Stiftungsverwaltung application"""
|
||||
return render(
|
||||
request,
|
||||
"stiftung/home.html",
|
||||
{"title": "Stiftungsverwaltung", "description": "Foundation Management System"},
|
||||
)
|
||||
from stiftung.services.calendar_service import StiftungsKalenderService
|
||||
|
||||
# Get upcoming events for the calendar widget
|
||||
calendar_service = StiftungsKalenderService()
|
||||
|
||||
# Get all events for the next 14 days
|
||||
from datetime import timedelta
|
||||
today = timezone.now().date()
|
||||
end_date = today + timedelta(days=14)
|
||||
all_events = calendar_service.get_all_events(today, end_date)
|
||||
|
||||
# Filter for upcoming and overdue
|
||||
upcoming_events = [e for e in all_events if not getattr(e, 'overdue', False)]
|
||||
overdue_events = [e for e in all_events if getattr(e, 'overdue', False)]
|
||||
|
||||
# Get current month events for mini calendar
|
||||
from calendar import monthrange
|
||||
_, last_day = monthrange(today.year, today.month)
|
||||
month_start = today.replace(day=1)
|
||||
month_end = today.replace(day=last_day)
|
||||
current_month_events = calendar_service.get_all_events(month_start, month_end)
|
||||
|
||||
context = {
|
||||
"title": "Stiftungsverwaltung",
|
||||
"description": "Foundation Management System",
|
||||
"upcoming_events": upcoming_events[:5], # Show only 5 upcoming events
|
||||
"overdue_events": overdue_events[:3], # Show only 3 overdue events
|
||||
"current_month_events": current_month_events,
|
||||
"today": today,
|
||||
}
|
||||
|
||||
return render(request, "stiftung/home.html", context)
|
||||
|
||||
|
||||
@login_required
|
||||
@@ -8152,3 +8179,412 @@ def geschichte_bild_upload(request, slug):
|
||||
}
|
||||
|
||||
return render(request, 'stiftung/geschichte/bild_form.html', context)
|
||||
|
||||
|
||||
@login_required
|
||||
def geschichte_bild_delete(request, slug, bild_id):
|
||||
"""Delete an image from a history page"""
|
||||
seite = get_object_or_404(GeschichteSeite, slug=slug)
|
||||
bild = get_object_or_404(GeschichteBild, id=bild_id, seite=seite)
|
||||
|
||||
if not request.user.has_perm('stiftung.delete_geschichtebild'):
|
||||
messages.error(request, 'Sie haben keine Berechtigung, Bilder zu löschen.')
|
||||
return redirect('stiftung:geschichte_detail', slug=slug)
|
||||
|
||||
if request.method == 'POST':
|
||||
bild_titel = bild.titel
|
||||
bild.delete()
|
||||
messages.success(request, f'Bild "{bild_titel}" wurde erfolgreich gelöscht.')
|
||||
return redirect('stiftung:geschichte_detail', slug=slug)
|
||||
|
||||
context = {
|
||||
'bild': bild,
|
||||
'seite': seite,
|
||||
'title': f'Bild löschen: {bild.titel}'
|
||||
}
|
||||
|
||||
return render(request, 'stiftung/geschichte/bild_delete.html', context)
|
||||
|
||||
|
||||
# Calendar Views
|
||||
@login_required
|
||||
def kalender_view(request):
|
||||
"""Main calendar view with different view types"""
|
||||
from stiftung.services.calendar_service import StiftungsKalenderService
|
||||
import calendar as cal
|
||||
|
||||
calendar_service = StiftungsKalenderService()
|
||||
|
||||
# Get current date and view parameters
|
||||
today = timezone.now().date()
|
||||
view_type = request.GET.get('view', 'month') # month, week, list, agenda
|
||||
year = int(request.GET.get('year', today.year))
|
||||
month = int(request.GET.get('month', today.month))
|
||||
|
||||
# Calculate date ranges based on view type
|
||||
if view_type == 'month':
|
||||
# Get events for the entire month
|
||||
start_date = date(year, month, 1)
|
||||
_, last_day = cal.monthrange(year, month)
|
||||
end_date = date(year, month, last_day)
|
||||
title_suffix = f"{cal.month_name[month]} {year}"
|
||||
|
||||
elif view_type == 'week':
|
||||
# Get current week
|
||||
week_start = today - timedelta(days=today.weekday())
|
||||
start_date = week_start
|
||||
end_date = week_start + timedelta(days=6)
|
||||
title_suffix = f"Woche vom {start_date.strftime('%d.%m')} - {end_date.strftime('%d.%m.%Y')}"
|
||||
|
||||
elif view_type == 'agenda':
|
||||
# Next 30 days
|
||||
start_date = today
|
||||
end_date = today + timedelta(days=30)
|
||||
title_suffix = "Nächste 30 Tage"
|
||||
|
||||
else: # list view
|
||||
# Next 90 days
|
||||
start_date = today
|
||||
end_date = today + timedelta(days=90)
|
||||
title_suffix = "Liste (nächste 90 Tage)"
|
||||
|
||||
# Get events for the date range
|
||||
events = calendar_service.get_all_events(start_date, end_date)
|
||||
|
||||
# Generate calendar grid for month view
|
||||
calendar_grid = None
|
||||
if view_type == 'month':
|
||||
calendar_grid = []
|
||||
first_day = date(year, month, 1)
|
||||
month_cal = cal.monthcalendar(year, month)
|
||||
|
||||
for week in month_cal:
|
||||
week_data = []
|
||||
for day in week:
|
||||
if day == 0:
|
||||
week_data.append(None)
|
||||
else:
|
||||
day_date = date(year, month, day)
|
||||
day_events = [e for e in events if e.date == day_date]
|
||||
week_data.append({
|
||||
'day': day,
|
||||
'date': day_date,
|
||||
'is_today': day_date == today,
|
||||
'events': day_events[:3], # Show max 3 events per day
|
||||
'event_count': len(day_events)
|
||||
})
|
||||
calendar_grid.append(week_data)
|
||||
|
||||
# Navigation dates for month view
|
||||
if month > 1:
|
||||
prev_month = month - 1
|
||||
prev_year = year
|
||||
else:
|
||||
prev_month = 12
|
||||
prev_year = year - 1
|
||||
|
||||
if month < 12:
|
||||
next_month = month + 1
|
||||
next_year = year
|
||||
else:
|
||||
next_month = 1
|
||||
next_year = year + 1
|
||||
|
||||
context = {
|
||||
'title': f'Kalender - {title_suffix}',
|
||||
'events': events,
|
||||
'calendar_grid': calendar_grid,
|
||||
'view_type': view_type,
|
||||
'year': year,
|
||||
'month': month,
|
||||
'today': today,
|
||||
'start_date': start_date,
|
||||
'end_date': end_date,
|
||||
'prev_year': prev_year,
|
||||
'prev_month': prev_month,
|
||||
'next_year': next_year,
|
||||
'next_month': next_month,
|
||||
'month_name': cal.month_name[month],
|
||||
'weekdays': ['Mo', 'Di', 'Mi', 'Do', 'Fr', 'Sa', 'So'],
|
||||
}
|
||||
|
||||
# Choose template based on view type
|
||||
if view_type == 'month':
|
||||
template = 'stiftung/kalender/month_view.html'
|
||||
elif view_type == 'week':
|
||||
template = 'stiftung/kalender/week_view.html'
|
||||
elif view_type == 'agenda':
|
||||
template = 'stiftung/kalender/agenda_view.html'
|
||||
else:
|
||||
template = 'stiftung/kalender/list_view.html'
|
||||
|
||||
return render(request, template, context)
|
||||
|
||||
|
||||
@login_required
|
||||
def kalender_create(request):
|
||||
"""Create new calendar event"""
|
||||
from stiftung.models import StiftungsKalenderEintrag
|
||||
|
||||
if request.method == 'POST':
|
||||
# Simple form handling - you can enhance this with Django forms
|
||||
titel = request.POST.get('titel')
|
||||
beschreibung = request.POST.get('beschreibung', '')
|
||||
datum = request.POST.get('datum')
|
||||
kategorie = request.POST.get('kategorie', 'termin')
|
||||
prioritaet = request.POST.get('prioritaet', 'normal')
|
||||
|
||||
if titel and datum:
|
||||
zeit_str = request.POST.get('zeit')
|
||||
uhrzeit = zeit_str if zeit_str else None
|
||||
ganztags = not bool(zeit_str)
|
||||
|
||||
StiftungsKalenderEintrag.objects.create(
|
||||
titel=titel,
|
||||
beschreibung=beschreibung,
|
||||
datum=datum,
|
||||
uhrzeit=uhrzeit,
|
||||
ganztags=ganztags,
|
||||
kategorie=kategorie,
|
||||
prioritaet=prioritaet,
|
||||
erstellt_von=request.user.username
|
||||
)
|
||||
messages.success(request, 'Kalendereintrag wurde erfolgreich erstellt.')
|
||||
return redirect('stiftung:kalender')
|
||||
else:
|
||||
messages.error(request, 'Titel und Datum sind erforderlich.')
|
||||
|
||||
context = {
|
||||
'title': 'Neuer Kalendereintrag',
|
||||
}
|
||||
|
||||
return render(request, 'stiftung/kalender/create.html', context)
|
||||
|
||||
|
||||
@login_required
|
||||
def kalender_detail(request, pk):
|
||||
"""Calendar event detail view"""
|
||||
from stiftung.models import StiftungsKalenderEintrag
|
||||
|
||||
event = get_object_or_404(StiftungsKalenderEintrag, pk=pk)
|
||||
|
||||
context = {
|
||||
'title': f'Kalendereintrag: {event.titel}',
|
||||
'event': event,
|
||||
}
|
||||
|
||||
return render(request, 'stiftung/kalender/detail.html', context)
|
||||
|
||||
|
||||
@login_required
|
||||
def kalender_edit(request, pk):
|
||||
"""Edit calendar event"""
|
||||
from stiftung.models import StiftungsKalenderEintrag
|
||||
|
||||
event = get_object_or_404(StiftungsKalenderEintrag, pk=pk)
|
||||
|
||||
if request.method == 'POST':
|
||||
event.titel = request.POST.get('titel', event.titel)
|
||||
event.beschreibung = request.POST.get('beschreibung', event.beschreibung)
|
||||
event.datum = request.POST.get('datum', event.datum)
|
||||
zeit_str = request.POST.get('zeit')
|
||||
if zeit_str:
|
||||
event.uhrzeit = zeit_str
|
||||
event.ganztags = False
|
||||
else:
|
||||
event.uhrzeit = None
|
||||
event.ganztags = True
|
||||
event.kategorie = request.POST.get('kategorie', event.kategorie)
|
||||
event.prioritaet = request.POST.get('prioritaet', event.prioritaet)
|
||||
event.erledigt = 'erledigt' in request.POST
|
||||
|
||||
event.save()
|
||||
messages.success(request, 'Kalendereintrag wurde aktualisiert.')
|
||||
return redirect('stiftung:kalender_detail', pk=pk)
|
||||
|
||||
context = {
|
||||
'title': f'Bearbeiten: {event.titel}',
|
||||
'event': event,
|
||||
}
|
||||
|
||||
return render(request, 'stiftung/kalender/edit.html', context)
|
||||
|
||||
|
||||
@login_required
|
||||
def kalender_delete(request, pk):
|
||||
"""Delete calendar event"""
|
||||
from stiftung.models import StiftungsKalenderEintrag
|
||||
|
||||
event = get_object_or_404(StiftungsKalenderEintrag, pk=pk)
|
||||
|
||||
if request.method == 'POST':
|
||||
event_titel = event.titel
|
||||
event.delete()
|
||||
messages.success(request, f'Kalendereintrag "{event_titel}" wurde gelöscht.')
|
||||
return redirect('stiftung:kalender')
|
||||
|
||||
context = {
|
||||
'title': f'Löschen: {event.titel}',
|
||||
'event': event,
|
||||
}
|
||||
|
||||
return render(request, 'stiftung/kalender/delete_confirm.html', context)
|
||||
|
||||
|
||||
@login_required
|
||||
def kalender_admin(request):
|
||||
"""Calendar administration with event sources and management"""
|
||||
from stiftung.models import StiftungsKalenderEintrag
|
||||
from stiftung.services.calendar_service import StiftungsKalenderService
|
||||
|
||||
# Get filter parameters
|
||||
show_custom = request.GET.get('show_custom', 'true') == 'true'
|
||||
show_payments = request.GET.get('show_payments', 'true') == 'true'
|
||||
show_leases = request.GET.get('show_leases', 'true') == 'true'
|
||||
show_birthdays = request.GET.get('show_birthdays', 'true') == 'true'
|
||||
category_filter = request.GET.get('category', '')
|
||||
priority_filter = request.GET.get('priority', '')
|
||||
|
||||
# Initialize calendar service
|
||||
calendar_service = StiftungsKalenderService()
|
||||
|
||||
# Get events based on filters
|
||||
from datetime import date, timedelta
|
||||
start_date = date.today() - timedelta(days=30)
|
||||
end_date = date.today() + timedelta(days=90)
|
||||
|
||||
all_events = []
|
||||
|
||||
# Custom calendar entries
|
||||
if show_custom:
|
||||
custom_events = calendar_service.get_calendar_events(start_date, end_date)
|
||||
all_events.extend(custom_events)
|
||||
|
||||
# Payment events
|
||||
if show_payments:
|
||||
payment_events = calendar_service.get_support_payment_events(start_date, end_date)
|
||||
all_events.extend(payment_events)
|
||||
|
||||
# Lease events
|
||||
if show_leases:
|
||||
lease_events = calendar_service.get_lease_events(start_date, end_date)
|
||||
all_events.extend(lease_events)
|
||||
|
||||
# Birthday events
|
||||
if show_birthdays:
|
||||
birthday_events = calendar_service.get_birthday_events(start_date, end_date)
|
||||
all_events.extend(birthday_events)
|
||||
|
||||
# Filter by category and priority if specified
|
||||
if category_filter:
|
||||
all_events = [e for e in all_events if getattr(e, 'category', '') == category_filter]
|
||||
|
||||
if priority_filter:
|
||||
all_events = [e for e in all_events if getattr(e, 'priority', '') == priority_filter]
|
||||
|
||||
# Sort events by date
|
||||
all_events.sort(key=lambda x: x.date)
|
||||
|
||||
# Get statistics
|
||||
custom_count = StiftungsKalenderEintrag.objects.count()
|
||||
total_events = len(all_events)
|
||||
|
||||
# Event source statistics
|
||||
stats = {
|
||||
'custom_events': len([e for e in all_events if getattr(e, 'source', '') == 'custom']),
|
||||
'payment_events': len([e for e in all_events if getattr(e, 'source', '') == 'payment']),
|
||||
'lease_events': len([e for e in all_events if getattr(e, 'source', '') == 'lease']),
|
||||
'birthday_events': len([e for e in all_events if getattr(e, 'source', '') == 'birthday']),
|
||||
'total_events': total_events,
|
||||
'custom_count': custom_count,
|
||||
}
|
||||
|
||||
context = {
|
||||
'title': 'Kalender Administration',
|
||||
'events': all_events,
|
||||
'stats': stats,
|
||||
'show_custom': show_custom,
|
||||
'show_payments': show_payments,
|
||||
'show_leases': show_leases,
|
||||
'show_birthdays': show_birthdays,
|
||||
'category_filter': category_filter,
|
||||
'priority_filter': priority_filter,
|
||||
'categories': StiftungsKalenderEintrag.KATEGORIE_CHOICES,
|
||||
'priorities': StiftungsKalenderEintrag.PRIORITAET_CHOICES,
|
||||
}
|
||||
|
||||
return render(request, 'stiftung/kalender/admin.html', context)
|
||||
|
||||
context = {
|
||||
'title': f'Löschen: {event.titel}',
|
||||
'event': event,
|
||||
}
|
||||
|
||||
return render(request, 'stiftung/kalender/delete.html', context)
|
||||
|
||||
|
||||
@login_required
|
||||
def kalender_api_events(request):
|
||||
"""API endpoint for calendar events (JSON)"""
|
||||
from django.http import JsonResponse
|
||||
from stiftung.services.calendar_service import StiftungsKalenderService
|
||||
from datetime import datetime
|
||||
|
||||
calendar_service = StiftungsKalenderService()
|
||||
|
||||
# Get date range from request
|
||||
start_date = request.GET.get('start')
|
||||
end_date = request.GET.get('end')
|
||||
|
||||
if start_date and end_date:
|
||||
try:
|
||||
start_date = datetime.strptime(start_date, '%Y-%m-%d').date()
|
||||
end_date = datetime.strptime(end_date, '%Y-%m-%d').date()
|
||||
except ValueError:
|
||||
return JsonResponse({'error': 'Invalid date format'}, status=400)
|
||||
|
||||
events = calendar_service.get_all_events(start_date, end_date)
|
||||
else:
|
||||
events = calendar_service.get_all_events()
|
||||
|
||||
# Convert to FullCalendar format
|
||||
calendar_events = []
|
||||
for event in events:
|
||||
calendar_events.append({
|
||||
'id': getattr(event, 'id', str(event.title)),
|
||||
'title': event.title,
|
||||
'start': event.date.strftime('%Y-%m-%d'),
|
||||
'description': getattr(event, 'description', ''),
|
||||
'className': f"event-{event.category}",
|
||||
'backgroundColor': f"var(--bs-{event.color})",
|
||||
'borderColor': f"var(--bs-{event.color})",
|
||||
})
|
||||
|
||||
return JsonResponse(calendar_events, safe=False)
|
||||
|
||||
|
||||
# Calendar Views
|
||||
@login_required
|
||||
def kalender_view(request):
|
||||
"""Full calendar view with all events"""
|
||||
from stiftung.services.calendar_service import StiftungsKalenderService
|
||||
|
||||
calendar_service = StiftungsKalenderService()
|
||||
|
||||
# Get current month events by default
|
||||
today = timezone.now().date()
|
||||
events = calendar_service.get_events_for_month(today.year, today.month)
|
||||
|
||||
context = {
|
||||
'events': events,
|
||||
'title': 'Stiftungskalender',
|
||||
'current_month': today.strftime('%B %Y'),
|
||||
}
|
||||
|
||||
return render(request, 'stiftung/kalender/kalender.html', context)
|
||||
|
||||
|
||||
context = {
|
||||
'title': 'Kalendereintrag löschen'
|
||||
}
|
||||
return render(request, 'stiftung/kalender/delete.html', context)
|
||||
|
||||
Reference in New Issue
Block a user