Files
drl_2/xuexiao/finance_display.py
user9994793890 ee860ce0ae Initial commit
2026-05-29 10:28:07 +08:00

199 lines
6.5 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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