140 lines
6.0 KiB
Python
140 lines
6.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')
|
|
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}."
|
|
)
|