Files
bin/finance/app/engine/loans.py
T
rdmsandClaude Haiku 4.5 80b8b369c7 refactor: Tilgungsplan-Helfer schedule_for, Kredit-Laufzeit 1–1200 Monate (A14-T5)
- Neuer schedule_for() in app/engine/loans.py: vereinheitlicht Tilgungsplan-Erstellung
- Laufzeit-Validierung in LoanIn und LoanPatch: 1–1200 Monate
- Aufrufstellen aktualisiert (routers/planning.py, services/breakdown.py,
  services/projection_service.py, services/planning_chart.py)
- Keine direkten loan_schedule(Decimal(...)) Aufrufe außerhalb engine/loans.py mehr
- Tests für schedule_for und Laufzeit-Grenzen (1, 1200, sowie 0/-5/1201 Ablehnung)

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
2026-09-30 13:10:55 +02:00

65 lines
2.6 KiB
Python

import calendar
from dataclasses import dataclass
from datetime import date
from decimal import ROUND_HALF_UP, Decimal
CENT = Decimal("0.01")
@dataclass(frozen=True)
class Installment:
due: date
payment: Decimal
interest: Decimal
principal: Decimal
remaining: Decimal
def add_months(d: date, months: int) -> date:
y, m0 = divmod(d.year * 12 + d.month - 1 + months, 12)
m = m0 + 1
return date(y, m, min(d.day, calendar.monthrange(y, m)[1]))
def annuity_payment(principal: Decimal, annual_rate_pct: Decimal, term_months: int) -> Decimal:
i = annual_rate_pct / Decimal(100) / Decimal(12)
if i == 0:
return (principal / term_months).quantize(CENT, ROUND_HALF_UP)
q = (Decimal(1) + i) ** term_months
return (principal * i * q / (q - Decimal(1))).quantize(CENT, ROUND_HALF_UP)
def loan_schedule(principal: Decimal, annual_rate_pct: Decimal, term_months: int,
payout_date: date, repayment_type: str) -> list[Installment]:
i = annual_rate_pct / Decimal(100) / Decimal(12)
plan: list[Installment] = []
remaining = principal
if repayment_type == "bullet":
for n in range(1, term_months + 1):
interest = (remaining * i).quantize(CENT, ROUND_HALF_UP)
principal_part = remaining if n == term_months else Decimal("0")
remaining = remaining - principal_part
plan.append(Installment(add_months(payout_date, n),
-(interest + principal_part), interest,
principal_part, remaining.quantize(CENT)))
return plan
rate = annuity_payment(principal, annual_rate_pct, term_months)
for n in range(1, term_months + 1):
interest = (remaining * i).quantize(CENT, ROUND_HALF_UP)
principal_part = rate - interest
if n == term_months or principal_part > remaining:
principal_part = remaining
remaining = remaining - principal_part
plan.append(Installment(add_months(payout_date, n),
-(interest + principal_part), interest,
principal_part, remaining.quantize(CENT)))
return plan
def schedule_for(loan) -> list[Installment]:
"""Tilgungsplan eines Kredit-Objekts (ORM-Loan oder gleichartig mit
principal, annual_rate_pct, term_months, payout_date, repayment_type).
Ersetzt die wiederholte Feld-für-Feld-Übergabe an loan_schedule (A14)."""
return loan_schedule(Decimal(loan.principal), Decimal(loan.annual_rate_pct),
loan.term_months, loan.payout_date, loan.repayment_type)