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