shcool/learning_center/models/course_teaching_class.py
liuhaoran 0b2a587b69 school_gengxin (#2)
Co-authored-by: hrrr <liuhaoran>
Reviewed-on: #2
2026-06-16 19:30:44 +08:00

295 lines
12 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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'}