from odoo import models, fields, api from odoo.exceptions import ValidationError class EducationTimetableSlot(models.Model): _name = 'education.timetable.slot' _description = 'Timetable Slot' _order = 'date desc, start_time' # Academic References (Student Population) program_id = fields.Many2one('education.program', string='Program', required=True) academic_year_id = fields.Many2one('education.academic.year', string='Academic Year', required=True) term_id = fields.Many2one('education.academic.term', string='Term') batch_id = fields.Many2one('education.batch', string='Batch') semester_id = fields.Many2one('education.semester', string='Semester', index=True) section_id = fields.Many2one('education.section', string='Section', index=True) company_id = fields.Many2one( 'res.company', string='Company', required=True, default=lambda self: self.env.company, index=True, ) # Slot Details subject_id = fields.Many2one('education.subject', string='Subject', required=True) faculty_id = fields.Many2one('res.users', string='Faculty / Instructor', required=True) room_id = fields.Many2one('education.room', string='Room / Venue', required=True) # Schedule date = fields.Date(string='Date', required=True) start_time = fields.Float(string='Start Time', required=True) end_time = fields.Float(string='End Time', required=True) active = fields.Boolean(default=True) state = fields.Selection([ ('draft', 'Draft'), ('confirmed', 'Confirmed'), ('cancelled', 'Cancelled') ], string='Status', default='draft') @api.depends('subject_id.name', 'date', 'start_time') def _compute_display_name(self): for rec in self: subj = rec.subject_id.name or 'Class' d = str(rec.date) if rec.date else '' rec.display_name = f"{subj} ({d})" def action_confirm(self): for rec in self: rec.state = 'confirmed' def action_cancel(self): for rec in self: rec.state = 'cancelled' def action_draft(self): for rec in self: rec.state = 'draft' @api.constrains('start_time', 'end_time') def _check_time(self): for rec in self: if rec.start_time < 0.0 or rec.end_time > 24.0: raise ValidationError("Start and end times must be within a valid 24-hour day (0:00 to 24:00).") if rec.start_time >= rec.end_time: raise ValidationError("Start time must be strictly earlier than end time.") @api.constrains( 'date', 'start_time', 'end_time', 'faculty_id', 'room_id', 'program_id', 'academic_year_id', 'term_id', 'batch_id', 'semester_id', 'section_id', 'company_id', 'state', 'active' ) def _check_overlap(self): for rec in self: if rec.state == 'cancelled' or not rec.active: continue domain_base = [ ('id', '!=', rec.id), ('date', '=', rec.date), ('state', '!=', 'cancelled'), ('active', '=', True), # Interval logic: existing_start < new_end AND existing_end > new_start ('start_time', '<', rec.end_time), ('end_time', '>', rec.start_time), ] if rec.company_id: domain_base.append(('company_id', '=', rec.company_id.id)) # 1. Check Faculty Conflict faculty_conflict = self.search(domain_base + [('faculty_id', '=', rec.faculty_id.id)], limit=1) if faculty_conflict: raise ValidationError( f"Faculty conflict: {rec.faculty_id.name} already has an active class scheduled on {rec.date} overlapping this time." ) # 2. Check Room Conflict room_conflict = self.search(domain_base + [('room_id', '=', rec.room_id.id)], limit=1) if room_conflict: raise ValidationError( f"Room conflict: Room '{rec.room_id.name}' is already booked for an overlapping class on {rec.date}." ) # 3. Check Student Population Conflict # Population is defined by Program + Academic Year + Term + Batch + Semester + Section population_domain = domain_base + [ ('program_id', '=', rec.program_id.id), ('academic_year_id', '=', rec.academic_year_id.id), ] # OR logic for term_id: either matches exactly, or one of them is False if rec.term_id: population_domain.append('|') population_domain.append(('term_id', '=', False)) population_domain.append(('term_id', '=', rec.term_id.id)) # OR logic for batch_id: either matches exactly, or one of them is False if rec.batch_id: population_domain.append('|') population_domain.append(('batch_id', '=', False)) population_domain.append(('batch_id', '=', rec.batch_id.id)) # OR logic for semester_id: either matches exactly, or one of them is False if rec.semester_id: population_domain.append('|') population_domain.append(('semester_id', '=', False)) population_domain.append(('semester_id', '=', rec.semester_id.id)) # OR logic for section_id: either matches exactly, or one of them is False if rec.section_id: population_domain.append('|') population_domain.append(('section_id', '=', False)) population_domain.append(('section_id', '=', rec.section_id.id)) population_conflict = self.search(population_domain, limit=1) if population_conflict: raise ValidationError( f"Student group conflict: The academic cohort already has an overlapping class scheduled on {rec.date}." )