"""财务视角:应退金额、退费预览(原价口径)""" from __future__ import annotations from typing import Any, Optional from models import StudentAccount, Course, MonthlySnapshot def _f(val) -> float: if val is None: return 0.0 return float(val) def is_refunded_account( account: StudentAccount, ledger: Optional[dict] = None, snap: Optional[MonthlySnapshot] = None, ) -> bool: if (account.account_status or '') == 'refunded': return True if snap and _f(snap.refund_amount) > 0: end_bal = ledger.get('end_balance') if ledger else None end_less = ledger.get('end_lessons') if ledger else None if end_bal is not None and _f(end_bal) == 0 and _f(end_less) == 0: return True return False def regular_lessons_from_ledger(account: StudentAccount, ledger: dict) -> float: v = ledger.get('end_lessons') if v is not None: return _f(v) return _f(account.normal_hours) def gift_lessons_from_ledger(account: StudentAccount, ledger: dict) -> float: v = ledger.get('end_gift_lessons') if v is not None: return _f(v) return _f(account.gifted_hours) def original_unit_price( account: StudentAccount, snap: Optional[MonthlySnapshot] = None, ) -> Optional[float]: if account.original_price_per_lesson is not None: v = float(account.original_price_per_lesson) if v > 0: return v if snap and snap.unit_price is not None: v = float(snap.unit_price) if v > 0: return v return None def account_refund_display( account: StudentAccount, ledger: Optional[dict] = None, snap: Optional[MonthlySnapshot] = None, ) -> dict[str, Any]: """单账户应退展示(列表用)""" ledger = ledger or {} if is_refunded_account(account, ledger, snap): return {'kind': 'refunded', 'text': '已退费', 'sort_value': -1.0} orig = original_unit_price(account, snap) regular = regular_lessons_from_ledger(account, ledger) if orig is None: return {'kind': 'no_price', 'text': '-', 'sort_value': -1.0} if regular <= 0: total = _f(ledger.get('end_total_lessons')) or _f(account.total_hours) gift = gift_lessons_from_ledger(account, ledger) if gift > 0 or total > 0: return {'kind': 'gift_only', 'text': '¥0(已入赠课)', 'sort_value': 0.0} return {'kind': 'zero', 'text': '¥0.00', 'amount': 0.0, 'sort_value': 0.0} amount = round(orig * regular, 2) return { 'kind': 'amount', 'text': None, 'amount': amount, 'sort_value': amount, } def aggregate_student_refund( accounts: list[StudentAccount], ledger_by_account: dict[int, dict], snap_by_account: Optional[dict[int, MonthlySnapshot]] = None, ) -> dict[str, Any]: """学员行:汇总多账户应退展示""" snap_by_account = snap_by_account or {} if not accounts: return {'kind': 'none', 'text': '-', 'sort_value': -1.0} parts = [ account_refund_display( a, ledger_by_account.get(a.id, {}), snap_by_account.get(a.id), ) for a in accounts ] if all(p['kind'] == 'refunded' for p in parts): return {'kind': 'refunded', 'text': '已退费', 'sort_value': -1.0} if all(p['kind'] == 'no_price' for p in parts): return {'kind': 'no_price', 'text': '-', 'sort_value': -1.0} amounts = [p['amount'] for p in parts if p['kind'] == 'amount'] if amounts: total = round(sum(amounts), 2) return {'kind': 'amount', 'amount': total, 'sort_value': total} if all(p['kind'] in ('gift_only', 'zero') for p in parts): return {'kind': 'gift_only', 'text': '¥0(已入赠课)', 'sort_value': 0.0} return {'kind': 'no_price', 'text': '-', 'sort_value': -1.0} def build_refund_preview( account: StudentAccount, course: Optional[Course], ledger: dict, snap: Optional[MonthlySnapshot] = None, ) -> dict[str, Any]: """学员详情:退费预览区块数据""" regular = regular_lessons_from_ledger(account, ledger) gift = gift_lessons_from_ledger(account, ledger) consumed = _f(account.consumed_normal) paid_total = _f(account.cumulative_amount) balance = ledger.get('end_balance') if balance is None: balance = None else: balance = _f(balance) orig_unit = original_unit_price(account) total_purchased_normal = consumed + regular total_all_hours = consumed + regular + gift + _f(account.consumed_gifted) if orig_unit is None: return { 'has_original': False, 'course_name': (course.course_code or course.name) if course else '—', 'refund_line': None, } original_total = round(orig_unit * total_purchased_normal, 2) if total_purchased_normal > 0 else 0.0 discount = round(max(0, original_total - paid_total), 2) consume_unit = round(paid_total / total_all_hours, 2) if total_all_hours > 0 else 0.0 refund_due = round(orig_unit * regular, 2) if regular > 0 else 0.0 if is_refunded_account(account, ledger, snap): refund_due = 0.0 refund_label = '已退费' elif regular <= 0 and (gift > 0 or _f(ledger.get('end_total_lessons')) > 0): refund_due = 0.0 refund_label = '¥0(已入赠课)' else: refund_label = None return { 'has_original': True, 'course_name': (course.course_code or course.name) if course else '—', 'original_total': original_total, 'paid_total': paid_total, 'discount': discount, 'original_unit': orig_unit, 'consume_unit': consume_unit, 'consumed_normal': consumed, 'regular_remaining': regular, 'gift_remaining': gift, 'balance': balance, 'refund_due': refund_due, 'refund_label': refund_label, } def build_students_finance( students: list, accounts_by_student: dict[int, list[StudentAccount]], ledger_by_account: dict[int, dict], snap_by_account: dict[int, MonthlySnapshot], ) -> dict[int, dict]: out = {} for s in students: accs = accounts_by_student.get(s.id, []) out[s.id] = aggregate_student_refund( accs, ledger_by_account, snap_by_account, ) return out