Implement advance payment schedule for quarterly payments

BREAKING CHANGE: Payment due dates now follow advance payment schedule:
- Q1 payments: Due December 15 of previous year
- Q2 payments: Due March 15 of same year
- Q3 payments: Due June 15 of same year
- Q4 payments: Due September 15 of same year

Changes:
- Updated create_quarterly_support_payment() to use advance schedule
- Enhanced diagnostic commands to check advance payment dates
- Added fix_advance_payment_dates command for comprehensive fixes
- Updated fix_q4_payment_dates to correct Q4 from 31.12 to 15.09

This aligns with semester-based document submissions while maintaining
the advance payment system where payments are made before each quarter.
This commit is contained in:
2025-10-01 12:04:34 +02:00
parent 149078aaee
commit 6aa218004c
4 changed files with 143 additions and 26 deletions

View File

@@ -89,14 +89,29 @@ class Command(BaseCommand):
else: else:
self.stdout.write(' ❌ Run: python manage.py update_semester_deadlines') self.stdout.write(' ❌ Run: python manage.py update_semester_deadlines')
# Check if payments need updating # Check if payments need updating (advance payment schedule)
wrong_payments = DestinataerUnterstuetzung.objects.filter( wrong_q4_payments = DestinataerUnterstuetzung.objects.filter(
faellig_am='2025-12-31', faellig_am='2025-12-31',
beschreibung__icontains='Q4/2025' beschreibung__icontains='Q4/2025'
).count() ).count()
if wrong_payments > 0: # Also check for other quarters with wrong advance payment dates
self.stdout.write(f'{wrong_payments} payments have wrong due date (31.12 instead of 15.12)') wrong_q1_payments = DestinataerUnterstuetzung.objects.filter(
faellig_am='2025-03-31',
beschreibung__icontains='Q1/2025'
).count()
if wrong_q4_payments > 0:
self.stdout.write(f'{wrong_q4_payments} Q4 payments have wrong due date (31.12 instead of 15.09)')
self.stdout.write(' Run: python manage.py fix_q4_payment_dates') self.stdout.write(' Run: python manage.py fix_q4_payment_dates')
elif wrong_q1_payments > 0:
self.stdout.write(f'{wrong_q1_payments} Q1 payments have wrong due date (advance payment schedule)')
else: else:
self.stdout.write(' ✓ Payment due dates appear correct') self.stdout.write(' ✓ Payment due dates appear correct')
# Show the advance payment schedule
self.stdout.write('\nAdvance Payment Schedule:')
self.stdout.write(' Q1 payments: Due December 15 of previous year')
self.stdout.write(' Q2 payments: Due March 15 of same year')
self.stdout.write(' Q3 payments: Due June 15 of same year')
self.stdout.write(' Q4 payments: Due September 15 of same year')

View File

@@ -0,0 +1,99 @@
from django.core.management.base import BaseCommand
from django.db import transaction
from datetime import date
from stiftung.models import DestinataerUnterstuetzung
class Command(BaseCommand):
help = 'Fix all quarterly payment dates to use advance payment schedule'
def add_arguments(self, parser):
parser.add_argument(
'--dry-run',
action='store_true',
help='Show what would be updated without making changes',
)
parser.add_argument(
'--year',
type=int,
default=2025,
help='Year to fix payments for (default: 2025)',
)
def handle(self, *args, **options):
dry_run = options['dry_run']
year = options['year']
if dry_run:
self.stdout.write(self.style.WARNING('DRY RUN MODE - No changes will be made'))
self.stdout.write(f'Checking quarterly payments for {year} using advance payment schedule...')
# Define correct advance payment schedule
advance_schedule = {
f'Q1/{year}': date(year - 1, 12, 15), # Q1 paid in December of previous year
f'Q2/{year}': date(year, 3, 15), # Q2 paid in March of same year
f'Q3/{year}': date(year, 6, 15), # Q3 paid in June of same year
f'Q4/{year}': date(year, 9, 15), # Q4 paid in September of same year
}
total_fixed = 0
for quarter_desc, correct_date in advance_schedule.items():
# Find payments for this quarter that have wrong dates
wrong_payments = DestinataerUnterstuetzung.objects.filter(
beschreibung__icontains=quarter_desc
).exclude(faellig_am=correct_date)
quarter_count = wrong_payments.count()
if quarter_count > 0:
self.stdout.write(f'\n{quarter_desc}: Found {quarter_count} payments with wrong due dates')
with transaction.atomic():
for payment in wrong_payments:
old_date = payment.faellig_am
if not dry_run:
payment.faellig_am = correct_date
payment.save(update_fields=['faellig_am'])
total_fixed += 1
# Show some examples
if total_fixed <= 10 or options['verbosity'] >= 2:
self.stdout.write(
f' {payment.destinataer.get_full_name()}: '
f'{old_date}{correct_date} ({payment.betrag}€)'
)
else:
self.stdout.write(f'{quarter_desc}: ✓ All payments have correct due date ({correct_date})')
# Summary
if total_fixed == 0:
self.stdout.write(self.style.SUCCESS(f'\n✓ All {year} quarterly payments already have correct advance payment dates!'))
elif dry_run:
self.stdout.write(
self.style.WARNING(f'\nDRY RUN: Would fix {total_fixed} payment due dates')
)
else:
self.stdout.write(
self.style.SUCCESS(f'\n✅ Successfully fixed {total_fixed} payment due dates')
)
# Show the advance payment schedule
self.stdout.write(f'\nAdvance Payment Schedule for {year}:')
for quarter_desc, due_date in advance_schedule.items():
self.stdout.write(f' {quarter_desc}: Due {due_date.strftime("%B %d, %Y")}')
# Check for other years that might need fixing
if year == 2025:
other_year_payments = DestinataerUnterstuetzung.objects.filter(
beschreibung__regex=r'Q[1-4]/202[6-9]'
).exclude(
faellig_am__in=[date(2024, 12, 15), date(2026, 3, 15), date(2026, 6, 15), date(2026, 9, 15)]
).count()
if other_year_payments > 0:
self.stdout.write(f'\n⚠️ Found {other_year_payments} payments for other years that may need fixing')
self.stdout.write(' Run this command with --year 2026, etc. to fix other years')

