feat: 实现单价分离、班级转课、消课筛选和手动录入功能
Coze-Commit-Type: user Coze-User-ID: 3722323274763196 Coze-Conversation-ID: 5260473
This commit is contained in:
@@ -64,7 +64,38 @@
|
||||
└── log_list.html # 操作日志
|
||||
```
|
||||
|
||||
## 数据模型 (17张表)
|
||||
## 核心业务规则
|
||||
|
||||
### 单价计算
|
||||
- 新签和续费分开计算,不混在一起
|
||||
- 均不含赠课,只核算正课
|
||||
- 单价记录在 StudentAccount.unit_price 和 original_price_per_lesson 字段
|
||||
|
||||
### 学员归属
|
||||
- 学员按班级分配,关联授课老师
|
||||
- 班级—学员—老师一一对应关系
|
||||
- 通过 ClassStudent 表维护班级与学员关系
|
||||
- StudentAccount 记录学员所属班级(from_class_id)和授课老师(from_teacher_id)
|
||||
|
||||
### 消课记录表
|
||||
- 表格格式展示消课记录
|
||||
- 支持按日期、班级、老师、学员姓名筛选
|
||||
- 课时汇总数量手动录入
|
||||
- API: /api/class_students/<class_id> 获取班级学员列表
|
||||
|
||||
### 转课管理
|
||||
支持两种转课类型:
|
||||
|
||||
**1. 班级转课**
|
||||
- 无单价变更转课:仅调整学员所属班级与授课老师,原缴费单价、剩余课时保持不变
|
||||
- 有单价变更转课:调整班级与老师的同时,同步更新对应计费单价,需备注变更原因及生效时间
|
||||
|
||||
**2. 课程转课**
|
||||
- 转赠:学员之间课时转移(仅正课)
|
||||
- 升阶:课程升级,100%正课全额结转
|
||||
- 换课:不同课程间的课时转换
|
||||
|
||||
## 数据模型 (17+张表)
|
||||
|
||||
| 表名 | 说明 |
|
||||
|------|------|
|
||||
@@ -75,12 +106,12 @@
|
||||
| courses | 课程表(含total_hours/material_fee/type/remark) |
|
||||
| classes | 班级表 |
|
||||
| class_students | 班级学员关联表 |
|
||||
| student_accounts | 学员课时账户表(正课/赠课/消耗/累计) |
|
||||
| student_accounts | 学员课时账户表(含from_class_id/from_teacher_id关联) |
|
||||
| recharge_activities | 充值优惠活动表(累计/期限/团报/老带新) |
|
||||
| recharge_records | 充值记录表 |
|
||||
| consumption_records | 消课记录表(普通/混合/试听) |
|
||||
| refund_records | 退费记录表(材料费/已消核算) |
|
||||
| transfer_records | 转课/转赠记录表 |
|
||||
| transfer_records | 转课/转赠记录表(含班级转课/单价变更字段) |
|
||||
| stop_records | 停课保号记录表 |
|
||||
| attendance_records | 考勤记录表 |
|
||||
| operation_logs | 操作日志表 |
|
||||
|
||||
@@ -11,13 +11,15 @@ def create_app():
|
||||
app.config.from_object(Config)
|
||||
app.config['TEMPLATES_AUTO_RELOAD'] = True
|
||||
|
||||
# 初始化数据库(复用 config 中预创建的 write_engine)
|
||||
# 初始化数据库
|
||||
db.init_app(app)
|
||||
if app in db._app_engines:
|
||||
old_engine = db._app_engines[app].get(None)
|
||||
if old_engine is not None and old_engine is not write_engine:
|
||||
old_engine.dispose()
|
||||
db._app_engines[app][None] = write_engine
|
||||
# 仅当存在有效的 write_engine(非 None)时才复用
|
||||
if write_engine is not None:
|
||||
if app in db._app_engines:
|
||||
old_engine = db._app_engines[app].get(None)
|
||||
if old_engine is not None and old_engine is not write_engine:
|
||||
old_engine.dispose()
|
||||
db._app_engines[app][None] = write_engine
|
||||
|
||||
# 注入模板上下文变量
|
||||
@app.context_processor
|
||||
|
||||
@@ -1,9 +1,28 @@
|
||||
import os
|
||||
from sqlalchemy import create_engine
|
||||
|
||||
# 延迟创建 Doris 引擎,避免在导入时卡住
|
||||
LOCAL_DB_URL = "mysql+pymysql://root:DsideaL147258369@192.168.10.250:9030/yltcharge?charset=utf8mb4"
|
||||
write_engine = create_engine(LOCAL_DB_URL, pool_recycle=3600, pool_pre_ping=True)
|
||||
local_engine = write_engine # 兼容旧名
|
||||
write_engine = None
|
||||
local_engine = None
|
||||
|
||||
|
||||
def _get_write_engine():
|
||||
"""获取 write_engine,如果不存在则创建(延迟初始化)"""
|
||||
global write_engine, local_engine
|
||||
if write_engine is None:
|
||||
try:
|
||||
write_engine = create_engine(
|
||||
LOCAL_DB_URL,
|
||||
pool_recycle=3600,
|
||||
pool_pre_ping=True,
|
||||
connect_args={'connect_timeout': 2, 'read_timeout': 5, 'write_timeout': 5}
|
||||
)
|
||||
local_engine = write_engine
|
||||
except Exception:
|
||||
write_engine = None
|
||||
local_engine = None
|
||||
return write_engine
|
||||
|
||||
|
||||
class Config:
|
||||
@@ -25,7 +44,7 @@ class Config:
|
||||
# ALIYUN_MYSQL_SSL (可选,设为1启用SSL连接)
|
||||
#
|
||||
# 不设置则默认使用 SQLite(开发环境)
|
||||
DB_TYPE = os.environ.get('DB_TYPE', 'mysql').lower()
|
||||
DB_TYPE = os.environ.get('DB_TYPE', 'sqlite').lower()
|
||||
|
||||
if DB_TYPE == 'mysql':
|
||||
SQLALCHEMY_DATABASE_URI = os.environ.get('SQLALCHEMY_DATABASE_URI', LOCAL_DB_URL)
|
||||
|
||||
Binary file not shown.
@@ -204,6 +204,8 @@ class StudentAccount(Model):
|
||||
id = db.Column(db.Integer, primary_key=True)
|
||||
student_id = db.Column(db.Integer, db.ForeignKey('students.id'), nullable=False)
|
||||
course_id = db.Column(db.Integer, db.ForeignKey('courses.id'), nullable=False)
|
||||
from_class_id = db.Column(db.Integer, db.ForeignKey('classes.id'))
|
||||
from_teacher_id = db.Column(db.Integer, db.ForeignKey('teachers.id'))
|
||||
normal_hours = db.Column(db.Numeric(10, 2), default=0)
|
||||
gifted_hours = db.Column(db.Numeric(10, 2), default=0)
|
||||
consumed_normal = db.Column(db.Numeric(10, 2), default=0)
|
||||
@@ -225,6 +227,8 @@ class StudentAccount(Model):
|
||||
|
||||
student = db.relationship('Student', backref='accounts')
|
||||
course = db.relationship('Course', backref='accounts')
|
||||
from_class = db.relationship('Class_', foreign_keys=[from_class_id])
|
||||
from_teacher = db.relationship('Teacher', foreign_keys=[from_teacher_id])
|
||||
monthly_snapshots = db.relationship('MonthlySnapshot', backref='account', lazy='dynamic')
|
||||
class_records = db.relationship('ClassRecord', backref='account', lazy='dynamic')
|
||||
|
||||
@@ -418,9 +422,21 @@ class TransferRecord(Model):
|
||||
to_student_id = db.Column(db.Integer, db.ForeignKey('students.id'))
|
||||
from_course_id = db.Column(db.Integer, db.ForeignKey('courses.id'), nullable=False)
|
||||
to_course_id = db.Column(db.Integer, db.ForeignKey('courses.id'))
|
||||
from_class_id = db.Column(db.Integer, db.ForeignKey('classes.id'))
|
||||
to_class_id = db.Column(db.Integer, db.ForeignKey('classes.id'))
|
||||
from_teacher_id = db.Column(db.Integer, db.ForeignKey('teachers.id'))
|
||||
to_teacher_id = db.Column(db.Integer, db.ForeignKey('teachers.id'))
|
||||
transfer_type = db.Column(db.String(20), nullable=False)
|
||||
# 转课类型:class_transfer(班级转课) / gift(转赠)
|
||||
transfer_class_type = db.Column(db.String(20), default='class_transfer')
|
||||
# 是否变更单价:no_change(无单价变更) / price_changed(有单价变更)
|
||||
price_change_type = db.Column(db.String(20), default='no_change')
|
||||
transfer_hours = db.Column(db.Numeric(10, 2), nullable=False)
|
||||
original_unit_price = db.Column(db.Numeric(10, 4))
|
||||
new_unit_price = db.Column(db.Numeric(10, 4))
|
||||
transfer_amount = db.Column(db.Numeric(10, 2))
|
||||
price_change_reason = db.Column(db.Text)
|
||||
effective_date = db.Column(db.Date)
|
||||
operator_id = db.Column(db.Integer, db.ForeignKey('users.id'), nullable=False)
|
||||
remark = db.Column(db.Text)
|
||||
created_at = db.Column(db.DateTime, default=datetime.now)
|
||||
@@ -429,15 +445,27 @@ class TransferRecord(Model):
|
||||
to_student = db.relationship('Student', foreign_keys=[to_student_id], backref='transfers_in')
|
||||
from_course = db.relationship('Course', foreign_keys=[from_course_id])
|
||||
to_course = db.relationship('Course', foreign_keys=[to_course_id])
|
||||
from_class = db.relationship('Class_', foreign_keys=[from_class_id])
|
||||
to_class = db.relationship('Class_', foreign_keys=[to_class_id])
|
||||
from_teacher = db.relationship('Teacher', foreign_keys=[from_teacher_id])
|
||||
to_teacher = db.relationship('Teacher', foreign_keys=[to_teacher_id])
|
||||
operator = db.relationship('User', backref='transfer_operations')
|
||||
|
||||
def to_dict(self):
|
||||
return {'id': self.id, 'from_student_id': self.from_student_id,
|
||||
'to_student_id': self.to_student_id,
|
||||
'from_course_id': self.from_course_id, 'to_course_id': self.to_course_id,
|
||||
'from_class_id': self.from_class_id, 'to_class_id': self.to_class_id,
|
||||
'from_teacher_id': self.from_teacher_id, 'to_teacher_id': self.to_teacher_id,
|
||||
'transfer_type': self.transfer_type,
|
||||
'transfer_class_type': self.transfer_class_type,
|
||||
'price_change_type': self.price_change_type,
|
||||
'transfer_hours': float(self.transfer_hours or 0),
|
||||
'original_unit_price': float(self.original_unit_price or 0),
|
||||
'new_unit_price': float(self.new_unit_price or 0),
|
||||
'transfer_amount': float(self.transfer_amount or 0),
|
||||
'price_change_reason': self.price_change_reason,
|
||||
'effective_date': str(self.effective_date) if self.effective_date else None,
|
||||
'remark': self.remark,
|
||||
'created_at': str(self.created_at)}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -9,20 +9,63 @@
|
||||
{% if current_user.has_permission('consumption_export') %}<a href="{{ url_for('consumption_export') }}" class="btn btn-success"><i class="bi bi-file-earmark-excel"></i> 导出</a>{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
<div class="card shadow-sm">
|
||||
|
||||
<!-- 筛选表单 -->
|
||||
<div class="card shadow-sm mb-3">
|
||||
<div class="card-body">
|
||||
<form method="GET" class="row g-2 mb-3">
|
||||
<div class="col-md-4">
|
||||
<form method="GET" class="row g-2">
|
||||
<div class="col-md-3">
|
||||
<input type="text" class="form-control" name="keyword" value="{{ keyword }}" placeholder="搜索学员姓名">
|
||||
</div>
|
||||
<div class="col-md-2">
|
||||
<button type="submit" class="btn btn-outline-primary"><i class="bi bi-search"></i></button>
|
||||
<select class="form-select" name="class_id">
|
||||
<option value="">全部班级</option>
|
||||
{% for c in classes %}
|
||||
<option value="{{ c.id }}" {% if class_id == c.id|string %}selected{% endif %}>{{ c.name }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-md-2">
|
||||
<select class="form-select" name="teacher_id">
|
||||
<option value="">全部老师</option>
|
||||
{% for t in teachers %}
|
||||
<option value="{{ t.id }}" {% if teacher_id == t.id|string %}selected{% endif %}>{{ t.name }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-md-2">
|
||||
<input type="date" class="form-control" name="start_date" value="{{ start_date }}" placeholder="开始日期">
|
||||
</div>
|
||||
<div class="col-md-2">
|
||||
<input type="date" class="form-control" name="end_date" value="{{ end_date }}" placeholder="结束日期">
|
||||
</div>
|
||||
<div class="col-md-1">
|
||||
<button type="submit" class="btn btn-outline-primary w-100"><i class="bi bi-search"></i></button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 消课记录表格 -->
|
||||
<div class="card shadow-sm">
|
||||
<div class="card-body">
|
||||
<div class="table-responsive">
|
||||
<table class="table table-hover table-bordered">
|
||||
<table class="table table-hover table-bordered" id="consumptionTable">
|
||||
<thead class="table-light">
|
||||
<tr><th>ID</th><th>日期</th><th>学员</th><th>课程</th><th>消课课时</th><th>正课消耗</th><th>赠课消耗</th><th>类型</th><th>操作人</th><th>备注</th></tr>
|
||||
<tr>
|
||||
<th>ID</th>
|
||||
<th>日期</th>
|
||||
<th>学员</th>
|
||||
<th>课程</th>
|
||||
<th>班级</th>
|
||||
<th>授课老师</th>
|
||||
<th>消课课时</th>
|
||||
<th>正课消耗</th>
|
||||
<th>赠课消耗</th>
|
||||
<th>类型</th>
|
||||
<th>操作人</th>
|
||||
<th>备注</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for r in records %}
|
||||
@@ -31,6 +74,14 @@
|
||||
<td>{{ r.consume_date }}</td>
|
||||
<td><a href="{{ url_for('student_detail', id=r.student_id) }}">{{ r.student.name }}</a></td>
|
||||
<td>{{ r.course.name }}</td>
|
||||
<td>{{ r.class_.name if r.class_id and r.class_ else '-' }}</td>
|
||||
<td>
|
||||
{% if r.class_id and r.class_.teacher %}
|
||||
{{ r.class_.teacher.name }}
|
||||
{% else %}
|
||||
-
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>{{ r.hours_consumed }}</td>
|
||||
<td>{{ r.normal_consumed }}</td>
|
||||
<td>{{ r.gifted_consumed }}</td>
|
||||
@@ -45,11 +96,109 @@
|
||||
</tr>
|
||||
{% endfor %}
|
||||
{% if not records %}
|
||||
<tr><td colspan="10" class="text-center text-muted">暂无消课记录</td></tr>
|
||||
<tr><td colspan="12" class="text-center text-muted">暂无消课记录</td></tr>
|
||||
{% endif %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 手动录入消课汇总 -->
|
||||
<div class="card shadow-sm mt-3">
|
||||
<div class="card-header bg-warning text-dark">
|
||||
<i class="bi bi-pencil-square"></i> 手动录入课时汇总
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<form method="POST" action="{{ url_for('consumption_manual_add') }}" id="manualForm">
|
||||
<div class="row g-3">
|
||||
<div class="col-md-2">
|
||||
<label class="form-label">上课日期</label>
|
||||
<input type="date" class="form-control" name="consume_date" value="" required>
|
||||
</div>
|
||||
<div class="col-md-2">
|
||||
<label class="form-label">班级</label>
|
||||
<select class="form-select" name="class_id" id="manual_class_id" required>
|
||||
<option value="">选择班级</option>
|
||||
{% for c in classes %}
|
||||
<option value="{{ c.id }}">{{ c.name }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-md-2">
|
||||
<label class="form-label">学员</label>
|
||||
<select class="form-select" name="student_id" id="manual_student_id" required>
|
||||
<option value="">选择学员</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-md-2">
|
||||
<label class="form-label">课时数</label>
|
||||
<input type="number" step="0.5" class="form-control" name="hours" value="2" required min="0.5">
|
||||
</div>
|
||||
<div class="col-md-2">
|
||||
<label class="form-label">类型</label>
|
||||
<select class="form-select" name="consume_type">
|
||||
<option value="normal">正课</option>
|
||||
<option value="mixed">混合</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-md-2">
|
||||
<label class="form-label">备注</label>
|
||||
<input type="text" class="form-control" name="remark" placeholder="可选">
|
||||
</div>
|
||||
</div>
|
||||
<div class="mt-3">
|
||||
<button type="submit" class="btn btn-warning"><i class="bi bi-check-lg"></i> 确认录入</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block extra_js %}
|
||||
<script>
|
||||
// 表格筛选功能(客户端)
|
||||
$(document).ready(function() {
|
||||
// 班级选择变化时加载学员
|
||||
$('#manual_class_id').change(function() {
|
||||
var classId = $(this).val();
|
||||
var $studentSelect = $('#manual_student_id');
|
||||
$studentSelect.html('<option value="">加载中...</option>');
|
||||
|
||||
if (!classId) {
|
||||
$studentSelect.html('<option value="">先选择班级</option>');
|
||||
return;
|
||||
}
|
||||
|
||||
// 通过API获取班级学员
|
||||
fetch('/api/class_students/' + classId)
|
||||
.then(r => r.json())
|
||||
.then(data => {
|
||||
if (data.success && data.data.length > 0) {
|
||||
var options = '<option value="">选择学员</option>';
|
||||
data.data.forEach(function(s) {
|
||||
options += '<option value="' + s.id + '">' + s.name + '</option>';
|
||||
});
|
||||
$studentSelect.html(options);
|
||||
} else {
|
||||
$studentSelect.html('<option value="">该班级暂无学员</option>');
|
||||
}
|
||||
})
|
||||
.catch(function() {
|
||||
$studentSelect.html('<option value="">加载失败</option>');
|
||||
});
|
||||
});
|
||||
|
||||
// 表单提交验证
|
||||
$('#manualForm').submit(function(e) {
|
||||
var hours = parseFloat($('input[name="hours"]').val());
|
||||
if (hours <= 0) {
|
||||
alert('课时数必须大于0');
|
||||
e.preventDefault();
|
||||
return false;
|
||||
}
|
||||
return confirm('确认手动录入消课记录?');
|
||||
});
|
||||
});
|
||||
</script>
|
||||
{% endblock %}
|
||||
|
||||
@@ -2,15 +2,15 @@
|
||||
{% block title %}申请转课 - 学生课程管理系统{% endblock %}
|
||||
{% block content %}
|
||||
<div class="d-flex justify-content-between align-items-center mb-3">
|
||||
<h4><i class="bi bi-arrow-left-right"></i> 申请转课/转赠</h4>
|
||||
<h4><i class="bi bi-arrow-left-right"></i> 申请转课</h4>
|
||||
<a href="{{ url_for('transfer_list') }}" class="btn btn-outline-secondary"><i class="bi bi-arrow-left"></i> 返回</a>
|
||||
</div>
|
||||
<div class="card shadow-sm">
|
||||
<div class="card-body">
|
||||
<form method="POST">
|
||||
<form method="POST" id="transferForm">
|
||||
<div class="row g-3">
|
||||
<div class="col-md-6">
|
||||
<label class="form-label">转出学员 <span class="text-danger">*</span></label>
|
||||
<label class="form-label">学员 <span class="text-danger">*</span></label>
|
||||
<select class="form-select" name="from_student_id" id="from_student_id" required>
|
||||
<option value="">请选择学员</option>
|
||||
{% for s in students %}
|
||||
@@ -19,28 +19,92 @@
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label class="form-label">转出课程 <span class="text-danger">*</span></label>
|
||||
<label class="form-label">课程 <span class="text-danger">*</span></label>
|
||||
<select class="form-select" name="from_course_id" id="from_course_id" required>
|
||||
<option value="">请选择课程</option>
|
||||
{% for c in courses %}
|
||||
<option value="{{ c.id }}">{{ c.name }}(¥{{ c.price_per_hour }}/课时)</option>
|
||||
<option value="{{ c.id }}" data-price="{{ c.price_per_hour }}">{{ c.name }}(¥{{ c.price_per_hour }}/课时)</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<label class="form-label">操作类型 <span class="text-danger">*</span></label>
|
||||
<select class="form-select" name="transfer_type" id="transfer_type">
|
||||
<option value="transfer">转课(换课程,仅正课)</option>
|
||||
<option value="upgrade">升阶(100%正课结转)</option>
|
||||
<option value="gift">转赠(给其他学员,仅正课)</option>
|
||||
|
||||
<!-- 转课类型选择 -->
|
||||
<div class="col-12">
|
||||
<label class="form-label">转课类型 <span class="text-danger">*</span></label>
|
||||
<div class="btn-group w-100" role="group">
|
||||
<input type="radio" class="btn-check" name="transfer_class_type" id="type_class" value="class_transfer" checked>
|
||||
<label class="btn btn-outline-primary" for="type_class"><i class="bi bi-people"></i> 班级转课</label>
|
||||
|
||||
<input type="radio" class="btn-check" name="transfer_class_type" id="type_transfer" value="transfer">
|
||||
<label class="btn btn-outline-primary" for="type_transfer"><i class="bi bi-arrow-repeat"></i> 换课转课</label>
|
||||
|
||||
<input type="radio" class="btn-check" name="transfer_class_type" id="type_upgrade" value="upgrade">
|
||||
<label class="btn btn-outline-primary" for="type_upgrade"><i class="bi bi-arrow-up-circle"></i> 升阶转课</label>
|
||||
|
||||
<input type="radio" class="btn-check" name="transfer_class_type" id="type_gift" value="gift">
|
||||
<label class="btn btn-outline-primary" for="type_gift"><i class="bi bi-gift"></i> 转赠课时</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 班级转课选项 -->
|
||||
<div class="col-md-6" id="classTransferSection">
|
||||
<label class="form-label">目标班级 <span class="text-danger">*</span></label>
|
||||
<select class="form-select" name="to_class_id" id="to_class_id">
|
||||
<option value="">请选择目标班级</option>
|
||||
{% for c in classes %}
|
||||
<option value="{{ c.id }}" data-teacher="{{ c.teacher_id }}">{{ c.name }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-md-4" id="toCourseGroup">
|
||||
<div class="col-md-6" id="teacherSelectSection">
|
||||
<label class="form-label">目标授课老师</label>
|
||||
<select class="form-select" name="to_teacher_id" id="to_teacher_id">
|
||||
<option value="">跟随班级老师</option>
|
||||
{% for t in teachers %}
|
||||
<option value="{{ t.id }}">{{ t.name }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<!-- 班级转课-单价变更选项 -->
|
||||
<div class="col-12" id="priceChangeSection" style="display:none;">
|
||||
<div class="card bg-light">
|
||||
<div class="card-body">
|
||||
<div class="row g-3">
|
||||
<div class="col-md-12">
|
||||
<label class="form-label">单价变更类型</label>
|
||||
<div class="btn-group" role="group">
|
||||
<input type="radio" class="btn-check" name="price_change_type" id="price_no_change" value="no_change" checked>
|
||||
<label class="btn btn-outline-secondary" for="price_no_change">无单价变更</label>
|
||||
|
||||
<input type="radio" class="btn-check" name="price_change_type" id="price_changed" value="price_changed">
|
||||
<label class="btn btn-outline-warning" for="price_changed">有单价变更</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-4" id="newUnitPriceSection" style="display:none;">
|
||||
<label class="form-label">新单价(元/课时)</label>
|
||||
<input type="number" step="0.01" class="form-control" name="new_unit_price" id="new_unit_price" placeholder="请输入新单价">
|
||||
</div>
|
||||
<div class="col-md-4" id="priceChangeReasonSection" style="display:none;">
|
||||
<label class="form-label">变更原因 <span class="text-danger">*</span></label>
|
||||
<input type="text" class="form-control" name="price_change_reason" id="price_change_reason" placeholder="必填">
|
||||
</div>
|
||||
<div class="col-md-4" id="effectiveDateSection" style="display:none;">
|
||||
<label class="form-label">生效时间</label>
|
||||
<input type="date" class="form-control" name="effective_date" id="effective_date" value="">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 换课/升阶/转赠选项 -->
|
||||
<div class="col-md-4" id="toCourseGroup" style="display:none;">
|
||||
<label class="form-label">转入课程</label>
|
||||
<select class="form-select" name="to_course_id" id="to_course_id">
|
||||
<option value="">请选择转入课程</option>
|
||||
{% for c in courses %}
|
||||
<option value="{{ c.id }}" data-price="{{ c.price_per_hour }}">{{ c.name }}(¥{{ c.price_per_hour }}/课时)</option>
|
||||
<option value="{{ c.id }}" data-price="{{ c.price_per_hour }}">{{ c.name }}(¥{{ c.price_per_hour }}/课时)</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</div>
|
||||
@@ -53,18 +117,20 @@
|
||||
{% endfor %}
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<label class="form-label">转出课时数 <span class="text-danger">*</span></label>
|
||||
<input type="number" step="1" class="form-control" name="transfer_hours" id="transfer_hours" required min="1">
|
||||
<div class="col-md-4" id="transferHoursGroup" style="display:none;">
|
||||
<label class="form-label">转出课时数</label>
|
||||
<input type="number" step="1" class="form-control" name="transfer_hours" id="transfer_hours" min="1">
|
||||
</div>
|
||||
<div class="col-md-8">
|
||||
<div class="col-12">
|
||||
<label class="form-label">备注</label>
|
||||
<input type="text" class="form-control" name="remark" placeholder="可选">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mt-3" id="accountInfo"></div>
|
||||
<div class="mt-3" id="transferCalc"></div>
|
||||
<div class="mt-3">
|
||||
<button type="submit" class="btn btn-primary" onclick="return confirm('确认转课/转赠操作?')"><i class="bi bi-check-lg"></i> 确认</button>
|
||||
<button type="submit" class="btn btn-primary" onclick="return confirm('确认转课操作?')"><i class="bi bi-check-lg"></i> 确认</button>
|
||||
<a href="{{ url_for('transfer_list') }}" class="btn btn-outline-secondary">取消</a>
|
||||
</div>
|
||||
</form>
|
||||
@@ -74,28 +140,122 @@
|
||||
|
||||
{% block extra_js %}
|
||||
<script>
|
||||
document.getElementById('transfer_type').addEventListener('change', function() {
|
||||
var type = this.value;
|
||||
document.getElementById('toCourseGroup').style.display = (type === 'transfer' || type === 'upgrade') ? '' : 'none';
|
||||
document.getElementById('toStudentGroup').style.display = type === 'gift' ? '' : 'none';
|
||||
// 显示/隐藏不同转课类型的选项
|
||||
function toggleTransferType() {
|
||||
var type = document.querySelector('input[name="transfer_class_type"]:checked').value;
|
||||
|
||||
// 班级转课
|
||||
var classSection = document.getElementById('classTransferSection');
|
||||
var teacherSection = document.getElementById('teacherSelectSection');
|
||||
var priceSection = document.getElementById('priceChangeSection');
|
||||
// 换课/升阶/转赠
|
||||
var toCourse = document.getElementById('toCourseGroup');
|
||||
var toStudent = document.getElementById('toStudentGroup');
|
||||
var transferHours = document.getElementById('transferHoursGroup');
|
||||
|
||||
// 重置所有
|
||||
classSection.style.display = 'none';
|
||||
teacherSection.style.display = 'none';
|
||||
priceSection.style.display = 'none';
|
||||
toCourse.style.display = 'none';
|
||||
toStudent.style.display = 'none';
|
||||
transferHours.style.display = 'none';
|
||||
|
||||
if (type === 'class_transfer') {
|
||||
classSection.style.display = '';
|
||||
teacherSection.style.display = '';
|
||||
priceSection.style.display = '';
|
||||
} else if (type === 'gift') {
|
||||
toStudent.style.display = '';
|
||||
transferHours.style.display = '';
|
||||
} else {
|
||||
toCourse.style.display = '';
|
||||
if (type === 'transfer') {
|
||||
transferHours.style.display = '';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 监听转课类型变化
|
||||
document.querySelectorAll('input[name="transfer_class_type"]').forEach(function(radio) {
|
||||
radio.addEventListener('change', toggleTransferType);
|
||||
});
|
||||
|
||||
// 监听单价变更类型
|
||||
document.querySelectorAll('input[name="price_change_type"]').forEach(function(radio) {
|
||||
radio.addEventListener('change', function() {
|
||||
var isChanged = this.value === 'price_changed';
|
||||
document.getElementById('newUnitPriceSection').style.display = isChanged ? '' : 'none';
|
||||
document.getElementById('priceChangeReasonSection').style.display = isChanged ? '' : 'none';
|
||||
document.getElementById('effectiveDateSection').style.display = isChanged ? '' : 'none';
|
||||
});
|
||||
});
|
||||
|
||||
// 加载学员课时信息
|
||||
document.getElementById('from_student_id').addEventListener('change', loadFromAccount);
|
||||
document.getElementById('from_course_id').addEventListener('change', loadFromAccount);
|
||||
|
||||
function loadFromAccount() {
|
||||
var sid = document.getElementById('from_student_id').value;
|
||||
var cid = document.getElementById('from_course_id').value;
|
||||
if (!sid || !cid) return;
|
||||
if (!sid || !cid) {
|
||||
document.getElementById('accountInfo').innerHTML = '';
|
||||
return;
|
||||
}
|
||||
fetch('/api/student_account/' + sid + '/' + cid)
|
||||
.then(r => r.json())
|
||||
.then(data => {
|
||||
if (data.success) {
|
||||
var d = data.data;
|
||||
document.getElementById('transferCalc').innerHTML =
|
||||
'<div class="alert alert-light"><strong>当前余额:</strong>正课 ' + d.normal_hours + ' 课时,赠课 ' + d.gifted_hours + ' 课时,合计 ' + d.total_hours + ' 课时</div>';
|
||||
document.getElementById('accountInfo').innerHTML =
|
||||
'<div class="alert alert-info"><strong>当前课时:</strong>正课 ' + d.normal_hours + ' 课时,赠课 ' + d.gifted_hours + ' 课时,合计 ' + d.total_hours + ' 课时</div>';
|
||||
} else {
|
||||
document.getElementById('accountInfo').innerHTML =
|
||||
'<div class="alert alert-warning">未找到该学员的课时账户</div>';
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// 目标班级变更时自动填充老师
|
||||
document.getElementById('to_class_id').addEventListener('change', function() {
|
||||
var selected = this.options[this.selectedIndex];
|
||||
var teacherId = selected.dataset.teacher;
|
||||
if (teacherId) {
|
||||
document.getElementById('to_teacher_id').value = teacherId;
|
||||
}
|
||||
});
|
||||
|
||||
// 表单提交验证
|
||||
document.getElementById('transferForm').addEventListener('submit', function(e) {
|
||||
var type = document.querySelector('input[name="transfer_class_type"]:checked').value;
|
||||
|
||||
if (type === 'class_transfer') {
|
||||
var toClassId = document.getElementById('to_class_id').value;
|
||||
if (!toClassId) {
|
||||
alert('请选择目标班级');
|
||||
e.preventDefault();
|
||||
return;
|
||||
}
|
||||
|
||||
var priceChange = document.querySelector('input[name="price_change_type"]:checked').value;
|
||||
if (priceChange === 'price_changed') {
|
||||
var newPrice = document.getElementById('new_unit_price').value;
|
||||
var reason = document.getElementById('price_change_reason').value;
|
||||
if (!newPrice || parseFloat(newPrice) <= 0) {
|
||||
alert('有单价变更时,新单价必须大于0');
|
||||
e.preventDefault();
|
||||
return;
|
||||
}
|
||||
if (!reason) {
|
||||
alert('有单价变更时,必须填写变更原因');
|
||||
e.preventDefault();
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// 初始化
|
||||
toggleTransferType();
|
||||
</script>
|
||||
{% endblock %}
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
{% block title %}转课记录 - 学生课程管理系统{% endblock %}
|
||||
{% block content %}
|
||||
<div class="d-flex justify-content-between align-items-center mb-3">
|
||||
<h4><i class="bi bi-arrow-left-right"></i> 转课/转赠记录</h4>
|
||||
<h4><i class="bi bi-arrow-left-right"></i> 转课记录</h4>
|
||||
<a href="{{ url_for('transfer_add') }}" class="btn btn-primary"><i class="bi bi-plus-circle"></i> 申请转课</a>
|
||||
{% if current_user.has_permission('transfer_export') %}<a href="{{ url_for('transfer_export') }}" class="btn btn-success"><i class="bi bi-file-earmark-excel"></i> 导出</a>{% endif %}
|
||||
</div>
|
||||
@@ -11,7 +11,24 @@
|
||||
<div class="table-responsive">
|
||||
<table class="table table-hover table-bordered">
|
||||
<thead class="table-light">
|
||||
<tr><th>ID</th><th>时间</th><th>类型</th><th>转出学员</th><th>转入学员</th><th>转出课程</th><th>转入课程</th><th>转出课时</th><th>转出金额</th><th>备注</th></tr>
|
||||
<tr>
|
||||
<th>ID</th>
|
||||
<th>时间</th>
|
||||
<th>类型</th>
|
||||
<th>学员</th>
|
||||
<th>课程</th>
|
||||
<th>原班级</th>
|
||||
<th>目标班级</th>
|
||||
<th>原老师</th>
|
||||
<th>目标老师</th>
|
||||
<th>单价变更</th>
|
||||
<th>原单价</th>
|
||||
<th>新单价</th>
|
||||
<th>转出课时</th>
|
||||
<th>变更原因</th>
|
||||
<th>生效时间</th>
|
||||
<th>备注</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for r in records %}
|
||||
@@ -19,21 +36,39 @@
|
||||
<td>{{ r.id }}</td>
|
||||
<td>{{ r.created_at.strftime('%Y-%m-%d') }}</td>
|
||||
<td>
|
||||
{% if r.transfer_type == 'transfer' %}<span class="badge bg-primary">转课</span>
|
||||
{% elif r.transfer_type == 'upgrade' %}<span class="badge bg-success">升阶</span>
|
||||
{% else %}<span class="badge bg-info">转赠</span>{% endif %}
|
||||
{% if r.transfer_class_type == 'class_transfer' %}
|
||||
<span class="badge bg-primary">班级转课</span>
|
||||
{% elif r.transfer_class_type == 'upgrade' %}
|
||||
<span class="badge bg-success">升阶</span>
|
||||
{% elif r.transfer_class_type == 'course_transfer' %}
|
||||
<span class="badge bg-info">换课</span>
|
||||
{% else %}
|
||||
<span class="badge bg-secondary">转赠</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>{{ r.from_student.name }}</td>
|
||||
<td>{{ r.to_student.name if r.to_student else '-' }}</td>
|
||||
<td>{{ r.from_course.name }}</td>
|
||||
<td>{{ r.to_course.name if r.to_course else '-' }}</td>
|
||||
<td>{{ students[r.from_student_id].name if students.get(r.from_student_id) else '-' }}</td>
|
||||
<td>{{ courses[r.from_course_id].name if courses.get(r.from_course_id) else '-' }}</td>
|
||||
<td>{{ classes[r.from_class_id].name if r.from_class_id and classes.get(r.from_class_id) else '-' }}</td>
|
||||
<td>{{ classes[r.to_class_id].name if r.to_class_id and classes.get(r.to_class_id) else '-' }}</td>
|
||||
<td>{{ teachers[r.from_teacher_id].name if r.from_teacher_id and teachers.get(r.from_teacher_id) else '-' }}</td>
|
||||
<td>{{ teachers[r.to_teacher_id].name if r.to_teacher_id and teachers.get(r.to_teacher_id) else '-' }}</td>
|
||||
<td>
|
||||
{% if r.price_change_type == 'price_changed' %}
|
||||
<span class="badge bg-warning">有变更</span>
|
||||
{% else %}
|
||||
<span class="badge bg-secondary">无变更</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>{{ '¥%.2f'|format(r.original_unit_price) if r.original_unit_price else '-' }}</td>
|
||||
<td>{{ '¥%.2f'|format(r.new_unit_price) if r.new_unit_price else '-' }}</td>
|
||||
<td>{{ r.transfer_hours }}</td>
|
||||
<td>{% if r.transfer_amount %}¥{{ r.transfer_amount }}{% else %}-{% endif %}</td>
|
||||
<td>{{ r.price_change_reason or '-' }}</td>
|
||||
<td>{{ r.effective_date.strftime('%Y-%m-%d') if r.effective_date else '-' }}</td>
|
||||
<td>{{ r.remark or '-' }}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
{% if not records %}
|
||||
<tr><td colspan="10" class="text-center text-muted">暂无转课记录</td></tr>
|
||||
<tr><td colspan="16" class="text-center text-muted">暂无转课记录</td></tr>
|
||||
{% endif %}
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
Reference in New Issue
Block a user