83 lines
4.0 KiB
Python
83 lines
4.0 KiB
Python
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')
|
|
|
|
# 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.constrains('start_time', 'end_time')
|
|
def _check_time(self):
|
|
for rec in self:
|
|
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')
|
|
def _check_overlap(self):
|
|
for rec in self:
|
|
domain_base = [
|
|
('date', '=', rec.date),
|
|
('id', '!=', rec.id),
|
|
# Interval logic: existing_start < new_end AND existing_end > new_start
|
|
('start_time', '<', rec.end_time),
|
|
('end_time', '>', rec.start_time),
|
|
]
|
|
|
|
# 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 a class overlapping this time on {rec.date}.")
|
|
|
|
# 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: {rec.room_id.name} is already booked for an overlapping time on {rec.date}.")
|
|
|
|
# 3. Check Student Population Conflict
|
|
# Population is defined by Program + Academic Year + Term + Batch
|
|
# If a slot has NO term, it applies to the whole year, so it should conflict with ANY term in that year.
|
|
# So if either the new slot or the existing slot has no term (or they match), it's a conflict.
|
|
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))
|
|
|
|
population_conflict = self.search(population_domain, limit=1)
|
|
if population_conflict:
|
|
raise ValidationError(f"Student Population conflict: This specific academic group already has a class overlapping this time on {rec.date}.")
|