Files
stiftung-management-system/app/stiftung/management/commands/generate_recurring_payments.py
Stiftung Development e0c7d0e351 Format code with Black and isort for CI/CD compliance
- Apply Black formatting to all Python files in core and stiftung modules
- Fix import statement ordering with isort
- Ensure all code meets automated quality standards
- Resolve CI/CD pipeline formatting failures
- Maintain consistent code style across the entire codebase
2025-09-06 21:04:07 +02:00

137 lines
5.0 KiB
Python

"""
Management command to generate due recurring support payments.
This command should be run daily via cron or similar scheduling system.
"""
import logging
from datetime import timedelta
from django.core.management.base import BaseCommand
from django.utils import timezone
from stiftung.models import UnterstuetzungWiederkehrend
logger = logging.getLogger(__name__)
class Command(BaseCommand):
help = "Generate due recurring support payments"
def add_arguments(self, parser):
parser.add_argument(
"--dry-run",
action="store_true",
help="Show what would be generated without actually creating payments",
)
parser.add_argument(
"--days-ahead",
type=int,
default=0,
help="Generate payments that are due within this many days (default: 0 = only today)",
)
def handle(self, *args, **options):
dry_run = options["dry_run"]
days_ahead = options["days_ahead"]
heute = timezone.now().date()
cutoff_date = heute + timedelta(days=days_ahead)
self.stdout.write(
self.style.SUCCESS(
f'Checking for recurring payments due up to {cutoff_date.strftime("%d.%m.%Y")}...'
)
)
if dry_run:
self.stdout.write(
self.style.WARNING("DRY RUN MODE - No payments will be created")
)
# Get all active recurring payment templates that are due
templates = UnterstuetzungWiederkehrend.objects.filter(
aktiv=True, naechste_generierung__lte=cutoff_date
).select_related("destinataer", "konto")
generated_count = 0
error_count = 0
for template in templates:
try:
if dry_run:
self.stdout.write(
f"Would generate: {template.destinataer.get_full_name()} - "
f'{template.betrag} due {template.naechste_generierung.strftime("%d.%m.%Y")}'
)
generated_count += 1
else:
# Actually generate the payment
neue_zahlung = template.generiere_naechste_zahlung()
if neue_zahlung:
self.stdout.write(
self.style.SUCCESS(
f"Generated: {neue_zahlung.destinataer.get_full_name()} - "
f'{neue_zahlung.betrag} due {neue_zahlung.faellig_am.strftime("%d.%m.%Y")}'
)
)
generated_count += 1
logger.info(f"Generated recurring payment: {neue_zahlung.pk}")
else:
self.stdout.write(
self.style.WARNING(
f"No payment generated for {template.destinataer.get_full_name()} "
f"(may have reached end date or not yet due)"
)
)
except Exception as e:
error_count += 1
self.stdout.write(
self.style.ERROR(
f"Error generating payment for {template.destinataer.get_full_name()}: {str(e)}"
)
)
logger.error(
f"Error generating recurring payment for template {template.pk}: {str(e)}"
)
# Summary
self.stdout.write("\n" + "=" * 50)
if dry_run:
self.stdout.write(
self.style.SUCCESS(
f"DRY RUN COMPLETE: {generated_count} payments would be generated"
)
)
else:
self.stdout.write(
self.style.SUCCESS(
f"GENERATION COMPLETE: {generated_count} payments generated"
)
)
if error_count > 0:
self.stdout.write(self.style.ERROR(f"{error_count} errors encountered"))
# Also check for overdue payments and report them
from stiftung.models import DestinataerUnterstuetzung
overdue_payments = DestinataerUnterstuetzung.objects.filter(
faellig_am__lt=heute, status__in=["geplant", "faellig"]
).select_related("destinataer")
if overdue_payments.exists():
self.stdout.write("\n" + "=" * 50)
self.stdout.write(
self.style.WARNING(
f"WARNING: {overdue_payments.count()} overdue payments found:"
)
)
for payment in overdue_payments[:10]: # Limit to first 10
days_overdue = (heute - payment.faellig_am).days
self.stdout.write(
f" - {payment.destinataer.get_full_name()}: €{payment.betrag} "
f"({days_overdue} days overdue)"
)
if overdue_payments.count() > 10:
self.stdout.write(f" ... and {overdue_payments.count() - 10} more")