View File

@@ -20,7 +20,7 @@ class Command(BaseCommand):
if dry_run: if dry_run:
self.stdout.write(self.style.WARNING('DRY RUN MODE - No changes will be made')) self.stdout.write(self.style.WARNING('DRY RUN MODE - No changes will be made'))
# Find all Q4/2025 payments with wrong due date (Dec 31 instead of Dec 15) # Find all Q4/2025 payments with wrong due date (Dec 31 instead of Sep 15)
wrong_payments = DestinataerUnterstuetzung.objects.filter( wrong_payments = DestinataerUnterstuetzung.objects.filter(
faellig_am='2025-12-31', faellig_am='2025-12-31',
beschreibung__icontains='Q4/2025' beschreibung__icontains='Q4/2025'
@@ -34,7 +34,7 @@ class Command(BaseCommand):
return return
updated_count = 0 updated_count = 0
correct_date = date(2025, 12, 15) # December 15, 2025 correct_date = date(2025, 9, 15) # September 15, 2025 (advance payment for Q4)
with transaction.atomic(): with transaction.atomic():
for payment in wrong_payments: for payment in wrong_payments:
@@ -57,13 +57,13 @@ class Command(BaseCommand):
if dry_run: if dry_run:
self.stdout.write( self.stdout.write(
self.style.SUCCESS( self.style.SUCCESS(
f'DRY RUN: Would update {updated_count} payment due dates from 31.12.2025 to 15.12.2025' f'DRY RUN: Would update {updated_count} payment due dates from 31.12.2025 to 15.09.2025'
) )
) )
else: else:
self.stdout.write( self.stdout.write(
self.style.SUCCESS( self.style.SUCCESS(
f'Successfully updated {updated_count} payment due dates to December 15, 2025' f'Successfully updated {updated_count} payment due dates to September 15, 2025'
) )
) )
@@ -86,8 +86,8 @@ class Command(BaseCommand):
self.stdout.write(f' ⚠️ {quarter_desc}: {old_date_payments} payments still use old date {old_expected}') self.stdout.write(f' ⚠️ {quarter_desc}: {old_date_payments} payments still use old date {old_expected}')
self.stdout.write(f' Should be: {new_expected}') self.stdout.write(f' Should be: {new_expected}')
self.stdout.write('\nSemester-based payment schedule:') self.stdout.write('\nAdvance payment schedule (payments made in advance):')
self.stdout.write(' Q1: March 15 (Spring semester)') self.stdout.write(' Q1 payments: Due December 15 of previous year')
self.stdout.write(' Q2: June 15 (Auto-approved with Q1)') self.stdout.write(' Q2 payments: Due March 15 of same year')
self.stdout.write(' Q3: September 15 (Fall semester)') self.stdout.write(' Q3 payments: Due June 15 of same year')
self.stdout.write(' Q4: December 15 (Auto-approved with Q3)') self.stdout.write(' Q4 payments: Due September 15 of same year')

View File

@@ -7484,6 +7484,7 @@ def create_quarterly_support_payment(nachweis):
Get or create a single support payment for this quarterly confirmation Get or create a single support payment for this quarterly confirmation
Ensures only one payment exists per destinataer per quarter Ensures only one payment exists per destinataer per quarter
""" """
from datetime import date
destinataer = nachweis.destinataer destinataer = nachweis.destinataer
# Check if all requirements are met # Check if all requirements are met
@@ -7532,19 +7533,21 @@ def create_quarterly_support_payment(nachweis):
if not default_konto: if not default_konto:
return None return None
# Calculate payment due date (last day of quarter) # Calculate payment due date (advance payment schedule)
quarter_end_month = nachweis.quartal * 3 # Payments are made in advance according to semester schedule:
# September 15: Payment for Q4 of current year
# December 15: Payment for Q1 of next year
# March 15: Payment for Q2 of current year
# June 15: Payment for Q3 of current year
if nachweis.quartal == 1: # Q1: January-March (ends March 31) if nachweis.quartal == 1: # Q1 payment due December 15 of previous year
quarter_end_day = 31 payment_due_date = date(nachweis.jahr - 1, 12, 15)
elif nachweis.quartal == 2: # Q2: April-June (ends June 30) elif nachweis.quartal == 2: # Q2 payment due March 15 of same year
quarter_end_day = 30 payment_due_date = date(nachweis.jahr, 3, 15)
elif nachweis.quartal == 3: # Q3: July-September (ends September 30) elif nachweis.quartal == 3: # Q3 payment due June 15 of same year
quarter_end_day = 30 payment_due_date = date(nachweis.jahr, 6, 15)
else: # Q4: October-December (ends December 31) else: # Q4 payment due September 15 of same year
quarter_end_day = 31 payment_due_date = date(nachweis.jahr, 9, 15)
payment_due_date = datetime(nachweis.jahr, quarter_end_month, quarter_end_day).date()
# Create the support payment # Create the support payment
payment = DestinataerUnterstuetzung.objects.create( payment = DestinataerUnterstuetzung.objects.create(