199 lines
6.5 KiB
Python
199 lines
6.5 KiB
Python
"""财务视角:应退金额、退费预览(原价口径)"""
|
||
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
|