from psycopg2 import IntegrityError from odoo.exceptions import ValidationError from odoo.tests.common import TransactionCase from odoo.tools import mute_logger class TestTimetable(TransactionCase): @classmethod def setUpClass(cls): super().setUpClass() cls.year = cls.env["mc.academic.year"].create({ "name": "TEST-TT-2026-27", "date_start": "2026-06-01", "date_end": "2027-04-30", }) cls.program = cls.env["mc.program"].create({ "name": "TEST TT Program", "code": "TEST-TT-P1", "sequence_no": 1, "display_label": "Test Grade", }) cls.batch_a = cls.env["mc.batch"].create({ "name": "TEST TT Batch A", "program_id": cls.program.id, "year_id": cls.year.id, }) cls.batch_b = cls.env["mc.batch"].create({ "name": "TEST TT Batch B", "program_id": cls.program.id, "year_id": cls.year.id, }) cls.subject = cls.env["mc.subject"].create({"name": "TEST TT Subject", "code": "TEST-TT-S1"}) cls.teacher_employee = cls.env["hr.employee"].create({"name": "TEST TT Teacher"}) cls.room = cls.env["mc.room"].create({"name": "TEST TT Room"}) def test_teacher_double_booking_rejected(self): self.env["mc.timetable.slot"].create({ "batch_id": self.batch_a.id, "year_id": self.year.id, "weekday": "mon", "period": 1, "subject_id": self.subject.id, "teacher_id": self.teacher_employee.id, }) with self.assertRaises(ValidationError): self.env["mc.timetable.slot"].create({ "batch_id": self.batch_b.id, "year_id": self.year.id, "weekday": "mon", "period": 1, "subject_id": self.subject.id, "teacher_id": self.teacher_employee.id, }) def test_room_double_booking_rejected(self): self.env["mc.timetable.slot"].create({ "batch_id": self.batch_a.id, "year_id": self.year.id, "weekday": "mon", "period": 2, "subject_id": self.subject.id, "room_id": self.room.id, }) with self.assertRaises(ValidationError): self.env["mc.timetable.slot"].create({ "batch_id": self.batch_b.id, "year_id": self.year.id, "weekday": "mon", "period": 2, "subject_id": self.subject.id, "room_id": self.room.id, }) def test_batch_double_booking_rejected(self): self.env["mc.timetable.slot"].create({ "batch_id": self.batch_a.id, "year_id": self.year.id, "weekday": "mon", "period": 3, "subject_id": self.subject.id, }) with self.assertRaises(ValidationError): self.env["mc.timetable.slot"].create({ "batch_id": self.batch_a.id, "year_id": self.year.id, "weekday": "mon", "period": 3, "subject_id": self.subject.id, }) def test_same_teacher_different_period_is_allowed(self): self.env["mc.timetable.slot"].create({ "batch_id": self.batch_a.id, "year_id": self.year.id, "weekday": "mon", "period": 4, "subject_id": self.subject.id, "teacher_id": self.teacher_employee.id, }) # Should not raise: different period, same teacher. self.env["mc.timetable.slot"].create({ "batch_id": self.batch_b.id, "year_id": self.year.id, "weekday": "mon", "period": 5, "subject_id": self.subject.id, "teacher_id": self.teacher_employee.id, }) @mute_logger("odoo.sql_db") def test_db_index_backs_teacher_conflict_even_if_orm_check_bypassed(self): self.env["mc.timetable.slot"].create({ "batch_id": self.batch_a.id, "year_id": self.year.id, "weekday": "tue", "period": 1, "subject_id": self.subject.id, "teacher_id": self.teacher_employee.id, }) with self.assertRaises(IntegrityError): with self.cr.savepoint(): self.env.cr.execute( "INSERT INTO mc_timetable_slot " "(batch_id, year_id, weekday, period, subject_id, teacher_id, " " create_uid, write_uid, create_date, write_date) " "VALUES (%s, %s, 'tue', 1, %s, %s, %s, %s, now(), now())", (self.batch_b.id, self.year.id, self.subject.id, self.teacher_employee.id, self.env.uid, self.env.uid), ) def test_updating_into_a_conflict_is_rejected(self): self.env["mc.timetable.slot"].create({ "batch_id": self.batch_a.id, "year_id": self.year.id, "weekday": "wed", "period": 1, "subject_id": self.subject.id, "teacher_id": self.teacher_employee.id, }) other = self.env["mc.timetable.slot"].create({ "batch_id": self.batch_b.id, "year_id": self.year.id, "weekday": "wed", "period": 2, "subject_id": self.subject.id, "teacher_id": self.teacher_employee.id, }) with self.assertRaises(ValidationError): other.write({"period": 1}) def test_batch_view_timetable_resolves_own_slots_only(self): slot_a = self.env["mc.timetable.slot"].create({ "batch_id": self.batch_a.id, "year_id": self.year.id, "weekday": "thu", "period": 1, "subject_id": self.subject.id, }) self.env["mc.timetable.slot"].create({ "batch_id": self.batch_b.id, "year_id": self.year.id, "weekday": "thu", "period": 1, "subject_id": self.subject.id, }) action = self.batch_a.action_view_timetable() self.assertEqual(action["domain"], [("batch_id", "=", self.batch_a.id)]) found = self.env["mc.timetable.slot"].search(action["domain"]) self.assertEqual(found, slot_a) def test_student_view_timetable_resolves_through_active_enrollment(self): partner = self.env["res.partner"].create({"name": "TT Student"}) student = self.env["mc.student"].create({"partner_id": partner.id, "name": "TT Student"}) self.env["mc.enrollment"].create({ "student_id": student.id, "program_id": self.program.id, "batch_id": self.batch_a.id, "year_id": self.year.id, "state": "active", }) action = student.action_view_timetable() self.assertEqual(action["domain"], [("batch_id", "=", self.batch_a.id)])