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:
2025-10-05 00:38:18 +02:00
parent 2961f376c3
commit c289cc3c58
36 changed files with 4039 additions and 99 deletions

View File

@@ -0,0 +1,58 @@
from django.core.management.base import BaseCommand
from stiftung.models import StiftungsKalenderEintrag
from datetime import date, timedelta
class Command(BaseCommand):
help = 'Creates sample calendar events for testing'
def handle(self, *args, **options):
today = date.today()
events = [
{
'titel': 'Vorstandssitzung',
'beschreibung': 'Monatliche Vorstandssitzung zur Besprechung aktueller Stiftungsangelegenheiten',
'datum': today + timedelta(days=7),
'kategorie': 'termin',
'prioritaet': 'hoch',
},
{
'titel': 'Zahlungserinnerung Familie Müller',
'beschreibung': 'Quartalsweise Unterstützung €500',
'datum': today + timedelta(days=3),
'kategorie': 'zahlung',
'prioritaet': 'kritisch',
},
{
'titel': 'Pachtvertrag Müller läuft aus',
'beschreibung': 'Vertrag für Grundstück A123 muss verlängert werden',
'datum': today + timedelta(days=30),
'kategorie': 'vertrag',
'prioritaet': 'hoch',
},
{
'titel': 'Geburtstag Maria Schmidt',
'beschreibung': '75. Geburtstag',
'datum': today + timedelta(days=5),
'kategorie': 'geburtstag',
'prioritaet': 'normal',
}
]
created_count = 0
for event_data in events:
event, created = StiftungsKalenderEintrag.objects.get_or_create(
titel=event_data['titel'],
defaults=event_data
)
if created:
created_count += 1
self.stdout.write(f'Created: {event.titel}')
else:
self.stdout.write(f'Already exists: {event.titel}')
total = StiftungsKalenderEintrag.objects.count()
self.stdout.write(
self.style.SUCCESS(f'✅ Created {created_count} new events. Total: {total}')
)

View File

@@ -0,0 +1,169 @@
"""
Management command to fix account balances for existing paid payments.
This command will:
1. Find all payments marked as 'ausgezahlt' (paid)
2. Check if corresponding bank transactions exist
3. Create missing bank transactions
4. Update account balances to reflect all paid payments
Usage:
python manage.py fix_account_balances
"""
from django.core.management.base import BaseCommand
from django.utils import timezone
from decimal import Decimal
from stiftung.models import DestinataerUnterstuetzung, BankTransaction, StiftungsKonto
class Command(BaseCommand):
help = 'Fix account balances for existing paid payments'
def add_arguments(self, parser):
parser.add_argument(
'--dry-run',
action='store_true',
help='Show what would be done without making changes',
)
parser.add_argument(
'--account',
type=str,
help='Only process payments for specific account (by kontoname)',
)
def handle(self, *args, **options):
dry_run = options['dry_run']
account_filter = options.get('account')
self.stdout.write(
self.style.SUCCESS('🔍 Analyzing paid payments and account balances...')
)
if dry_run:
self.stdout.write(
self.style.WARNING('DRY RUN MODE - No changes will be made')
)
# Get all paid payments
paid_payments_query = DestinataerUnterstuetzung.objects.filter(
status='ausgezahlt'
).select_related('konto', 'destinataer')
if account_filter:
paid_payments_query = paid_payments_query.filter(
konto__kontoname__icontains=account_filter
)
paid_payments = paid_payments_query.all()
self.stdout.write(f"Found {paid_payments.count()} paid payments")
# Group payments by account
accounts_data = {}
missing_transactions = []
for payment in paid_payments:
konto_id = payment.konto.id
if konto_id not in accounts_data:
accounts_data[konto_id] = {
'konto': payment.konto,
'payments': [],
'total_paid': Decimal('0.00'),
'missing_transactions': []
}
accounts_data[konto_id]['payments'].append(payment)
accounts_data[konto_id]['total_paid'] += payment.betrag
# Check if bank transaction exists for this payment
existing_transaction = BankTransaction.objects.filter(
konto=payment.konto,
betrag=-payment.betrag, # Negative for outgoing payment
kommentare__contains=f'Unterstützung {payment.id}'
).first()
if not existing_transaction:
accounts_data[konto_id]['missing_transactions'].append(payment)
missing_transactions.append(payment)
# Report findings
self.stdout.write("\n📊 ANALYSIS RESULTS:")
self.stdout.write("=" * 50)
for account_data in accounts_data.values():
konto = account_data['konto']
payments_count = len(account_data['payments'])
total_paid = account_data['total_paid']
missing_count = len(account_data['missing_transactions'])
self.stdout.write(f"\n🏦 {konto.bank_name} - {konto.kontoname}")
self.stdout.write(f" Current Balance: €{konto.saldo}")
self.stdout.write(f" Paid Payments: {payments_count} (Total: €{total_paid})")
self.stdout.write(f" Missing Transactions: {missing_count}")
if missing_count > 0:
expected_balance = konto.saldo - sum(p.betrag for p in account_data['missing_transactions'])
self.stdout.write(
self.style.WARNING(f" Expected Balance after fix: €{expected_balance}")
)
if not missing_transactions:
self.stdout.write(
self.style.SUCCESS("\n✅ All paid payments have corresponding transactions!")
)
return
self.stdout.write(f"\n⚠️ Found {len(missing_transactions)} payments without transactions")
if not dry_run:
self.stdout.write("\n🔧 CREATING MISSING TRANSACTIONS...")
created_count = 0
for payment in missing_transactions:
# Create bank transaction
transaction = BankTransaction.objects.create(
konto=payment.konto,
datum=payment.ausgezahlt_am or payment.faellig_am,
valuta=payment.ausgezahlt_am or payment.faellig_am,
betrag=-payment.betrag, # Negative for outgoing payment
waehrung='EUR',
verwendungszweck=f"Unterstützungszahlung: {payment.beschreibung or payment.destinataer.get_full_name()}",
empfaenger_zahlungspflichtiger=payment.empfaenger_name or payment.destinataer.get_full_name(),
iban_gegenpartei=payment.empfaenger_iban or '',
transaction_type='ueberweisung',
status='verified',
kommentare=f'Nachträglich erstellt für Unterstützung {payment.id} - Zahlung vom {payment.ausgezahlt_am or payment.faellig_am}',
)
# Update account balance
payment.konto.saldo -= payment.betrag
payment.konto.saldo_datum = payment.ausgezahlt_am or payment.faellig_am
payment.konto.save()
created_count += 1
self.stdout.write(
f" ✅ Created transaction for {payment.destinataer.get_full_name()}: €{payment.betrag}"
)
self.stdout.write(
self.style.SUCCESS(f"\n🎉 Successfully created {created_count} transactions and updated account balances!")
)
# Show final balances
self.stdout.write("\n📈 UPDATED ACCOUNT BALANCES:")
for account_data in accounts_data.values():
if account_data['missing_transactions']:
konto = account_data['konto']
konto.refresh_from_db() # Get updated balance
self.stdout.write(f" {konto.bank_name} - {konto.kontoname}: €{konto.saldo}")
else:
self.stdout.write("\n📝 DRY RUN - Would create the following transactions:")
for payment in missing_transactions:
self.stdout.write(
f" - {payment.destinataer.get_full_name()}: €{payment.betrag} "
f"on {payment.ausgezahlt_am or payment.faellig_am} "
f"from {payment.konto.kontoname}"
)

