feat: 实现单价分离、班级转课、消课筛选和手动录入功能
Coze-Commit-Type: user Coze-User-ID: 3722323274763196 Coze-Conversation-ID: 5260473
This commit is contained in:
@@ -5,7 +5,7 @@ from utils import (
|
||||
permission_required, export_excel, login_required, log_operation, parse_date,
|
||||
filter_students_query, teacher_can_access_class, teacher_can_access_student,
|
||||
)
|
||||
from models import (db, Student, Course, Class_, ClassStudent, ConsumptionRecord)
|
||||
from models import (db, Student, Course, Class_, ClassStudent, Teacher, ConsumptionRecord)
|
||||
import business_rules as rules
|
||||
|
||||
|
||||
@@ -16,7 +16,14 @@ def register_routes(app):
|
||||
@permission_required('consumption_view')
|
||||
def consumption_list():
|
||||
keyword = request.args.get('keyword', '')
|
||||
class_id = request.args.get('class_id', '')
|
||||
teacher_id = request.args.get('teacher_id', '')
|
||||
start_date = request.args.get('start_date', '')
|
||||
end_date = request.args.get('end_date', '')
|
||||
|
||||
query = ConsumptionRecord.query
|
||||
|
||||
# 按学员姓名搜索
|
||||
if keyword:
|
||||
sq = filter_students_query(Student.query.filter(Student.name.contains(keyword)))
|
||||
student_ids = [s.id for s in sq.all()]
|
||||
@@ -24,8 +31,46 @@ def register_routes(app):
|
||||
query = query.filter(ConsumptionRecord.student_id.in_(student_ids))
|
||||
else:
|
||||
query = query.filter(False)
|
||||
|
||||
# 按班级筛选
|
||||
if class_id:
|
||||
class_students = ClassStudent.query.filter_by(class_id=int(class_id), status=1).all()
|
||||
student_ids = [cs.student_id for cs in class_students]
|
||||
if student_ids:
|
||||
query = query.filter(ConsumptionRecord.student_id.in_(student_ids))
|
||||
else:
|
||||
query = query.filter(False)
|
||||
|
||||
# 按老师筛选
|
||||
if teacher_id:
|
||||
teacher_classes = Class_.query.filter_by(teacher_id=int(teacher_id), status=1).all()
|
||||
class_ids = [c.id for c in teacher_classes]
|
||||
class_students = ClassStudent.query.filter(
|
||||
ClassStudent.class_id.in_(class_ids),
|
||||
ClassStudent.status == 1
|
||||
).all()
|
||||
student_ids = [cs.student_id for cs in class_students]
|
||||
if student_ids:
|
||||
query = query.filter(ConsumptionRecord.student_id.in_(student_ids))
|
||||
else:
|
||||
query = query.filter(False)
|
||||
|
||||
# 按日期范围筛选
|
||||
if start_date:
|
||||
query = query.filter(ConsumptionRecord.consume_date >= start_date)
|
||||
if end_date:
|
||||
query = query.filter(ConsumptionRecord.consume_date <= end_date)
|
||||
|
||||
records = query.order_by(ConsumptionRecord.consume_date.desc()).all()
|
||||
return render_template('consumption_list.html', records=records, keyword=keyword)
|
||||
|
||||
# 获取筛选选项
|
||||
classes = Class_.query.filter_by(status=1).all()
|
||||
teachers = Teacher.query.filter_by(status=1).all()
|
||||
|
||||
return render_template('consumption_list.html', records=records, keyword=keyword,
|
||||
class_id=class_id, teacher_id=teacher_id,
|
||||
start_date=start_date, end_date=end_date,
|
||||
classes=classes, teachers=teachers)
|
||||
|
||||
@app.route('/consumptions/add', methods=['GET', 'POST'], endpoint='consumption_add')
|
||||
@login_required
|
||||
@@ -231,3 +276,79 @@ def register_routes(app):
|
||||
r.remark or '', r.created_at.strftime('%Y-%m-%d %H:%M') if r.created_at else '',
|
||||
])
|
||||
return export_excel('消课记录', headers, rows)
|
||||
|
||||
@app.route('/api/class_students/<int:class_id>')
|
||||
@login_required
|
||||
def api_class_students(class_id):
|
||||
"""获取班级学员列表"""
|
||||
class_students = ClassStudent.query.filter_by(class_id=class_id, status=1).all()
|
||||
student_ids = [cs.student_id for cs in class_students]
|
||||
students = Student.query.filter(Student.id.in_(student_ids), Student.status == 1).all()
|
||||
return {'success': True, 'data': [{'id': s.id, 'name': s.name} for s in students]}
|
||||
|
||||
@app.route('/consumptions/manual', methods=['POST'], endpoint='consumption_manual_add')
|
||||
@login_required
|
||||
@permission_required('consumption_add')
|
||||
def consumption_manual_add():
|
||||
"""手动录入消课汇总"""
|
||||
consume_date = parse_date(request.form.get('consume_date')) or date.today()
|
||||
student_id = int(request.form.get('student_id'))
|
||||
class_id = request.form.get('class_id')
|
||||
if class_id:
|
||||
class_id = int(class_id)
|
||||
hours = float(request.form.get('hours', 2))
|
||||
consume_type = request.form.get('consume_type', 'normal')
|
||||
remark = request.form.get('remark', '')
|
||||
|
||||
# 获取班级关联的课程
|
||||
cls = db.session.get(Class_, class_id) if class_id else None
|
||||
course_id = cls.course_id if cls else None
|
||||
|
||||
if not course_id:
|
||||
flash('请选择有效的班级', 'danger')
|
||||
return redirect(url_for('consumption_list'))
|
||||
|
||||
account = rules.lock_student_account(student_id, course_id)
|
||||
if not account:
|
||||
flash('未找到该学员的课时账户', 'danger')
|
||||
return redirect(url_for('consumption_list'))
|
||||
|
||||
if account.is_stopped:
|
||||
flash('该学员正在停课保号中,无法消课', 'danger')
|
||||
return redirect(url_for('consumption_list'))
|
||||
|
||||
try:
|
||||
normal_consumed, gifted_consumed, actual_type = rules.deduct_hours(
|
||||
account, hours,
|
||||
)
|
||||
except ValueError as e:
|
||||
flash(str(e), 'danger')
|
||||
return redirect(url_for('consumption_list'))
|
||||
|
||||
course = db.session.get(Course, course_id)
|
||||
unit_at = rules.account_effective_unit_price(account, course)
|
||||
|
||||
record = ConsumptionRecord(
|
||||
student_id=student_id,
|
||||
class_id=class_id,
|
||||
course_id=course_id,
|
||||
hours_consumed=hours,
|
||||
consume_type=actual_type,
|
||||
normal_consumed=normal_consumed,
|
||||
gifted_consumed=gifted_consumed,
|
||||
unit_price_at_consume=unit_at,
|
||||
operator_id=session.get('user_id'),
|
||||
consume_date=consume_date,
|
||||
is_makeup=0,
|
||||
is_trial=0,
|
||||
remark='手动录入: ' + remark if remark else '手动录入',
|
||||
)
|
||||
db.session.add(record)
|
||||
rules.sync_account_snapshots(account, consume_date)
|
||||
log_operation(
|
||||
'手动消课录入',
|
||||
f'学员ID:{student_id} 手动消课{hours}课时 归属{consume_date}',
|
||||
)
|
||||
db.session.commit()
|
||||
flash('手动录入消课成功', 'success')
|
||||
return redirect(url_for('consumption_list'))
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
"""转课/转赠路由"""
|
||||
from datetime import date
|
||||
from flask import render_template, request, redirect, url_for, flash, session
|
||||
from models import db, Student, Course, StudentAccount, TransferRecord
|
||||
from utils import login_required, log_operation, permission_required, export_excel
|
||||
from models import db, Student, Course, Class_, Teacher, StudentAccount, TransferRecord, ClassStudent
|
||||
from utils import login_required, log_operation, permission_required, export_excel, parse_date
|
||||
import business_rules as rules
|
||||
|
||||
|
||||
@@ -12,7 +13,14 @@ def register_routes(app):
|
||||
@permission_required('transfer_view')
|
||||
def transfer_list():
|
||||
records = TransferRecord.query.order_by(TransferRecord.created_at.desc()).all()
|
||||
return render_template('transfer_list.html', records=records)
|
||||
# 获取关联信息用于显示
|
||||
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)
|
||||
|
||||
@app.route('/transfers/add', methods=['GET', 'POST'], endpoint='transfer_add')
|
||||
@login_required
|
||||
@@ -22,9 +30,7 @@ def register_routes(app):
|
||||
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')
|
||||
transfer_hours = rules.normalize_consumption_hours(
|
||||
request.form.get('transfer_hours', 1)
|
||||
)
|
||||
transfer_class_type = request.form.get('transfer_class_type', 'class_transfer')
|
||||
remark = request.form.get('remark', '')
|
||||
|
||||
from_account = rules.lock_student_account(from_student_id, from_course_id)
|
||||
@@ -33,8 +39,106 @@ def register_routes(app):
|
||||
return redirect(url_for('transfer_add'))
|
||||
|
||||
normal_avail = int(float(from_account.normal_hours or 0))
|
||||
current_unit_price = float(from_account.unit_price or 0)
|
||||
|
||||
if transfer_type == 'gift':
|
||||
# 班级转课 - 无单价变更
|
||||
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)
|
||||
)
|
||||
to_student_id = int(request.form.get('to_student_id'))
|
||||
if transfer_hours > normal_avail:
|
||||
flash(
|
||||
@@ -66,6 +170,7 @@ def register_routes(app):
|
||||
from_course_id=from_course_id,
|
||||
to_course_id=from_course_id,
|
||||
transfer_type='gift',
|
||||
transfer_class_type='gift',
|
||||
transfer_hours=transfer_hours,
|
||||
operator_id=session.get('user_id'),
|
||||
remark=remark,
|
||||
@@ -77,8 +182,9 @@ def register_routes(app):
|
||||
f'学员{from_student_id} 转赠正课{transfer_hours}课时给学员{to_student_id}',
|
||||
)
|
||||
flash('转赠成功(仅正课)', 'success')
|
||||
|
||||
# 升阶 - 换课程
|
||||
elif transfer_type == 'upgrade':
|
||||
# §五 升阶:100% 正课全额结转,不新建订单
|
||||
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)
|
||||
@@ -118,6 +224,7 @@ def register_routes(app):
|
||||
from_course_id=from_course_id,
|
||||
to_course_id=to_course_id,
|
||||
transfer_type='upgrade',
|
||||
transfer_class_type='upgrade',
|
||||
transfer_hours=transfer_normal,
|
||||
transfer_amount=transfer_amount,
|
||||
operator_id=session.get('user_id'),
|
||||
@@ -130,8 +237,13 @@ def register_routes(app):
|
||||
f'学员{from_student_id} 正课{transfer_normal}课时→课程{to_course_id} 转入{to_hours}课时',
|
||||
)
|
||||
flash(f'升阶成功!100%正课结转,转入{to_hours}课时', 'success')
|
||||
|
||||
# 普通转课 - 换课程课时
|
||||
else:
|
||||
to_course_id = int(request.form.get('to_course_id'))
|
||||
transfer_hours = rules.normalize_consumption_hours(
|
||||
request.form.get('transfer_hours', 1)
|
||||
)
|
||||
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)
|
||||
@@ -176,6 +288,7 @@ def register_routes(app):
|
||||
from_course_id=from_course_id,
|
||||
to_course_id=to_course_id,
|
||||
transfer_type='transfer',
|
||||
transfer_class_type='course_transfer',
|
||||
transfer_hours=transfer_hours,
|
||||
transfer_amount=transfer_amount,
|
||||
operator_id=session.get('user_id'),
|
||||
@@ -193,7 +306,10 @@ def register_routes(app):
|
||||
|
||||
students = Student.query.filter_by(status=1).all()
|
||||
courses = Course.query.filter_by(status=1).all()
|
||||
return render_template('transfer_form.html', students=students, courses=courses)
|
||||
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)
|
||||
|
||||
@app.route('/transfers/export', endpoint='transfer_export')
|
||||
@login_required
|
||||
|
||||
Reference in New Issue
Block a user