Initial commit
This commit is contained in:
251
xuexiao/keshi_stats.py
Normal file
251
xuexiao/keshi_stats.py
Normal file
@@ -0,0 +1,251 @@
|
||||
"""基于课时核对表 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
|
||||
Reference in New Issue
Block a user