295 lines
12 KiB
Python
295 lines
12 KiB
Python
from odoo import api, fields, models
|
||
from odoo.exceptions import ValidationError
|
||
import base64
|
||
from io import BytesIO
|
||
|
||
try:
|
||
import pandas as pd
|
||
except ImportError:
|
||
pd = None
|
||
|
||
|
||
class CourseTeachingClass(models.Model):
|
||
_name = 'course.teaching_class'
|
||
_description = '教学班'
|
||
_rec_name = 'display_name'
|
||
_order = 'course_id, semester, name'
|
||
_inherit = ['mail.thread', 'mail.activity.mixin']
|
||
|
||
# ==================== 显示名称 ====================
|
||
display_name = fields.Char(string='显示名称', compute='_compute_display_name', store=True)
|
||
name = fields.Char(string='教学班名称', required=True, help='如:1班、2班、周一班')
|
||
code = fields.Char(string='班级代码', help='如:CS101-01')
|
||
semester = fields.Selection([
|
||
('2024-2025-1', '2024-2025学年第一学期'),
|
||
('2024-2025-2', '2024-2025学年第二学期'),
|
||
('2025-2026-1', '2025-2026学年第一学期'),
|
||
('2025-2026-2', '2025-2026学年第二学期'),
|
||
], string='学期', required=True, default=lambda self: self._get_current_semester())
|
||
|
||
# ==================== 关联课程 ====================
|
||
course_id = fields.Many2one('learning.course', string='课程', required=True, ondelete='cascade')
|
||
course_name = fields.Char(related='course_id.name', string='课程名称')
|
||
|
||
@api.depends('course_name', 'name', 'semester')
|
||
def _compute_display_name(self):
|
||
for record in self:
|
||
semester_map = dict(self._fields['semester'].selection)
|
||
semester_name = semester_map.get(record.semester, '')
|
||
record.display_name = f"{record.course_name} - {record.name}({semester_name})"
|
||
|
||
# ==================== 基本信息 ====================
|
||
course_code = fields.Char(string='课程代码', related='course_id.code', store=True)
|
||
course_credit = fields.Float(string='学分', related='course_id.credit', store=True)
|
||
course_exam_type = fields.Selection(string='考核方式', related='course_id.exam_type', store=True)
|
||
|
||
# ==================== 教师信息 ====================
|
||
teacher_ids = fields.Many2many('hr.employee', string='授课教师',
|
||
relation='teaching_class_teacher_rel')
|
||
main_teacher_id = fields.Many2one('hr.employee', string='主讲教师')
|
||
assistant_ids = fields.Many2many('hr.employee', string='助教',
|
||
relation='teaching_class_assistant_rel')
|
||
|
||
# ==================== 学生信息(保留仅做展示,数据以 enrollment 为准) ====================
|
||
student_ids = fields.Many2many('student.info', string='选课学生',
|
||
relation='teaching_class_student_rel')
|
||
student_count = fields.Integer(string='选课人数', compute='_compute_student_count')
|
||
|
||
# ==================== 上课信息 ====================
|
||
schedule = fields.Text(string='上课时间地点', help='如:周一 1-2节 教学楼101')
|
||
start_date = fields.Date(string='开课日期')
|
||
end_date = fields.Date(string='结课日期')
|
||
total_weeks = fields.Integer(string='教学周数', default=16)
|
||
|
||
# ==================== 人数限制 ====================
|
||
max_students = fields.Integer(string='人数上限', default=60)
|
||
min_students = fields.Integer(string='开课人数下限', default=20)
|
||
is_full = fields.Boolean(string='是否满员', compute='_compute_is_full')
|
||
|
||
# ==================== 状态 ====================
|
||
state = fields.Selection([
|
||
('draft', '未开放'),
|
||
('open', '选课中'),
|
||
('teaching', '教学中'),
|
||
('closed', '已结课'),
|
||
('cancelled', '已取消'),
|
||
], string='状态', default='draft', tracking=True)
|
||
|
||
# ==================== 教学进度 ====================
|
||
current_chapter = fields.Integer(string='当前进度章节', help='当前教学进度')
|
||
progress_rate = fields.Float(string='教学进度(%)', compute='_compute_progress_rate')
|
||
|
||
# ==================== 统计字段 ====================
|
||
homework_count = fields.Integer(string='作业数量', compute='_compute_homework_count')
|
||
submitted_count = fields.Integer(string='已提交作业数', compute='_compute_homework_count')
|
||
resource_count = fields.Integer(string='资源数量', compute='_compute_resource_count')
|
||
|
||
# ==================== 关联子表(核心:所有业务依赖 enrollment_ids) ====================
|
||
homework_ids = fields.One2many('course.homework', 'teaching_class_id', string='班级作业')
|
||
student_score_ids = fields.One2many('course.student.score', 'teaching_class_id', string='班级成绩')
|
||
enrollment_ids = fields.One2many('course.teaching_class.enrollment', 'teaching_class_id', string='选课记录')
|
||
course_syllabus_ids = fields.One2many('course.syllabus', 'teaching_class_id', string='教案/大纲')
|
||
|
||
# ==================== 计算方法(全部走选课记录) ====================
|
||
def _compute_student_count(self):
|
||
for record in self:
|
||
valid_enroll = record.enrollment_ids.filtered(lambda e: e.state == 'enrolled')
|
||
record.student_count = len(valid_enroll)
|
||
|
||
def _compute_is_full(self):
|
||
for record in self:
|
||
record.is_full = record.max_students > 0 and record.student_count >= record.max_students
|
||
|
||
def _compute_progress_rate(self):
|
||
for record in self:
|
||
if record.total_weeks and record.current_chapter:
|
||
record.progress_rate = record.current_chapter / record.total_weeks * 100
|
||
else:
|
||
record.progress_rate = 0
|
||
|
||
def _compute_homework_count(self):
|
||
for record in self:
|
||
homeworks = self.env['course.homework'].search([('teaching_class_id', '=', record.id)])
|
||
record.homework_count = len(homeworks)
|
||
record.submitted_count = 0
|
||
|
||
def _compute_resource_count(self):
|
||
for record in self:
|
||
resources = self.env['course.chapter.resource'].search([
|
||
('teaching_class_id', '=', record.id)
|
||
])
|
||
record.resource_count = len(resources)
|
||
|
||
# ==================== 业务按钮方法 ====================
|
||
def action_open_enroll(self):
|
||
self.state = 'open'
|
||
|
||
def action_start_teaching(self):
|
||
self.state = 'teaching'
|
||
|
||
def action_close(self):
|
||
self.state = 'closed'
|
||
|
||
def action_cancel(self):
|
||
self.state = 'cancelled'
|
||
|
||
def action_add_students(self):
|
||
"""打开批量添加学生向导(底层批量生成选课记录)"""
|
||
return {
|
||
'type': 'ir.actions.act_window',
|
||
'name': '添加学生',
|
||
'res_model': 'course.teaching_class.enrollment.wizard',
|
||
'view_mode': 'form',
|
||
'target': 'new',
|
||
'context': {'default_teaching_class_id': self.id},
|
||
}
|
||
|
||
def action_import_students(self):
|
||
"""导入入口,复用向导"""
|
||
return self.action_add_students()
|
||
|
||
@api.model
|
||
def _get_current_semester(self):
|
||
import datetime
|
||
now = datetime.datetime.now()
|
||
year = now.year
|
||
if now.month >= 9:
|
||
return f'{year}-{year + 1}-1'
|
||
elif now.month >= 2:
|
||
return f'{year - 1}-{year}-2'
|
||
else:
|
||
return f'{year - 1}-{year}-1'
|
||
|
||
# 可选:双向同步 student_ids 和 enrollment_ids(保证两边显示一致,后期可删除 student_ids)
|
||
@api.depends('enrollment_ids.student_id')
|
||
def _sync_student_ids(self):
|
||
for rec in self:
|
||
stu_ids = rec.enrollment_ids.mapped('student_id').ids
|
||
rec.student_ids = [(6, 0, stu_ids)]
|
||
|
||
student_ids = fields.Many2many(
|
||
'student.info',
|
||
string='选课学生',
|
||
relation='teaching_class_student_rel',
|
||
compute='_sync_student_ids',
|
||
store=True
|
||
)
|
||
|
||
|
||
class CourseTeachingClassEnrollment(models.Model):
|
||
_name = 'course.teaching_class.enrollment'
|
||
_description = '教学班选课记录'
|
||
_rec_name = 'display_name'
|
||
_order = 'teaching_class_id, student_id'
|
||
|
||
display_name = fields.Char(string='显示名称', compute='_compute_display_name', store=True)
|
||
course_id = fields.Many2one(related="teaching_class_id.course_id", string="课程", store=True)
|
||
teaching_class_id = fields.Many2one('course.teaching_class', string='教学班', required=True, ondelete='cascade')
|
||
student_id = fields.Many2one('student.info', string='学生', required=True, ondelete='cascade')
|
||
stu_name = fields.Char(related='student_id.stu_name', string='学生姓名')
|
||
stu_phone = fields.Char(related='student_id.stu_phone', string='学生电话')
|
||
|
||
# 选课信息
|
||
enroll_date = fields.Date(string='选课日期', default=fields.Date.today)
|
||
state = fields.Selection([
|
||
('enrolled', '已选课'),
|
||
('dropped', '已退课'),
|
||
('failed', '不及格'),
|
||
('passed', '已通过'),
|
||
], string='状态', default='enrolled', tracking=True)
|
||
|
||
# 成绩(最终成绩)
|
||
final_score = fields.Float(string='最终成绩')
|
||
grade_level = fields.Selection([
|
||
('A', '优秀'),
|
||
('B', '良好'),
|
||
('C', '中等'),
|
||
('D', '及格'),
|
||
('F', '不及格'),
|
||
], string='等级')
|
||
|
||
@api.depends('teaching_class_id.display_name', 'student_id.stu_name')
|
||
def _compute_display_name(self):
|
||
for record in self:
|
||
record.display_name = f"{record.student_id.stu_name} - {record.teaching_class_id.display_name}"
|
||
|
||
@api.model
|
||
def create(self, vals):
|
||
"""新增选课记录校验人数上限"""
|
||
res = super().create(vals)
|
||
cls = res.teaching_class_id
|
||
valid_num = len(cls.enrollment_ids.filtered(lambda e: e.state == 'enrolled'))
|
||
if cls.max_students and valid_num > cls.max_students:
|
||
raise ValidationError("该教学班人数已达上限,无法继续添加学生!")
|
||
return res
|
||
|
||
|
||
# ==================== 批量添加/导入学生 向导(临时模型) ====================
|
||
class CourseTeachingClassEnrollmentWizard(models.TransientModel):
|
||
_name = 'course.teaching_class.enrollment.wizard'
|
||
_description = '批量添加&导入学生向导'
|
||
|
||
teaching_class_id = fields.Many2one(
|
||
'course.teaching_class', string='教学班', required=True
|
||
)
|
||
student_ids = fields.Many2many('student.info', string='选择学生')
|
||
file = fields.Binary(string='上传Excel文件')
|
||
filename = fields.Char(string='文件名')
|
||
|
||
def action_add_students(self):
|
||
"""手动勾选学生 → 批量生成选课记录"""
|
||
self.ensure_one()
|
||
cls = self.teaching_class_id
|
||
exist_stu_ids = set(cls.enrollment_ids.mapped('student_id').ids)
|
||
create_list = []
|
||
|
||
for stu in self.student_ids:
|
||
if stu.id not in exist_stu_ids:
|
||
create_list.append({
|
||
'teaching_class_id': cls.id,
|
||
'student_id': stu.id,
|
||
'state': 'enrolled'
|
||
})
|
||
|
||
if create_list:
|
||
self.env['course.teaching_class.enrollment'].create(create_list)
|
||
return {'type': 'ir.actions.act_window_close'}
|
||
|
||
def action_import_excel(self):
|
||
"""Excel导入 → 批量生成选课记录"""
|
||
self.ensure_one()
|
||
if not self.file:
|
||
raise ValidationError("请先选择Excel文件!")
|
||
if not pd:
|
||
raise ValidationError("缺少 pandas 库,请先安装:pip install pandas openpyxl")
|
||
|
||
cls = self.teaching_class_id
|
||
exist_stu_ids = set(cls.enrollment_ids.mapped('student_id').ids)
|
||
create_list = []
|
||
|
||
try:
|
||
file_data = base64.b64decode(self.file)
|
||
excel_file = BytesIO(file_data)
|
||
df = pd.read_excel(excel_file, engine='openpyxl')
|
||
|
||
for _, row in df.iterrows():
|
||
stu_code = str(row.get('学号', '')).strip()
|
||
if not stu_code:
|
||
continue
|
||
student = self.env['student.info'].search([('stu_id', '=', stu_code)], limit=1)
|
||
if student and student.id not in exist_stu_ids:
|
||
create_list.append({
|
||
'teaching_class_id': cls.id,
|
||
'student_id': student.id,
|
||
'state': 'enrolled'
|
||
})
|
||
except Exception as e:
|
||
raise ValidationError(f"解析Excel失败:{str(e)}")
|
||
|
||
if create_list:
|
||
self.env['course.teaching_class.enrollment'].create(create_list)
|
||
return {'type': 'ir.actions.act_window_close'}
|
||
|
||
|
||
|