from odoo import models, fields, api from odoo.exceptions import ValidationError class EducationExamMarksEntryWizard(models.TransientModel): _name = 'education.exam.marks.entry.wizard' _description = 'Subject-wise Marks Entry Wizard' exam_id = fields.Many2one( 'education.exam', string='Exam', required=True, domain="[('state', 'in', ['ongoing', 'completed'])]" ) subject_id = fields.Many2one( 'education.subject', string='Subject', required=True ) max_marks = fields.Float(string='Maximum Marks', readonly=True) pass_marks = fields.Float(string='Pass Marks', readonly=True) faculty_id = fields.Many2one('res.users', string='Assigned Faculty', readonly=True) line_ids = fields.One2many( 'education.exam.marks.entry.wizard.line', 'wizard_id', string='Student Marks' ) @api.onchange('exam_id') def _onchange_exam_id(self): self.subject_id = False self.line_ids = [(5, 0, 0)] if self.exam_id: # Restrict subject domain to subjects scheduled in this exam subject_ids = self.exam_id.line_ids.mapped('subject_id.id') return {'domain': {'subject_id': [('id', 'in', subject_ids)]}} return {'domain': {'subject_id': []}} @api.onchange('exam_id', 'subject_id') def _onchange_exam_and_subject(self): if self.exam_id and self.subject_id: self._check_teacher_authorization() self._load_subject_details() self._load_student_lines() def _check_teacher_authorization(self): self.ensure_one() if not self.env.user.has_group('education_exam.group_education_exam_administrator'): schedule_line = self.env['education.exam.line'].search([ ('exam_id', '=', self.exam_id.id), ('subject_id', '=', self.subject_id.id), ], limit=1) if schedule_line and schedule_line.faculty_id and schedule_line.faculty_id != self.env.user: raise ValidationError( f"Access Denied: You are not authorized to enter marks for subject '{self.subject_id.name}'. " f"This subject is assigned to {schedule_line.faculty_id.name}." ) def _load_subject_details(self): schedule_line = self.env['education.exam.line'].search([ ('exam_id', '=', self.exam_id.id), ('subject_id', '=', self.subject_id.id), ], limit=1) if schedule_line: self.max_marks = schedule_line.max_marks self.pass_marks = schedule_line.pass_marks self.faculty_id = schedule_line.faculty_id.id else: self.max_marks = 0.0 self.pass_marks = 0.0 self.faculty_id = False def _load_student_lines(self): self.line_ids = [(5, 0, 0)] result_lines = self.env['education.exam.result.line'].sudo().search([ ('result_id.exam_id', '=', self.exam_id.id), ('subject_id', '=', self.subject_id.id), ]) lines = [] for rl in result_lines: student = rl.result_id.student_id.sudo() lines.append((0, 0, { 'result_line_id': rl.id, 'student_id': student.id, 'roll_number': student.roll_number or '', 'admission_number': student.admission_number or '', 'obtained_marks': rl.obtained_marks, 'remarks': rl.remarks or '', })) self.line_ids = lines def action_load_students(self): """Action button to reload student lines explicitly.""" self.ensure_one() self._check_teacher_authorization() self._load_subject_details() self._load_student_lines() return {'type': 'ir.actions.do_nothing'} def action_save_marks(self): self.ensure_one() self._check_teacher_authorization() if self.exam_id.state == 'results_published': raise ValidationError("Cannot save marks for published exams. Results must be unpublished first.") if not self.line_ids: raise ValidationError("No student marks to save. Please select a valid exam and subject, or generate results first.") for line in self.line_ids: if line.obtained_marks < -1.0: raise ValidationError( f"Marks for student '{line.student_id.full_name}' cannot be negative (use -1 for pending)." ) if line.obtained_marks > self.max_marks: raise ValidationError( f"Marks for student '{line.student_id.full_name}' ({line.obtained_marks}) " f"cannot exceed maximum marks ({self.max_marks})." ) if line.result_line_id: line.result_line_id.write({ 'obtained_marks': line.obtained_marks, 'remarks': line.remarks, }) return { 'type': 'ir.actions.client', 'tag': 'display_notification', 'params': { 'title': 'Marks Saved', 'message': f"Marks successfully saved for '{self.subject_id.name}'.", 'type': 'success', 'sticky': False, 'next': {'type': 'ir.actions.act_window_close'}, } } class EducationExamMarksEntryWizardLine(models.TransientModel): _name = 'education.exam.marks.entry.wizard.line' _description = 'Subject-wise Marks Entry Wizard Line' wizard_id = fields.Many2one( 'education.exam.marks.entry.wizard', string='Wizard', required=True, ondelete='cascade' ) result_line_id = fields.Many2one('education.exam.result.line', string='Exam Result Line') student_id = fields.Many2one('education.student', string='Student', required=True) roll_number = fields.Char(string='Roll No.', readonly=True) admission_number = fields.Char(string='Admission No.', readonly=True) obtained_marks = fields.Float(string='Obtained Marks', default=-1.0, required=True) remarks = fields.Char(string='Remarks')