2026-05-29 10:28:07 +08:00
|
|
|
|
"""转课/转赠路由"""
|
2026-05-29 10:40:41 +08:00
|
|
|
|
from datetime import date
|
2026-05-29 10:28:07 +08:00
|
|
|
|
from flask import render_template, request, redirect, url_for, flash, session
|
2026-05-29 10:40:41 +08:00
|
|
|
|
from models import db, Student, Course, Class_, Teacher, StudentAccount, TransferRecord, ClassStudent
|
|
|
|
|
|
from utils import login_required, log_operation, permission_required, export_excel, parse_date
|
2026-05-29 10:28:07 +08:00
|
|
|
|
import business_rules as rules
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def register_routes(app):
|
|
|
|
|
|
|
|
|
|
|
|
@app.route('/transfers', endpoint='transfer_list')
|
|
|
|
|
|
@login_required
|
|
|
|
|
|
@permission_required('transfer_view')
|
|
|
|
|
|
def transfer_list():
|
|
|
|
|
|
records = TransferRecord.query.order_by(TransferRecord.created_at.desc()).all()
|
2026-05-29 10:40:41 +08:00
|
|
|
|
# 获取关联信息用于显示
|
|
|
|
|
|
students = {s.id: s for s in Student.query.all()}
|
|
|
|
|
|
courses = {c.id: c for c in Course.query.all()}
|
|
|
|
|
|
classes = {c.id: c for c in Class_.query.all()}
|
|
|
|
|
|
teachers = {t.id: t for t in Teacher.query.all()}
|
|
|
|
|
|
return render_template('transfer_list.html', records=records,
|
|
|
|
|
|
students=students, courses=courses,
|
|
|
|
|
|
classes=classes, teachers=teachers)
|
2026-05-29 10:28:07 +08:00
|
|
|
|
|
|
|
|
|
|
@app.route('/transfers/add', methods=['GET', 'POST'], endpoint='transfer_add')
|
|
|
|
|
|
@login_required
|
|
|
|
|
|
@permission_required('transfer_add')
|
|
|
|
|
|
def transfer_add():
|
|
|
|
|
|
if request.method == 'POST':
|
|
|
|
|
|
from_student_id = int(request.form.get('from_student_id'))
|
|
|
|
|
|
from_course_id = int(request.form.get('from_course_id'))
|
|
|
|
|
|
transfer_type = request.form.get('transfer_type', 'transfer')
|
2026-05-29 10:40:41 +08:00
|
|
|
|
transfer_class_type = request.form.get('transfer_class_type', 'class_transfer')
|
2026-05-29 10:28:07 +08:00
|
|
|
|
remark = request.form.get('remark', '')
|
|
|
|
|
|
|
|
|
|
|
|
from_account = rules.lock_student_account(from_student_id, from_course_id)
|
|
|
|
|
|
if not from_account:
|
|
|
|
|
|
flash('未找到转出课时账户', 'danger')
|
|
|
|
|
|
return redirect(url_for('transfer_add'))
|
|
|
|
|
|
|
|
|
|
|
|
normal_avail = int(float(from_account.normal_hours or 0))
|
2026-05-29 10:40:41 +08:00
|
|
|
|
current_unit_price = float(from_account.unit_price or 0)
|
2026-05-29 10:28:07 +08:00
|
|
|
|
|
2026-05-29 10:40:41 +08:00
|
|
|
|
# 班级转课 - 无单价变更
|
|
|
|
|
|
if transfer_class_type == 'class_transfer':
|
|
|
|
|
|
to_class_id = request.form.get('to_class_id')
|
|
|
|
|
|
to_teacher_id = request.form.get('to_teacher_id')
|
|
|
|
|
|
price_change_type = request.form.get('price_change_type', 'no_change')
|
|
|
|
|
|
|
|
|
|
|
|
if not to_class_id:
|
|
|
|
|
|
flash('请选择目标班级', 'danger')
|
|
|
|
|
|
return redirect(url_for('transfer_add'))
|
|
|
|
|
|
|
|
|
|
|
|
to_class_id = int(to_class_id)
|
|
|
|
|
|
to_teacher_id = int(to_teacher_id) if to_teacher_id else None
|
|
|
|
|
|
|
|
|
|
|
|
# 获取原班级和老师信息
|
|
|
|
|
|
from_class_id = from_account.from_class_id if hasattr(from_account, 'from_class_id') and from_account.from_class_id else None
|
|
|
|
|
|
from_teacher_id = from_account.from_teacher_id if hasattr(from_account, 'from_teacher_id') and from_account.from_teacher_id else None
|
|
|
|
|
|
|
|
|
|
|
|
# 更新班级学员关系
|
|
|
|
|
|
if from_class_id:
|
|
|
|
|
|
old_cs = ClassStudent.query.filter_by(
|
|
|
|
|
|
student_id=from_student_id, class_id=from_class_id, status=1
|
|
|
|
|
|
).first()
|
|
|
|
|
|
if old_cs:
|
|
|
|
|
|
old_cs.status = 0
|
|
|
|
|
|
|
|
|
|
|
|
# 添加新的班级学员关系
|
|
|
|
|
|
existing_cs = ClassStudent.query.filter_by(
|
|
|
|
|
|
student_id=from_student_id, class_id=to_class_id, status=1
|
|
|
|
|
|
).first()
|
|
|
|
|
|
if not existing_cs:
|
|
|
|
|
|
cs = ClassStudent(
|
|
|
|
|
|
student_id=from_student_id,
|
|
|
|
|
|
class_id=to_class_id,
|
|
|
|
|
|
join_date=date.today(),
|
|
|
|
|
|
status=1
|
|
|
|
|
|
)
|
|
|
|
|
|
db.session.add(cs)
|
|
|
|
|
|
|
|
|
|
|
|
new_unit_price = current_unit_price
|
|
|
|
|
|
price_change_reason = ''
|
|
|
|
|
|
effective_date = None
|
|
|
|
|
|
|
|
|
|
|
|
# 有单价变更
|
|
|
|
|
|
if price_change_type == 'price_changed':
|
|
|
|
|
|
new_unit_price = float(request.form.get('new_unit_price', 0))
|
|
|
|
|
|
price_change_reason = request.form.get('price_change_reason', '')
|
|
|
|
|
|
effective_date_str = request.form.get('effective_date', '')
|
|
|
|
|
|
effective_date = parse_date(effective_date_str) if effective_date_str else date.today()
|
|
|
|
|
|
|
|
|
|
|
|
if not new_unit_price or new_unit_price <= 0:
|
|
|
|
|
|
flash('有单价变更时,新单价必须大于0', 'danger')
|
|
|
|
|
|
return redirect(url_for('transfer_add'))
|
|
|
|
|
|
if not price_change_reason:
|
|
|
|
|
|
flash('有单价变更时,必须填写变更原因', 'danger')
|
|
|
|
|
|
return redirect(url_for('transfer_add'))
|
|
|
|
|
|
|
|
|
|
|
|
# 更新账户单价
|
|
|
|
|
|
from_account.unit_price = new_unit_price
|
|
|
|
|
|
from_account.original_price_per_lesson = new_unit_price
|
|
|
|
|
|
|
|
|
|
|
|
# 记录转课
|
|
|
|
|
|
record = TransferRecord(
|
|
|
|
|
|
from_student_id=from_student_id,
|
|
|
|
|
|
from_course_id=from_course_id,
|
|
|
|
|
|
to_course_id=from_course_id, # 同课程
|
|
|
|
|
|
from_class_id=from_class_id,
|
|
|
|
|
|
to_class_id=to_class_id,
|
|
|
|
|
|
from_teacher_id=from_teacher_id,
|
|
|
|
|
|
to_teacher_id=to_teacher_id,
|
|
|
|
|
|
transfer_type='class_transfer',
|
|
|
|
|
|
transfer_class_type='class_transfer',
|
|
|
|
|
|
price_change_type=price_change_type,
|
|
|
|
|
|
transfer_hours=normal_avail, # 保留全部课时
|
|
|
|
|
|
original_unit_price=current_unit_price,
|
|
|
|
|
|
new_unit_price=new_unit_price,
|
|
|
|
|
|
transfer_amount=normal_avail * current_unit_price,
|
|
|
|
|
|
price_change_reason=price_change_reason,
|
|
|
|
|
|
effective_date=effective_date,
|
|
|
|
|
|
operator_id=session.get('user_id'),
|
|
|
|
|
|
remark=remark,
|
|
|
|
|
|
)
|
|
|
|
|
|
db.session.add(record)
|
|
|
|
|
|
db.session.commit()
|
|
|
|
|
|
|
|
|
|
|
|
change_desc = f'班级:{from_class_id or "无"}→{to_class_id}'
|
|
|
|
|
|
if price_change_type == 'price_changed':
|
|
|
|
|
|
change_desc += f', 单价:{current_unit_price}→{new_unit_price}'
|
|
|
|
|
|
log_operation('班级转课(有单价变更)', f'学员{from_student_id} {change_desc}, 原因:{price_change_reason}')
|
|
|
|
|
|
flash(f'转课成功(有单价变更)!单价已更新为{new_unit_price}元/课时', 'success')
|
|
|
|
|
|
else:
|
|
|
|
|
|
log_operation('班级转课(无单价变更)', f'学员{from_student_id} {change_desc}')
|
|
|
|
|
|
flash('转课成功(无单价变更)!班级和老师已更新,课时保持不变', 'success')
|
|
|
|
|
|
|
|
|
|
|
|
# 转赠 - 给其他学员
|
|
|
|
|
|
elif transfer_type == 'gift':
|
|
|
|
|
|
transfer_hours = rules.normalize_consumption_hours(
|
|
|
|
|
|
request.form.get('transfer_hours', 1)
|
|
|
|
|
|
)
|
2026-05-29 10:28:07 +08:00
|
|
|
|
to_student_id = int(request.form.get('to_student_id'))
|
|
|
|
|
|
if transfer_hours > normal_avail:
|
|
|
|
|
|
flash(
|
|
|
|
|
|
f'赠课不可转赠。仅可转赠正课,当前正课余额{normal_avail}课时',
|
|
|
|
|
|
'danger',
|
|
|
|
|
|
)
|
|
|
|
|
|
return redirect(url_for('transfer_add'))
|
|
|
|
|
|
|
|
|
|
|
|
from_account.normal_hours = normal_avail - transfer_hours
|
|
|
|
|
|
rules.update_account_unit_price(from_account)
|
|
|
|
|
|
rules.sync_account_snapshots(from_account)
|
|
|
|
|
|
|
|
|
|
|
|
to_account = StudentAccount.query.filter_by(
|
|
|
|
|
|
student_id=to_student_id, course_id=from_course_id,
|
|
|
|
|
|
).first()
|
|
|
|
|
|
if not to_account:
|
|
|
|
|
|
to_account = StudentAccount(
|
|
|
|
|
|
student_id=to_student_id, course_id=from_course_id,
|
|
|
|
|
|
)
|
|
|
|
|
|
db.session.add(to_account)
|
|
|
|
|
|
db.session.flush()
|
|
|
|
|
|
to_account.normal_hours = int(float(to_account.normal_hours or 0)) + transfer_hours
|
|
|
|
|
|
rules.update_account_unit_price(to_account)
|
|
|
|
|
|
rules.sync_account_snapshots(to_account)
|
|
|
|
|
|
|
|
|
|
|
|
record = TransferRecord(
|
|
|
|
|
|
from_student_id=from_student_id,
|
|
|
|
|
|
to_student_id=to_student_id,
|
|
|
|
|
|
from_course_id=from_course_id,
|
|
|
|
|
|
to_course_id=from_course_id,
|
|
|
|
|
|
transfer_type='gift',
|
2026-05-29 10:40:41 +08:00
|
|
|
|
transfer_class_type='gift',
|
2026-05-29 10:28:07 +08:00
|
|
|
|
transfer_hours=transfer_hours,
|
|
|
|
|
|
operator_id=session.get('user_id'),
|
|
|
|
|
|
remark=remark,
|
|
|
|
|
|
)
|
|
|
|
|
|
db.session.add(record)
|
|
|
|
|
|
db.session.commit()
|
|
|
|
|
|
log_operation(
|
|
|
|
|
|
'转赠',
|
|
|
|
|
|
f'学员{from_student_id} 转赠正课{transfer_hours}课时给学员{to_student_id}',
|
|
|
|
|
|
)
|
|
|
|
|
|
flash('转赠成功(仅正课)', 'success')
|
2026-05-29 10:40:41 +08:00
|
|
|
|
|
|
|
|
|
|
# 升阶 - 换课程
|
2026-05-29 10:28:07 +08:00
|
|
|
|
elif transfer_type == 'upgrade':
|
|
|
|
|
|
to_course_id = int(request.form.get('to_course_id'))
|
|
|
|
|
|
from_course = db.session.get(Course, from_course_id)
|
|
|
|
|
|
to_course = db.session.get(Course, to_course_id)
|
|
|
|
|
|
from_unit = rules.account_effective_unit_price(from_account, from_course)
|
|
|
|
|
|
to_unit = rules.account_effective_unit_price(
|
|
|
|
|
|
StudentAccount.query.filter_by(
|
|
|
|
|
|
student_id=from_student_id, course_id=to_course_id,
|
|
|
|
|
|
).first() or from_account, to_course,
|
|
|
|
|
|
) or float(to_course.price_per_hour or 0) if to_course else 0
|
|
|
|
|
|
if to_unit <= 0 and to_course:
|
|
|
|
|
|
to_unit = float(to_course.price_per_hour or 0)
|
|
|
|
|
|
|
|
|
|
|
|
transfer_normal = normal_avail
|
|
|
|
|
|
to_hours = rules.calc_transfer_to_hours(
|
|
|
|
|
|
transfer_normal, from_unit, to_unit,
|
|
|
|
|
|
)
|
|
|
|
|
|
transfer_amount = transfer_normal * from_unit
|
|
|
|
|
|
|
|
|
|
|
|
from_account.normal_hours = 0
|
|
|
|
|
|
rules.update_account_unit_price(from_account)
|
|
|
|
|
|
rules.sync_account_snapshots(from_account)
|
|
|
|
|
|
to_account = StudentAccount.query.filter_by(
|
|
|
|
|
|
student_id=from_student_id, course_id=to_course_id,
|
|
|
|
|
|
).first()
|
|
|
|
|
|
if not to_account:
|
|
|
|
|
|
to_account = StudentAccount(
|
|
|
|
|
|
student_id=from_student_id, course_id=to_course_id,
|
|
|
|
|
|
)
|
|
|
|
|
|
db.session.add(to_account)
|
|
|
|
|
|
db.session.flush()
|
|
|
|
|
|
to_account.normal_hours = int(float(to_account.normal_hours or 0)) + to_hours
|
|
|
|
|
|
rules.update_account_unit_price(to_account)
|
|
|
|
|
|
rules.sync_account_snapshots(to_account)
|
|
|
|
|
|
|
|
|
|
|
|
record = TransferRecord(
|
|
|
|
|
|
from_student_id=from_student_id,
|
|
|
|
|
|
from_course_id=from_course_id,
|
|
|
|
|
|
to_course_id=to_course_id,
|
|
|
|
|
|
transfer_type='upgrade',
|
2026-05-29 10:40:41 +08:00
|
|
|
|
transfer_class_type='upgrade',
|
2026-05-29 10:28:07 +08:00
|
|
|
|
transfer_hours=transfer_normal,
|
|
|
|
|
|
transfer_amount=transfer_amount,
|
|
|
|
|
|
operator_id=session.get('user_id'),
|
|
|
|
|
|
remark=remark or '升阶结转',
|
|
|
|
|
|
)
|
|
|
|
|
|
db.session.add(record)
|
|
|
|
|
|
db.session.commit()
|
|
|
|
|
|
log_operation(
|
|
|
|
|
|
'升阶',
|
|
|
|
|
|
f'学员{from_student_id} 正课{transfer_normal}课时→课程{to_course_id} 转入{to_hours}课时',
|
|
|
|
|
|
)
|
|
|
|
|
|
flash(f'升阶成功!100%正课结转,转入{to_hours}课时', 'success')
|
2026-05-29 10:40:41 +08:00
|
|
|
|
|
|
|
|
|
|
# 普通转课 - 换课程课时
|
2026-05-29 10:28:07 +08:00
|
|
|
|
else:
|
|
|
|
|
|
to_course_id = int(request.form.get('to_course_id'))
|
2026-05-29 10:40:41 +08:00
|
|
|
|
transfer_hours = rules.normalize_consumption_hours(
|
|
|
|
|
|
request.form.get('transfer_hours', 1)
|
|
|
|
|
|
)
|
2026-05-29 10:28:07 +08:00
|
|
|
|
from_course = db.session.get(Course, from_course_id)
|
|
|
|
|
|
to_course = db.session.get(Course, to_course_id)
|
|
|
|
|
|
from_unit = rules.account_effective_unit_price(from_account, from_course)
|
|
|
|
|
|
to_account_pre = StudentAccount.query.filter_by(
|
|
|
|
|
|
student_id=from_student_id, course_id=to_course_id,
|
|
|
|
|
|
).first()
|
|
|
|
|
|
to_unit = rules.account_effective_unit_price(to_account_pre, to_course)
|
|
|
|
|
|
if to_unit <= 0 and to_course:
|
|
|
|
|
|
to_unit = float(to_course.price_per_hour or 0)
|
|
|
|
|
|
|
|
|
|
|
|
if transfer_hours > normal_avail:
|
|
|
|
|
|
flash(
|
|
|
|
|
|
f'仅可转出正课。当前正课{normal_avail}课时,赠课{int(float(from_account.gifted_hours or 0))}课时不可转课',
|
|
|
|
|
|
'danger',
|
|
|
|
|
|
)
|
|
|
|
|
|
return redirect(url_for('transfer_add'))
|
|
|
|
|
|
|
|
|
|
|
|
transfer_amount = transfer_hours * from_unit
|
|
|
|
|
|
to_hours = rules.calc_transfer_to_hours(
|
|
|
|
|
|
transfer_hours, from_unit, to_unit,
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
from_account.normal_hours = normal_avail - transfer_hours
|
|
|
|
|
|
rules.update_account_unit_price(from_account)
|
|
|
|
|
|
rules.sync_account_snapshots(from_account)
|
|
|
|
|
|
|
|
|
|
|
|
to_account = StudentAccount.query.filter_by(
|
|
|
|
|
|
student_id=from_student_id, course_id=to_course_id,
|
|
|
|
|
|
).first()
|
|
|
|
|
|
if not to_account:
|
|
|
|
|
|
to_account = StudentAccount(
|
|
|
|
|
|
student_id=from_student_id, course_id=to_course_id,
|
|
|
|
|
|
)
|
|
|
|
|
|
db.session.add(to_account)
|
|
|
|
|
|
db.session.flush()
|
|
|
|
|
|
to_account.normal_hours = int(float(to_account.normal_hours or 0)) + to_hours
|
|
|
|
|
|
rules.update_account_unit_price(to_account)
|
|
|
|
|
|
rules.sync_account_snapshots(to_account)
|
|
|
|
|
|
|
|
|
|
|
|
record = TransferRecord(
|
|
|
|
|
|
from_student_id=from_student_id,
|
|
|
|
|
|
from_course_id=from_course_id,
|
|
|
|
|
|
to_course_id=to_course_id,
|
|
|
|
|
|
transfer_type='transfer',
|
2026-05-29 10:40:41 +08:00
|
|
|
|
transfer_class_type='course_transfer',
|
2026-05-29 10:28:07 +08:00
|
|
|
|
transfer_hours=transfer_hours,
|
|
|
|
|
|
transfer_amount=transfer_amount,
|
|
|
|
|
|
operator_id=session.get('user_id'),
|
|
|
|
|
|
remark=remark,
|
|
|
|
|
|
)
|
|
|
|
|
|
db.session.add(record)
|
|
|
|
|
|
db.session.commit()
|
|
|
|
|
|
log_operation(
|
|
|
|
|
|
'转课',
|
|
|
|
|
|
f'学员{from_student_id} 转出正课{transfer_hours}课时→转入{to_hours}课时',
|
|
|
|
|
|
)
|
|
|
|
|
|
flash(f'转课成功!转入{to_hours}课时(仅正课参与换算)', 'success')
|
|
|
|
|
|
|
|
|
|
|
|
return redirect(url_for('transfer_list'))
|
|
|
|
|
|
|
|
|
|
|
|
students = Student.query.filter_by(status=1).all()
|
|
|
|
|
|
courses = Course.query.filter_by(status=1).all()
|
2026-05-29 10:40:41 +08:00
|
|
|
|
classes = Class_.query.filter_by(status=1).all()
|
|
|
|
|
|
teachers = Teacher.query.filter_by(status=1).all()
|
|
|
|
|
|
return render_template('transfer_form.html', students=students, courses=courses,
|
|
|
|
|
|
classes=classes, teachers=teachers)
|
2026-05-29 10:28:07 +08:00
|
|
|
|
|
|
|
|
|
|
@app.route('/transfers/export', endpoint='transfer_export')
|
|
|
|
|
|
@login_required
|
|
|
|
|
|
@permission_required('transfer_export')
|
|
|
|
|
|
def transfer_export():
|
|
|
|
|
|
from utils import export_transfers as do_export
|
|
|
|
|
|
return do_export()
|