View File

@@ -0,0 +1,145 @@
"""
Management command to reconcile account balances with actual transactions.
This command will:
1. Calculate the correct balance based on all bank transactions
2. Update the account balance to match the calculated balance
3. Show discrepancies between stored and calculated balances
Usage:
python manage.py reconcile_balances
"""
from django.core.management.base import BaseCommand
from django.utils import timezone
from decimal import Decimal
from stiftung.models import StiftungsKonto, BankTransaction
class Command(BaseCommand):
help = 'Reconcile account balances with bank transactions'
def add_arguments(self, parser):
parser.add_argument(
'--dry-run',
action='store_true',
help='Show what would be done without making changes',
)
parser.add_argument(
'--account',
type=str,
help='Only process specific account (by kontoname)',
)
def handle(self, *args, **options):
dry_run = options['dry_run']
account_filter = options.get('account')
self.stdout.write(
self.style.SUCCESS('🔍 Reconciling account balances with transactions...')
)
if dry_run:
self.stdout.write(
self.style.WARNING('DRY RUN MODE - No changes will be made')
)
# Get accounts to process
accounts_query = StiftungsKonto.objects.filter(aktiv=True)
if account_filter:
accounts_query = accounts_query.filter(kontoname__icontains=account_filter)
accounts = accounts_query.all()
if not accounts:
self.stdout.write(
self.style.ERROR('No accounts found matching criteria')
)
return
self.stdout.write(f"Processing {accounts.count()} account(s)...")
total_discrepancies = 0
fixed_accounts = 0
for account in accounts:
self.stdout.write(f"\n🏦 {account.bank_name} - {account.kontoname}")
self.stdout.write(f" Stored Balance: €{account.saldo}")
self.stdout.write(f" Last Updated: {account.saldo_datum}")
# Calculate balance from transactions
transactions = BankTransaction.objects.filter(konto=account).order_by('datum')
calculated_balance = Decimal('0.00')
transaction_count = transactions.count()
for transaction in transactions:
calculated_balance += transaction.betrag
self.stdout.write(f" Transactions: {transaction_count}")
self.stdout.write(f" Calculated Balance: €{calculated_balance}")
discrepancy = account.saldo - calculated_balance
if discrepancy != 0:
total_discrepancies += 1
self.stdout.write(
self.style.WARNING(f" ⚠️ Discrepancy: €{discrepancy}")
)
if not dry_run:
# Update the account balance
old_balance = account.saldo
account.saldo = calculated_balance
# Update the balance date to the latest transaction date or today
if transactions.exists():
latest_transaction = transactions.order_by('-datum').first()
account.saldo_datum = latest_transaction.datum
else:
account.saldo_datum = timezone.now().date()
account.save()
fixed_accounts += 1
self.stdout.write(
self.style.SUCCESS(
f" ✅ Updated: €{old_balance} → €{calculated_balance}"
)
)
else:
self.stdout.write(
f" 📝 Would update: €{account.saldo} → €{calculated_balance}"
)
else:
self.stdout.write(
self.style.SUCCESS(" ✅ Balance is correct")
)
# Show recent transactions
if transaction_count > 0:
self.stdout.write(" Recent transactions:")
recent = transactions.order_by('-datum')[:3]
for trans in recent:
self.stdout.write(
f" {trans.datum}: €{trans.betrag} - {trans.verwendungszweck[:50]}"
)
# Summary
self.stdout.write("\n" + "=" * 50)
if dry_run:
self.stdout.write(
f"📊 Found {total_discrepancies} account(s) with balance discrepancies"
)
if total_discrepancies > 0:
self.stdout.write(" Run without --dry-run to fix these discrepancies")
else:
if fixed_accounts > 0:
self.stdout.write(
self.style.SUCCESS(
f"🎉 Fixed {fixed_accounts} account balance(s)"
)
)
else:
self.stdout.write(
self.style.SUCCESS("✅ All account balances were already correct")
)