Serves Demo Scene 7: mc.timetable.slot (batch, weekday, period,
subject, teacher, room, year, term) is the single dataset. "Three
read views" are three entry points into that same model rather than
three separate data structures - a stat button on mc.batch, on
mc.teacher, and on mc.student (resolved through the student's active
enrollment to their batch) each open the same list/search action with
a different domain. Auto-generation is out of scope per spec; this
module only configures slots by hand (demo data is a real Mon-Fri
week for Grade 8-A, since a timetable is a recurring weekly pattern
and one week fully represents it, unlike attendance/fees which
genuinely need a run of history).
Conflict prevention ("a teacher or a room cannot hold two slots in
the same weekday+period") follows the same two-layer pattern used for
mc.academic.year.is_current and mc.enrollment in O1: a partial unique
index per conflict type (teacher, room, and - not explicitly asked
for but an obvious extension of the same rule - batch, since a batch
can't be in two places at once either) is the actual guarantee, and a
pre-check in create()/write() raises a readable ValidationError
before the insert/update, not after, for the same reason established
building mc.enrollment: the DB index fires first and the friendly
message is unreachable otherwise.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
136 lines
6.3 KiB
Python
136 lines
6.3 KiB
Python
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)])
|