Initial commit
This commit is contained in:
192
xuexiao/keshi_display.py
Normal file
192
xuexiao/keshi_display.py
Normal file
@@ -0,0 +1,192 @@
|
||||
"""课时核对表:UI 展示辅助(主月份快照、格式化)"""
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Optional
|
||||
|
||||
from sqlalchemy import func
|
||||
|
||||
from models import db, MonthlySnapshot, StudentAccount
|
||||
|
||||
|
||||
def list_period_stats() -> list[dict]:
|
||||
"""各月导入条数(用于月份切换)"""
|
||||
rows = db.session.query(
|
||||
MonthlySnapshot.year,
|
||||
MonthlySnapshot.month,
|
||||
func.count(MonthlySnapshot.id),
|
||||
).group_by(
|
||||
MonthlySnapshot.year, MonthlySnapshot.month,
|
||||
).order_by(
|
||||
MonthlySnapshot.year.desc(),
|
||||
MonthlySnapshot.month.desc(),
|
||||
).all()
|
||||
return [
|
||||
{'year': int(y), 'month': int(m), 'count': int(c)}
|
||||
for y, m, c in rows
|
||||
]
|
||||
|
||||
|
||||
def get_latest_period(min_records: int = 20) -> tuple[Optional[int], Optional[int]]:
|
||||
"""
|
||||
主展示月份:在条数>=min_records 的月份中取最新年月。
|
||||
避免测试月(如 2026-05 仅 1 条)被按年月排序误选为默认月。
|
||||
"""
|
||||
rows = (
|
||||
db.session.query(
|
||||
MonthlySnapshot.year,
|
||||
MonthlySnapshot.month,
|
||||
func.count(MonthlySnapshot.id),
|
||||
)
|
||||
.group_by(MonthlySnapshot.year, MonthlySnapshot.month)
|
||||
.having(func.count(MonthlySnapshot.id) >= min_records)
|
||||
.order_by(
|
||||
MonthlySnapshot.year.desc(),
|
||||
MonthlySnapshot.month.desc(),
|
||||
)
|
||||
.all()
|
||||
)
|
||||
if rows:
|
||||
y, m, _ = rows[0]
|
||||
return int(y), int(m)
|
||||
row2 = (
|
||||
db.session.query(MonthlySnapshot.year, MonthlySnapshot.month)
|
||||
.order_by(MonthlySnapshot.year.desc(), MonthlySnapshot.month.desc())
|
||||
.first()
|
||||
)
|
||||
return (int(row2[0]), int(row2[1])) if row2 else (None, None)
|
||||
|
||||
|
||||
def attach_latest_snapshots(
|
||||
accounts: list[StudentAccount],
|
||||
year: Optional[int] = None,
|
||||
month: Optional[int] = None,
|
||||
) -> dict[int, MonthlySnapshot]:
|
||||
"""指定月份(默认主月份)下每个账户的快照"""
|
||||
if not accounts:
|
||||
return {}
|
||||
if year is None or month is None:
|
||||
year, month = get_latest_period()
|
||||
if not year or not month:
|
||||
return {}
|
||||
ids = [a.id for a in accounts]
|
||||
snaps = MonthlySnapshot.query.filter(
|
||||
MonthlySnapshot.account_id.in_(ids),
|
||||
MonthlySnapshot.year == year,
|
||||
MonthlySnapshot.month == month,
|
||||
).all()
|
||||
return {s.account_id: s for s in snaps}
|
||||
|
||||
|
||||
def snapshot_lessons_display(snap: Optional[MonthlySnapshot], account: StudentAccount) -> dict:
|
||||
"""剩余课时 / 余额 / 单价(来自 Excel 月末列)"""
|
||||
if snap:
|
||||
end_normal = snap.end_lessons
|
||||
end_gift = snap.end_gift_lessons
|
||||
end_total = snap.end_total_lessons
|
||||
end_balance = snap.end_balance
|
||||
# 历史误映射:当月列曾写入 prev_*,月末展示优先 end_*,否则回退 prev_*
|
||||
if end_normal is None and snap.prev_lessons is not None:
|
||||
end_normal = snap.prev_lessons
|
||||
if end_total is None and snap.prev_total_lessons is not None:
|
||||
end_total = snap.prev_total_lessons
|
||||
if end_balance is None and snap.prev_balance is not None:
|
||||
end_balance = snap.prev_balance
|
||||
if end_gift is None and end_total is not None and end_normal is not None:
|
||||
end_gift = end_total - end_normal
|
||||
if end_gift is None and snap.prev_gift_lessons is not None and snap.end_lessons is None:
|
||||
end_gift = snap.prev_gift_lessons
|
||||
if end_total is None:
|
||||
parts = [float(end_normal or 0), float(end_gift or 0)]
|
||||
if any(parts):
|
||||
end_total = sum(parts)
|
||||
unit = snap.unit_price
|
||||
if (unit is None or float(unit or 0) == 0) and snap.prev_balance and snap.prev_lessons:
|
||||
try:
|
||||
unit = float(snap.prev_balance) / float(snap.prev_lessons)
|
||||
except (TypeError, ValueError, ZeroDivisionError):
|
||||
unit = account.unit_price
|
||||
elif unit is None:
|
||||
unit = account.unit_price
|
||||
return {
|
||||
'end_lessons': end_normal,
|
||||
'end_gift_lessons': end_gift,
|
||||
'end_total_lessons': end_total,
|
||||
'end_balance': end_balance,
|
||||
'unit_price': unit,
|
||||
'period': snap.period_label,
|
||||
}
|
||||
return {
|
||||
'end_lessons': account.normal_hours,
|
||||
'end_gift_lessons': account.gifted_hours,
|
||||
'end_total_lessons': account.total_hours,
|
||||
'end_balance': None,
|
||||
'unit_price': account.unit_price,
|
||||
'period': None,
|
||||
}
|
||||
|
||||
|
||||
def fmt_money(val) -> str:
|
||||
if val is None:
|
||||
return '-'
|
||||
return f'¥{float(val):,.2f}'
|
||||
|
||||
|
||||
def fmt_hours(val) -> str:
|
||||
if val is None:
|
||||
return '-'
|
||||
v = float(val)
|
||||
if v == int(v):
|
||||
return str(int(v))
|
||||
return f'{v:.2f}'
|
||||
|
||||
|
||||
def fmt_price(val) -> str:
|
||||
if val is None:
|
||||
return '-'
|
||||
v = float(val)
|
||||
if v == 0:
|
||||
return '0.00'
|
||||
return f'{v:.2f}'
|
||||
|
||||
|
||||
def summarize_students_ledger(
|
||||
student_ids: list[int],
|
||||
year: Optional[int] = None,
|
||||
month: Optional[int] = None,
|
||||
) -> dict[int, dict]:
|
||||
"""按学员汇总主月份 Excel 快照:总剩余课时、总余额、课程数"""
|
||||
if not student_ids:
|
||||
return {}
|
||||
if year is None or month is None:
|
||||
year, month = get_latest_period()
|
||||
if not year or not month:
|
||||
return {}
|
||||
|
||||
rows = (
|
||||
db.session.query(MonthlySnapshot, StudentAccount)
|
||||
.join(StudentAccount, MonthlySnapshot.account_id == StudentAccount.id)
|
||||
.filter(
|
||||
MonthlySnapshot.year == year,
|
||||
MonthlySnapshot.month == month,
|
||||
StudentAccount.student_id.in_(student_ids),
|
||||
)
|
||||
.all()
|
||||
)
|
||||
out: dict[int, dict] = {}
|
||||
for snap, acc in rows:
|
||||
disp = snapshot_lessons_display(snap, acc)
|
||||
sid = acc.student_id
|
||||
bucket = out.setdefault(sid, {
|
||||
'total_lessons': 0.0,
|
||||
'total_balance': 0.0,
|
||||
'course_count': 0,
|
||||
'period': disp.get('period'),
|
||||
})
|
||||
th = disp.get('end_total_lessons')
|
||||
if th is not None:
|
||||
bucket['total_lessons'] += float(th)
|
||||
tb = disp.get('end_balance')
|
||||
if tb is not None:
|
||||
bucket['total_balance'] += float(tb)
|
||||
bucket['course_count'] += 1
|
||||
return out
|
||||
Reference in New Issue
Block a user