2026-09-25 18:00:58 +05:30

194 lines
9.3 KiB
Python

from odoo import models, fields, api
from odoo.exceptions import ValidationError
from datetime import datetime, timedelta
import pytz
class EducationExam(models.Model):
_name = 'education.exam'
_description = 'Examination'
name = fields.Char(string='Name', required=True)
code = fields.Char(string='Code', required=True)
academic_year_id = fields.Many2one('education.academic.year', string='Academic Year', required=True)
program_id = fields.Many2one('education.program', string='Program', required=True)
term_id = fields.Many2one('education.academic.term', string='Semester/Term', required=True)
exam_type_id = fields.Many2one('education.exam.type', string='Exam Type', required=True)
start_date = fields.Date(string='Start Date', required=True)
end_date = fields.Date(string='End Date', required=True)
grading_system_id = fields.Many2one('education.grading.system', string='Grading System', required=True)
state = fields.Selection([
('draft', 'Draft'),
('scheduled', 'Scheduled'),
('ongoing', 'Ongoing'),
('completed', 'Completed'),
('results_published', 'Results Published')
], string='Status', required=True, default='draft', tracking=True)
result_published_date = fields.Date(string='Result Published Date')
active = fields.Boolean(default=True)
line_ids = fields.One2many('education.exam.line', 'exam_id', string='Schedule')
result_ids = fields.One2many('education.exam.result', 'exam_id', string='Results')
def action_schedule(self):
for rec in self:
if not rec.line_ids:
raise ValidationError("Please add at least one subject to the schedule.")
rec.state = 'scheduled'
def action_start(self):
for rec in self:
if rec.state != 'scheduled':
raise ValidationError("Only scheduled exams can be started.")
earliest_line = min(rec.line_ids, key=lambda l: (l.exam_date, l.start_time), default=None)
if earliest_line:
# Get the user's timezone, fallback to UTC
user_tz = pytz.timezone(self.env.user.tz or 'UTC')
# Create naive datetime from the earliest exam_date and start_time (which is in hours)
earliest_dt_naive = datetime.combine(earliest_line.exam_date, datetime.min.time()) + timedelta(hours=earliest_line.start_time)
# Localize this naive datetime to the user's timezone
earliest_dt_user = user_tz.localize(earliest_dt_naive)
# Get current time in UTC, and convert to user's timezone for comparison
now_utc = fields.Datetime.now()
now_user = pytz.utc.localize(now_utc).astimezone(user_tz)
if now_user < earliest_dt_user:
raise ValidationError(f"This exam cannot be started yet. It is scheduled to start on {earliest_dt_user.strftime('%d/%m/%Y at %I:%M %p')}.")
rec.state = 'ongoing'
def action_complete(self):
for rec in self:
if rec.state != 'ongoing':
raise ValidationError("Only ongoing exams can be completed.")
rec.state = 'completed'
def action_generate_results(self):
"""Generates empty result records for all students enrolled in the program and term."""
for rec in self:
if rec.state != 'completed':
raise ValidationError("Results can only be generated for completed exams.")
students = self.env['education.student'].search([
('program_id', '=', rec.program_id.id),
('academic_year_id', '=', rec.academic_year_id.id),
('academic_term_id', '=', rec.term_id.id),
('status', 'in', ['active', 'enrolled', 'admitted'])
])
if not students:
raise ValidationError("No active/enrolled students found for this Program, Academic Year, and Semester.")
for student in students:
# Check if result already exists
existing = self.env['education.exam.result'].search([
('student_id', '=', student.id),
('exam_id', '=', rec.id)
])
if not existing:
result = self.env['education.exam.result'].create({
'student_id': student.id,
'exam_id': rec.id,
})
# Create result lines based on schedule
for line in rec.line_ids:
self.env['education.exam.result.line'].create({
'result_id': result.id,
'subject_id': line.subject_id.id,
'exam_type_id': rec.exam_type_id.id,
'max_marks': line.max_marks,
'pass_marks': line.pass_marks,
})
def action_publish_results(self):
for rec in self:
# Validations before publishing
if not rec.result_ids:
raise ValidationError("No results generated yet.")
for result in rec.result_ids:
if not result.line_ids:
raise ValidationError(f"Result for student {result.student_id.name} has no subjects.")
if result.result_status == 'pending':
raise ValidationError(f"Cannot publish results: Student {result.student_id.name} has pending evaluations.")
for line in result.line_ids:
if line.obtained_marks < 0:
raise ValidationError(f"Marks not entered for {line.subject_id.name} for student {result.student_id.name}.")
if line.obtained_marks > line.max_marks:
raise ValidationError(f"Obtained marks cannot exceed maximum marks for {line.subject_id.name}.")
rec.write({
'state': 'results_published',
'result_published_date': fields.Date.today()
})
# Update publication status on results
rec.result_ids.write({
'published': True,
'published_date': fields.Date.today()
})
def action_reset_draft(self):
self.write({'state': 'draft'})
@api.constrains('start_date', 'end_date')
def _check_exam_dates(self):
for rec in self:
if rec.start_date and rec.end_date and rec.end_date < rec.start_date:
raise ValidationError("End date cannot be before the start date.")
class EducationExamLine(models.Model):
_name = 'education.exam.line'
_description = 'Exam Schedule Line'
exam_id = fields.Many2one('education.exam', string='Exam', required=True, ondelete='cascade')
subject_id = fields.Many2one('education.subject', string='Subject', required=True)
exam_date = fields.Date(string='Exam Date', required=True)
start_time = fields.Float(string='Start Time', required=True)
end_time = fields.Float(string='End Time', required=True)
max_marks = fields.Float(string='Maximum Marks', required=True)
pass_marks = fields.Float(string='Pass Marks', required=True)
room = fields.Char(string='Room')
venue = fields.Char(string='Venue')
faculty_id = fields.Many2one('res.users', string='Faculty/Invigilator')
@api.constrains('start_time', 'end_time')
def _check_times(self):
for record in self:
if record.end_time <= record.start_time:
raise ValidationError("End time must be greater than start time.")
@api.constrains('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 be greater than maximum marks.")
@api.constrains('exam_date')
def _check_date(self):
for record in self:
if record.exam_id and (record.exam_date < record.exam_id.start_date or record.exam_date > record.exam_id.end_date):
raise ValidationError("Exam date must fall within the exam start and end dates.")
@api.constrains('exam_date', 'start_time', 'end_time', 'exam_id')
def _check_schedule_conflict(self):
for rec in self:
domain = [
('id', '!=', rec.id),
('exam_date', '=', rec.exam_date),
('start_time', '<', rec.end_time),
('end_time', '>', rec.start_time),
('exam_id.program_id', '=', rec.exam_id.program_id.id),
('exam_id.academic_year_id', '=', rec.exam_id.academic_year_id.id),
('exam_id.term_id', '=', rec.exam_id.term_id.id),
]
conflicts = self.search(domain)
if conflicts:
conflict = conflicts[0]
raise ValidationError(f"Exam schedule conflict: '{rec.subject_id.name}' overlaps with '{conflict.subject_id.name}' for the same student group on {rec.exam_date}.")