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:
58
app/stiftung/management/commands/create_sample_events.py
Normal file
58
app/stiftung/management/commands/create_sample_events.py
Normal 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}')
|
||||
)
|
||||
169
app/stiftung/management/commands/fix_account_balances.py
Normal file
169
app/stiftung/management/commands/fix_account_balances.py
Normal 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}"
|
||||
)
|
||||
145
app/stiftung/management/commands/reconcile_balances.py
Normal file
145
app/stiftung/management/commands/reconcile_balances.py
Normal 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")
|
||||
)
|
||||
Reference in New Issue
Block a user