252 lines
8.7 KiB
Python
252 lines
8.7 KiB
Python
"""基于课时核对表 MonthlySnapshot 的统计汇总(Excel 导入数据)"""
|
||
from __future__ import annotations
|
||
|
||
from typing import Optional
|
||
|
||
from sqlalchemy import func
|
||
|
||
from models import db, MonthlySnapshot, StudentAccount, Course, ClassRecord
|
||
from keshi_display import get_latest_period, list_period_stats
|
||
|
||
|
||
def _recharge_expr():
|
||
return (
|
||
func.coalesce(MonthlySnapshot.new_signup_amount, 0)
|
||
+ func.coalesce(MonthlySnapshot.renewal_amount, 0)
|
||
)
|
||
|
||
|
||
def _snapshot_query(year: Optional[int] = None, month: Optional[int] = None):
|
||
q = db.session.query(MonthlySnapshot)
|
||
if year is not None:
|
||
q = q.filter(MonthlySnapshot.year == year)
|
||
if month is not None:
|
||
q = q.filter(MonthlySnapshot.month == month)
|
||
return q
|
||
|
||
|
||
def period_overview(year: int, month: int) -> dict:
|
||
"""单月概览:充值、消课、退费、新签人数"""
|
||
base = _snapshot_query(year, month)
|
||
recharge = base.with_entities(func.coalesce(func.sum(_recharge_expr()), 0)).scalar() or 0
|
||
consumed_h = base.with_entities(
|
||
func.coalesce(func.sum(MonthlySnapshot.consumed_lessons), 0),
|
||
).scalar() or 0
|
||
consumed_amt = base.with_entities(
|
||
func.coalesce(func.sum(MonthlySnapshot.consumed_amount), 0),
|
||
).scalar() or 0
|
||
refund = base.with_entities(
|
||
func.coalesce(func.sum(MonthlySnapshot.refund_amount), 0),
|
||
).scalar() or 0
|
||
end_balance = base.with_entities(
|
||
func.coalesce(func.sum(MonthlySnapshot.end_balance), 0),
|
||
).scalar() or 0
|
||
new_signups = (
|
||
db.session.query(func.count(func.distinct(StudentAccount.student_id)))
|
||
.select_from(MonthlySnapshot)
|
||
.join(StudentAccount, MonthlySnapshot.account_id == StudentAccount.id)
|
||
.filter(
|
||
MonthlySnapshot.year == year,
|
||
MonthlySnapshot.month == month,
|
||
db.or_(
|
||
MonthlySnapshot.new_signup_amount > 0,
|
||
MonthlySnapshot.new_signup_lessons > 0,
|
||
),
|
||
)
|
||
.scalar()
|
||
or 0
|
||
)
|
||
active_accounts = base.count()
|
||
return {
|
||
'recharge': float(recharge),
|
||
'consumed_hours': float(consumed_h),
|
||
'consumed_amount': float(consumed_amt),
|
||
'refund': float(refund),
|
||
'end_balance': float(end_balance),
|
||
'new_signups': int(new_signups),
|
||
'active_accounts': int(active_accounts),
|
||
}
|
||
|
||
|
||
def year_overview(year: int) -> dict:
|
||
"""年度汇总(该年各月快照之和)"""
|
||
base = _snapshot_query(year, None)
|
||
recharge = base.with_entities(func.coalesce(func.sum(_recharge_expr()), 0)).scalar() or 0
|
||
consumed_h = base.with_entities(
|
||
func.coalesce(func.sum(MonthlySnapshot.consumed_lessons), 0),
|
||
).scalar() or 0
|
||
refund = base.with_entities(
|
||
func.coalesce(func.sum(MonthlySnapshot.refund_amount), 0),
|
||
).scalar() or 0
|
||
new_signups = (
|
||
db.session.query(func.count(func.distinct(StudentAccount.student_id)))
|
||
.select_from(MonthlySnapshot)
|
||
.join(StudentAccount, MonthlySnapshot.account_id == StudentAccount.id)
|
||
.filter(
|
||
MonthlySnapshot.year == year,
|
||
db.or_(
|
||
MonthlySnapshot.new_signup_amount > 0,
|
||
MonthlySnapshot.new_signup_lessons > 0,
|
||
),
|
||
)
|
||
.scalar()
|
||
or 0
|
||
)
|
||
return {
|
||
'recharge': float(recharge),
|
||
'consumed_hours': float(consumed_h),
|
||
'refund': float(refund),
|
||
'new_signups': int(new_signups),
|
||
}
|
||
|
||
|
||
def cumulative_finance() -> dict:
|
||
"""全库累计(所有已导入月份)"""
|
||
recharge = (
|
||
db.session.query(func.coalesce(func.sum(_recharge_expr()), 0)).scalar() or 0
|
||
)
|
||
refund = (
|
||
db.session.query(func.coalesce(func.sum(MonthlySnapshot.refund_amount), 0)).scalar()
|
||
or 0
|
||
)
|
||
consumed_amt = (
|
||
db.session.query(func.coalesce(func.sum(MonthlySnapshot.consumed_amount), 0)).scalar()
|
||
or 0
|
||
)
|
||
return {
|
||
'total_recharge': float(recharge),
|
||
'total_refund': float(refund),
|
||
'net_income': float(recharge) - float(refund),
|
||
'total_consumed_amount': float(consumed_amt),
|
||
}
|
||
|
||
|
||
def course_stats_for_period(year: int, month: int) -> list[dict]:
|
||
"""各课程:充值、消课课时、消课金额、月末余额"""
|
||
rows = (
|
||
db.session.query(
|
||
Course.name,
|
||
Course.course_code,
|
||
func.coalesce(func.sum(_recharge_expr()), 0),
|
||
func.coalesce(func.sum(MonthlySnapshot.consumed_lessons), 0),
|
||
func.coalesce(func.sum(MonthlySnapshot.consumed_amount), 0),
|
||
func.coalesce(func.sum(MonthlySnapshot.refund_amount), 0),
|
||
func.coalesce(func.sum(MonthlySnapshot.end_balance), 0),
|
||
)
|
||
.select_from(MonthlySnapshot)
|
||
.join(StudentAccount, MonthlySnapshot.account_id == StudentAccount.id)
|
||
.join(Course, StudentAccount.course_id == Course.id)
|
||
.filter(MonthlySnapshot.year == year, MonthlySnapshot.month == month)
|
||
.group_by(Course.id, Course.name, Course.course_code)
|
||
.order_by(func.sum(_recharge_expr()).desc())
|
||
.all()
|
||
)
|
||
return [
|
||
{
|
||
'name': name,
|
||
'code': code or name,
|
||
'recharge': float(r or 0),
|
||
'consumed_hours': float(h or 0),
|
||
'consumed_amount': float(a or 0),
|
||
'refund': float(rf or 0),
|
||
'end_balance': float(bal or 0),
|
||
}
|
||
for name, code, r, h, a, rf, bal in rows
|
||
if float(r or 0) or float(h or 0) or float(rf or 0) or float(bal or 0)
|
||
]
|
||
|
||
|
||
def refund_breakdown(year: int, month: int) -> list[dict]:
|
||
"""有退费金额的记录按课程汇总"""
|
||
rows = (
|
||
db.session.query(
|
||
Course.name,
|
||
func.count(MonthlySnapshot.id),
|
||
func.coalesce(func.sum(MonthlySnapshot.refund_amount), 0),
|
||
func.coalesce(func.sum(MonthlySnapshot.refund_lessons), 0),
|
||
)
|
||
.select_from(MonthlySnapshot)
|
||
.join(StudentAccount, MonthlySnapshot.account_id == StudentAccount.id)
|
||
.join(Course, StudentAccount.course_id == Course.id)
|
||
.filter(
|
||
MonthlySnapshot.year == year,
|
||
MonthlySnapshot.month == month,
|
||
MonthlySnapshot.refund_amount > 0,
|
||
)
|
||
.group_by(Course.id, Course.name)
|
||
.order_by(func.sum(MonthlySnapshot.refund_amount).desc())
|
||
.all()
|
||
)
|
||
return [
|
||
{
|
||
'name': name,
|
||
'count': int(cnt),
|
||
'amount': float(amt or 0),
|
||
'lessons': float(less or 0),
|
||
}
|
||
for name, cnt, amt, less in rows
|
||
]
|
||
|
||
|
||
def monthly_trend_from_snapshots() -> list[dict]:
|
||
"""已导入各月充值/消课/退费趋势"""
|
||
stats = list_period_stats()
|
||
if not stats:
|
||
return []
|
||
trend = []
|
||
for p in sorted(stats, key=lambda x: (x['year'], x['month'])):
|
||
ov = period_overview(p['year'], p['month'])
|
||
trend.append({
|
||
'month': f"{p['year']}-{p['month']:02d}",
|
||
'label': f"{p['month']}月",
|
||
'recharge': ov['recharge'],
|
||
'consumption': ov['consumed_hours'],
|
||
'refund': ov['refund'],
|
||
'count': p['count'],
|
||
})
|
||
return trend
|
||
|
||
|
||
def class_activity_for_period(year: int, month: int) -> list[dict]:
|
||
"""上课记录按课程汇总(人次/课时)"""
|
||
rows = (
|
||
db.session.query(
|
||
Course.name,
|
||
func.count(ClassRecord.id),
|
||
func.coalesce(func.sum(ClassRecord.lessons_consumed), 0),
|
||
)
|
||
.select_from(ClassRecord)
|
||
.join(StudentAccount, ClassRecord.account_id == StudentAccount.id)
|
||
.join(Course, StudentAccount.course_id == Course.id)
|
||
.filter(ClassRecord.year == year, ClassRecord.month == month)
|
||
.group_by(Course.id, Course.name)
|
||
.order_by(func.count(ClassRecord.id).desc())
|
||
.limit(15)
|
||
.all()
|
||
)
|
||
return [
|
||
{'name': name, 'sessions': int(cnt), 'lessons': float(less or 0)}
|
||
for name, cnt, less in rows
|
||
]
|
||
|
||
|
||
def resolve_report_period(
|
||
year: Optional[int] = None,
|
||
month: Optional[int] = None,
|
||
) -> tuple[int, int]:
|
||
if year and month:
|
||
return int(year), int(month)
|
||
py, pm = get_latest_period()
|
||
if py and pm:
|
||
return py, pm
|
||
row = (
|
||
db.session.query(MonthlySnapshot.year, MonthlySnapshot.month)
|
||
.order_by(MonthlySnapshot.year.desc(), MonthlySnapshot.month.desc())
|
||
.first()
|
||
)
|
||
if row:
|
||
return int(row[0]), int(row[1])
|
||
from datetime import date
|
||
t = date.today()
|
||
return t.year, t.month
|