from odoo import models, fields, api from odoo.exceptions import ValidationError class EducationExamResult(models.Model): _name = 'education.exam.result' _description = 'Exam Result' _rec_name = 'student_id' student_id = fields.Many2one('education.student', string='Student', required=True) exam_id = fields.Many2one('education.exam', string='Exam', required=True, ondelete='cascade') total_max_marks = fields.Float(string='Total Maximum Marks', compute='_compute_totals', store=True) total_obtained_marks = fields.Float(string='Total Obtained Marks', compute='_compute_totals', store=True) percentage = fields.Float(string='Percentage', compute='_compute_totals', store=True) grade_id = fields.Many2one('education.grade', string='Grade', compute='_compute_totals', store=True) grade_point = fields.Float(string='Grade Point', compute='_compute_totals', store=True) result_status = fields.Selection([ ('pending', 'Pending / Not Evaluated'), ('pass', 'Pass'), ('fail', 'Fail') ], string='Result Status', compute='_compute_totals', store=True, default='pending') published = fields.Boolean(string='Published', default=False) published_date = fields.Date(string='Published Date') line_ids = fields.One2many('education.exam.result.line', 'result_id', string='Subject Marks') _unique_student_exam = models.Constraint( 'unique(student_id, exam_id)', 'A result already exists for this student for this exam!' ) def write(self, vals): for rec in self: if rec.published: if 'published' in vals and not vals['published']: if not self.env.user.has_group('education_exam.group_education_exam_administrator'): raise ValidationError("Only an Examination Administrator can unpublish exam results.") else: locked_fields = {'student_id', 'exam_id', 'line_ids'} if any(f in vals for f in locked_fields): raise ValidationError( f"Results for student '{rec.student_id.full_name}' are published and locked. " "An Examination Administrator must unpublish results first." ) return super().write(vals) def unlink(self): for rec in self: if rec.published: raise ValidationError( f"Cannot delete published results for student '{rec.student_id.full_name}'. " "Unpublish results first." ) return super().unlink() @api.depends('line_ids.max_marks', 'line_ids.obtained_marks', 'line_ids.status', 'exam_id.grading_system_id') def _compute_totals(self): for rec in self: max_marks = sum(rec.line_ids.mapped('max_marks')) obtained_marks = sum(rec.line_ids.mapped('obtained_marks')) rec.total_max_marks = max_marks rec.total_obtained_marks = obtained_marks if max_marks > 0: rec.percentage = (obtained_marks / max_marks) * 100.0 else: rec.percentage = 0.0 # Determine overall grade if rec.exam_id and rec.exam_id.grading_system_id: grade = rec.exam_id.grading_system_id.get_grade(rec.percentage) if grade: rec.grade_id = grade.id rec.grade_point = grade.grade_point else: rec.grade_id = False rec.grade_point = 0.0 else: rec.grade_id = False rec.grade_point = 0.0 # Overall status is pending if any subject is pending, Fail if any subject is failed, otherwise Pass if not rec.line_ids or any(line.status == 'pending' for line in rec.line_ids): rec.result_status = 'pending' elif any(line.status == 'fail' for line in rec.line_ids): rec.result_status = 'fail' else: rec.result_status = 'pass' class EducationExamResultLine(models.Model): _name = 'education.exam.result.line' _description = 'Exam Result Line' result_id = fields.Many2one('education.exam.result', string='Result', required=True, ondelete='cascade') subject_id = fields.Many2one('education.subject', string='Subject', required=True) exam_type_id = fields.Many2one('education.exam.type', string='Exam Type') max_marks = fields.Float(string='Maximum Marks', required=True) pass_marks = fields.Float(string='Pass Marks', required=True) obtained_marks = fields.Float(string='Obtained Marks', default=-1.0) # -1 indicates not entered percentage = fields.Float(string='Percentage', compute='_compute_grade_details', store=True) grade_id = fields.Many2one('education.grade', string='Grade', compute='_compute_grade_details', store=True) grade_point = fields.Float(string='Grade Point', compute='_compute_grade_details', store=True) status = fields.Selection([ ('pending', 'Pending / Not Evaluated'), ('pass', 'Pass'), ('fail', 'Fail') ], string='Status', compute='_compute_grade_details', store=True, default='pending') remarks = fields.Char(string='Remarks') _unique_result_subject = models.Constraint( 'unique(result_id, subject_id)', 'A mark entry for this subject already exists in this exam result!' ) def write(self, vals): for rec in self: if rec.result_id.published: raise ValidationError( f"Cannot modify marks for subject '{rec.subject_id.name}' because the exam results are published and locked. " "An Examination Administrator must unpublish results first." ) return super().write(vals) def unlink(self): for rec in self: if rec.result_id.published: raise ValidationError( f"Cannot delete marks for subject '{rec.subject_id.name}' because the exam results are published." ) return super().unlink() @api.constrains('obtained_marks', 'max_marks', 'pass_marks') def _check_marks(self): for record in self: if record.max_marks <= 0: raise ValidationError("Maximum marks must be greater than 0.") if record.pass_marks < 0: raise ValidationError("Pass marks cannot be negative.") if record.pass_marks > record.max_marks: raise ValidationError("Pass marks cannot exceed maximum marks.") # Using -1 to indicate not entered yet. So < -1 is invalid. if record.obtained_marks < -1.0: raise ValidationError("Obtained marks cannot be negative (use -1 for pending).") if record.obtained_marks > record.max_marks: raise ValidationError( f"Obtained marks ({record.obtained_marks}) cannot exceed maximum marks ({record.max_marks}) " f"for {record.subject_id.name}." ) @api.depends('obtained_marks', 'max_marks', 'pass_marks', 'result_id.exam_id.grading_system_id') def _compute_grade_details(self): for rec in self: if rec.obtained_marks >= 0 and rec.max_marks > 0: rec.percentage = (rec.obtained_marks / rec.max_marks) * 100.0 # Check pass/fail based on marks first if rec.obtained_marks >= rec.pass_marks: rec.status = 'pass' else: rec.status = 'fail' # Apply grading system if rec.result_id.exam_id.grading_system_id: grade = rec.result_id.exam_id.grading_system_id.get_grade(rec.percentage) if grade: rec.grade_id = grade.id rec.grade_point = grade.grade_point else: rec.grade_id = False rec.grade_point = 0.0 else: rec.grade_id = False rec.grade_point = 0.0 else: rec.percentage = 0.0 rec.grade_id = False rec.grade_point = 0.0 rec.status = 'pending'