183 lines
8.3 KiB
Python
183 lines
8.3 KiB
Python
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'
|