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
|