Files
drl_2/xuexiao/finance_display.py

199 lines
6.5 KiB
Python
Raw Normal View History

2026-05-29 10:28:07 +08:00
"""财务视角:应退金额、退费预览(原价口径)"""
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