O5: mc_education_timetable - one dataset, three views, conflict-checked

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>
This commit is contained in:
metatroncubeswdev 2026-09-11 12:53:47 -04:00
parent e5aba291f1
commit a36c89222b
16 changed files with 694 additions and 0 deletions

View File

@ -0,0 +1 @@
from . import models

View File

@ -0,0 +1,22 @@
{
"name": "School ERP - Timetable",
"version": "19.0.1.0.0",
"category": "Education",
"summary": "One timetable dataset, viewed by batch, by teacher, or by student.",
"author": "Metatroncube Software Solutions LLP",
"license": "Other proprietary",
"depends": ["mc_education_base"],
"data": [
"security/ir.model.access.csv",
"views/mc_timetable_slot_views.xml",
"views/mc_batch_views.xml",
"views/mc_teacher_views.xml",
"views/mc_student_views.xml",
"views/mc_education_timetable_menus.xml",
],
"demo": [
"demo/mc_timetable_slot_demo.xml",
],
"installable": True,
"application": False,
}

View File

@ -0,0 +1,245 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<!-- A full Mon-Fri week for Grade 8-A - a timetable is a recurring
weekly pattern, so one real week fully represents the dataset
(unlike attendance/fees, it doesn't need a "term of history").
Arun Prakash (the one demo teacher, a mathematics teacher) is
assigned to the Mathematics periods; the rest are unassigned
since no other demo teacher exists yet. -->
<record id="demo_slot_g8a_mon_p1" model="mc.timetable.slot">
<field name="batch_id" ref="mc_education_base.demo_batch_grade8_a"/>
<field name="year_id" ref="mc_education_base.demo_year_2026_27"/>
<field name="weekday">mon</field>
<field name="period">1</field>
<field name="subject_id" ref="mc_education_base.demo_subject_math"/>
<field name="teacher_id" ref="mc_education_base.demo_employee_arun_prakash"/>
<field name="room_id" ref="mc_education_base.demo_room_101"/>
</record>
<record id="demo_slot_g8a_mon_p2" model="mc.timetable.slot">
<field name="batch_id" ref="mc_education_base.demo_batch_grade8_a"/>
<field name="year_id" ref="mc_education_base.demo_year_2026_27"/>
<field name="weekday">mon</field>
<field name="period">2</field>
<field name="subject_id" ref="mc_education_base.demo_subject_english"/>
<field name="room_id" ref="mc_education_base.demo_room_101"/>
</record>
<record id="demo_slot_g8a_mon_p3" model="mc.timetable.slot">
<field name="batch_id" ref="mc_education_base.demo_batch_grade8_a"/>
<field name="year_id" ref="mc_education_base.demo_year_2026_27"/>
<field name="weekday">mon</field>
<field name="period">3</field>
<field name="subject_id" ref="mc_education_base.demo_subject_science"/>
<field name="room_id" ref="mc_education_base.demo_room_101"/>
</record>
<record id="demo_slot_g8a_mon_p4" model="mc.timetable.slot">
<field name="batch_id" ref="mc_education_base.demo_batch_grade8_a"/>
<field name="year_id" ref="mc_education_base.demo_year_2026_27"/>
<field name="weekday">mon</field>
<field name="period">4</field>
<field name="subject_id" ref="mc_education_base.demo_subject_social_studies"/>
<field name="room_id" ref="mc_education_base.demo_room_101"/>
</record>
<record id="demo_slot_g8a_mon_p5" model="mc.timetable.slot">
<field name="batch_id" ref="mc_education_base.demo_batch_grade8_a"/>
<field name="year_id" ref="mc_education_base.demo_year_2026_27"/>
<field name="weekday">mon</field>
<field name="period">5</field>
<field name="subject_id" ref="mc_education_base.demo_subject_hindi"/>
<field name="room_id" ref="mc_education_base.demo_room_101"/>
</record>
<record id="demo_slot_g8a_tue_p1" model="mc.timetable.slot">
<field name="batch_id" ref="mc_education_base.demo_batch_grade8_a"/>
<field name="year_id" ref="mc_education_base.demo_year_2026_27"/>
<field name="weekday">tue</field>
<field name="period">1</field>
<field name="subject_id" ref="mc_education_base.demo_subject_english"/>
<field name="room_id" ref="mc_education_base.demo_room_101"/>
</record>
<record id="demo_slot_g8a_tue_p2" model="mc.timetable.slot">
<field name="batch_id" ref="mc_education_base.demo_batch_grade8_a"/>
<field name="year_id" ref="mc_education_base.demo_year_2026_27"/>
<field name="weekday">tue</field>
<field name="period">2</field>
<field name="subject_id" ref="mc_education_base.demo_subject_math"/>
<field name="teacher_id" ref="mc_education_base.demo_employee_arun_prakash"/>
<field name="room_id" ref="mc_education_base.demo_room_101"/>
</record>
<record id="demo_slot_g8a_tue_p3" model="mc.timetable.slot">
<field name="batch_id" ref="mc_education_base.demo_batch_grade8_a"/>
<field name="year_id" ref="mc_education_base.demo_year_2026_27"/>
<field name="weekday">tue</field>
<field name="period">3</field>
<field name="subject_id" ref="mc_education_base.demo_subject_social_studies"/>
<field name="room_id" ref="mc_education_base.demo_room_101"/>
</record>
<record id="demo_slot_g8a_tue_p4" model="mc.timetable.slot">
<field name="batch_id" ref="mc_education_base.demo_batch_grade8_a"/>
<field name="year_id" ref="mc_education_base.demo_year_2026_27"/>
<field name="weekday">tue</field>
<field name="period">4</field>
<field name="subject_id" ref="mc_education_base.demo_subject_science"/>
<field name="room_id" ref="mc_education_base.demo_room_101"/>
</record>
<record id="demo_slot_g8a_tue_p5" model="mc.timetable.slot">
<field name="batch_id" ref="mc_education_base.demo_batch_grade8_a"/>
<field name="year_id" ref="mc_education_base.demo_year_2026_27"/>
<field name="weekday">tue</field>
<field name="period">5</field>
<field name="subject_id" ref="mc_education_base.demo_subject_hindi"/>
<field name="room_id" ref="mc_education_base.demo_room_101"/>
</record>
<record id="demo_slot_g8a_wed_p1" model="mc.timetable.slot">
<field name="batch_id" ref="mc_education_base.demo_batch_grade8_a"/>
<field name="year_id" ref="mc_education_base.demo_year_2026_27"/>
<field name="weekday">wed</field>
<field name="period">1</field>
<field name="subject_id" ref="mc_education_base.demo_subject_science"/>
<field name="room_id" ref="mc_education_base.demo_room_101"/>
</record>
<record id="demo_slot_g8a_wed_p2" model="mc.timetable.slot">
<field name="batch_id" ref="mc_education_base.demo_batch_grade8_a"/>
<field name="year_id" ref="mc_education_base.demo_year_2026_27"/>
<field name="weekday">wed</field>
<field name="period">2</field>
<field name="subject_id" ref="mc_education_base.demo_subject_social_studies"/>
<field name="room_id" ref="mc_education_base.demo_room_101"/>
</record>
<record id="demo_slot_g8a_wed_p3" model="mc.timetable.slot">
<field name="batch_id" ref="mc_education_base.demo_batch_grade8_a"/>
<field name="year_id" ref="mc_education_base.demo_year_2026_27"/>
<field name="weekday">wed</field>
<field name="period">3</field>
<field name="subject_id" ref="mc_education_base.demo_subject_math"/>
<field name="teacher_id" ref="mc_education_base.demo_employee_arun_prakash"/>
<field name="room_id" ref="mc_education_base.demo_room_101"/>
</record>
<record id="demo_slot_g8a_wed_p4" model="mc.timetable.slot">
<field name="batch_id" ref="mc_education_base.demo_batch_grade8_a"/>
<field name="year_id" ref="mc_education_base.demo_year_2026_27"/>
<field name="weekday">wed</field>
<field name="period">4</field>
<field name="subject_id" ref="mc_education_base.demo_subject_hindi"/>
<field name="room_id" ref="mc_education_base.demo_room_101"/>
</record>
<record id="demo_slot_g8a_wed_p5" model="mc.timetable.slot">
<field name="batch_id" ref="mc_education_base.demo_batch_grade8_a"/>
<field name="year_id" ref="mc_education_base.demo_year_2026_27"/>
<field name="weekday">wed</field>
<field name="period">5</field>
<field name="subject_id" ref="mc_education_base.demo_subject_english"/>
<field name="room_id" ref="mc_education_base.demo_room_101"/>
</record>
<record id="demo_slot_g8a_thu_p1" model="mc.timetable.slot">
<field name="batch_id" ref="mc_education_base.demo_batch_grade8_a"/>
<field name="year_id" ref="mc_education_base.demo_year_2026_27"/>
<field name="weekday">thu</field>
<field name="period">1</field>
<field name="subject_id" ref="mc_education_base.demo_subject_hindi"/>
<field name="room_id" ref="mc_education_base.demo_room_101"/>
</record>
<record id="demo_slot_g8a_thu_p2" model="mc.timetable.slot">
<field name="batch_id" ref="mc_education_base.demo_batch_grade8_a"/>
<field name="year_id" ref="mc_education_base.demo_year_2026_27"/>
<field name="weekday">thu</field>
<field name="period">2</field>
<field name="subject_id" ref="mc_education_base.demo_subject_science"/>
<field name="room_id" ref="mc_education_base.demo_room_101"/>
</record>
<record id="demo_slot_g8a_thu_p3" model="mc.timetable.slot">
<field name="batch_id" ref="mc_education_base.demo_batch_grade8_a"/>
<field name="year_id" ref="mc_education_base.demo_year_2026_27"/>
<field name="weekday">thu</field>
<field name="period">3</field>
<field name="subject_id" ref="mc_education_base.demo_subject_english"/>
<field name="room_id" ref="mc_education_base.demo_room_101"/>
</record>
<record id="demo_slot_g8a_thu_p4" model="mc.timetable.slot">
<field name="batch_id" ref="mc_education_base.demo_batch_grade8_a"/>
<field name="year_id" ref="mc_education_base.demo_year_2026_27"/>
<field name="weekday">thu</field>
<field name="period">4</field>
<field name="subject_id" ref="mc_education_base.demo_subject_math"/>
<field name="teacher_id" ref="mc_education_base.demo_employee_arun_prakash"/>
<field name="room_id" ref="mc_education_base.demo_room_101"/>
</record>
<record id="demo_slot_g8a_thu_p5" model="mc.timetable.slot">
<field name="batch_id" ref="mc_education_base.demo_batch_grade8_a"/>
<field name="year_id" ref="mc_education_base.demo_year_2026_27"/>
<field name="weekday">thu</field>
<field name="period">5</field>
<field name="subject_id" ref="mc_education_base.demo_subject_social_studies"/>
<field name="room_id" ref="mc_education_base.demo_room_101"/>
</record>
<record id="demo_slot_g8a_fri_p1" model="mc.timetable.slot">
<field name="batch_id" ref="mc_education_base.demo_batch_grade8_a"/>
<field name="year_id" ref="mc_education_base.demo_year_2026_27"/>
<field name="weekday">fri</field>
<field name="period">1</field>
<field name="subject_id" ref="mc_education_base.demo_subject_social_studies"/>
<field name="room_id" ref="mc_education_base.demo_room_101"/>
</record>
<record id="demo_slot_g8a_fri_p2" model="mc.timetable.slot">
<field name="batch_id" ref="mc_education_base.demo_batch_grade8_a"/>
<field name="year_id" ref="mc_education_base.demo_year_2026_27"/>
<field name="weekday">fri</field>
<field name="period">2</field>
<field name="subject_id" ref="mc_education_base.demo_subject_hindi"/>
<field name="room_id" ref="mc_education_base.demo_room_101"/>
</record>
<record id="demo_slot_g8a_fri_p3" model="mc.timetable.slot">
<field name="batch_id" ref="mc_education_base.demo_batch_grade8_a"/>
<field name="year_id" ref="mc_education_base.demo_year_2026_27"/>
<field name="weekday">fri</field>
<field name="period">3</field>
<field name="subject_id" ref="mc_education_base.demo_subject_science"/>
<field name="room_id" ref="mc_education_base.demo_room_101"/>
</record>
<record id="demo_slot_g8a_fri_p4" model="mc.timetable.slot">
<field name="batch_id" ref="mc_education_base.demo_batch_grade8_a"/>
<field name="year_id" ref="mc_education_base.demo_year_2026_27"/>
<field name="weekday">fri</field>
<field name="period">4</field>
<field name="subject_id" ref="mc_education_base.demo_subject_english"/>
<field name="room_id" ref="mc_education_base.demo_room_101"/>
</record>
<record id="demo_slot_g8a_fri_p5" model="mc.timetable.slot">
<field name="batch_id" ref="mc_education_base.demo_batch_grade8_a"/>
<field name="year_id" ref="mc_education_base.demo_year_2026_27"/>
<field name="weekday">fri</field>
<field name="period">5</field>
<field name="subject_id" ref="mc_education_base.demo_subject_math"/>
<field name="teacher_id" ref="mc_education_base.demo_employee_arun_prakash"/>
<field name="room_id" ref="mc_education_base.demo_room_101"/>
</record>
<!-- Grade 5-B: a smaller slice, enough to prove the dataset is not
Grade 8-A-only. -->
<record id="demo_slot_g5b_mon_p1" model="mc.timetable.slot">
<field name="batch_id" ref="mc_education_base.demo_batch_grade5_b"/>
<field name="year_id" ref="mc_education_base.demo_year_2026_27"/>
<field name="weekday">mon</field>
<field name="period">1</field>
<field name="subject_id" ref="mc_education_base.demo_subject_english"/>
<field name="room_id" ref="mc_education_base.demo_room_102"/>
</record>
<record id="demo_slot_g5b_mon_p2" model="mc.timetable.slot">
<field name="batch_id" ref="mc_education_base.demo_batch_grade5_b"/>
<field name="year_id" ref="mc_education_base.demo_year_2026_27"/>
<field name="weekday">mon</field>
<field name="period">2</field>
<field name="subject_id" ref="mc_education_base.demo_subject_science"/>
<field name="room_id" ref="mc_education_base.demo_room_102"/>
</record>
<record id="demo_slot_g5b_tue_p1" model="mc.timetable.slot">
<field name="batch_id" ref="mc_education_base.demo_batch_grade5_b"/>
<field name="year_id" ref="mc_education_base.demo_year_2026_27"/>
<field name="weekday">tue</field>
<field name="period">1</field>
<field name="subject_id" ref="mc_education_base.demo_subject_social_studies"/>
<field name="room_id" ref="mc_education_base.demo_room_102"/>
</record>
</odoo>

View File

@ -0,0 +1,4 @@
from . import mc_timetable_slot
from . import mc_batch
from . import mc_teacher
from . import mc_student

View File

@ -0,0 +1,16 @@
from odoo import models
class McBatch(models.Model):
_inherit = "mc.batch"
def action_view_timetable(self):
self.ensure_one()
return {
"type": "ir.actions.act_window",
"name": "Timetable - %s" % self.name,
"res_model": "mc.timetable.slot",
"view_mode": "list,form",
"domain": [("batch_id", "=", self.id)],
"context": {"default_batch_id": self.id, "search_default_group_weekday": 1},
}

View File

@ -0,0 +1,22 @@
from odoo import _, models
from odoo.exceptions import UserError
class McStudent(models.Model):
_inherit = "mc.student"
def action_view_timetable(self):
self.ensure_one()
enrollment = self.env["mc.enrollment"].search([
("student_id", "=", self.id), ("state", "=", "active"),
], limit=1)
if not enrollment:
raise UserError(_("%s has no active enrollment.") % self.name)
return {
"type": "ir.actions.act_window",
"name": "Timetable - %s" % self.name,
"res_model": "mc.timetable.slot",
"view_mode": "list,form",
"domain": [("batch_id", "=", enrollment.batch_id.id)],
"context": {"search_default_group_weekday": 1},
}

View File

@ -0,0 +1,16 @@
from odoo import models
class McTeacher(models.Model):
_inherit = "mc.teacher"
def action_view_timetable(self):
self.ensure_one()
return {
"type": "ir.actions.act_window",
"name": "Timetable - %s" % (self.name or ""),
"res_model": "mc.timetable.slot",
"view_mode": "list,form",
"domain": [("teacher_id", "=", self.employee_id.id)],
"context": {"search_default_group_weekday": 1},
}

View File

@ -0,0 +1,108 @@
from odoo import _, api, fields, models
from odoo.exceptions import ValidationError
WEEKDAYS = [
("mon", "Monday"),
("tue", "Tuesday"),
("wed", "Wednesday"),
("thu", "Thursday"),
("fri", "Friday"),
("sat", "Saturday"),
]
class McTimetableSlot(models.Model):
_name = "mc.timetable.slot"
_description = "Timetable Slot"
_order = "year_id desc, weekday, period"
_rec_name = "display_name"
batch_id = fields.Many2one("mc.batch", string="Batch", required=True, ondelete="cascade")
weekday = fields.Selection(WEEKDAYS, string="Weekday", required=True)
period = fields.Integer(string="Period", required=True)
subject_id = fields.Many2one("mc.subject", string="Subject", required=True, ondelete="restrict")
teacher_id = fields.Many2one("hr.employee", string="Teacher", ondelete="restrict")
room_id = fields.Many2one("mc.room", string="Room", ondelete="restrict")
year_id = fields.Many2one("mc.academic.year", string="Academic Year", required=True, ondelete="restrict")
term_id = fields.Many2one("mc.academic.term", string="Term", ondelete="restrict")
@api.depends("batch_id.name", "weekday", "period", "subject_id.name")
def _compute_display_name(self):
weekday_labels = dict(self._fields["weekday"].selection)
for slot in self:
slot.display_name = "%s - %s P%s - %s" % (
slot.batch_id.name or "?",
weekday_labels.get(slot.weekday, "?"),
slot.period,
slot.subject_id.name or "?",
)
def init(self):
# A teacher or a room cannot hold two slots in the same
# weekday+period (O5 spec, verbatim). Partial unique indexes are
# the actual guarantee; _check_no_conflict below only exists to
# turn the same violation into a message a scheduler can read,
# and - same lesson as mc.enrollment - has to run BEFORE the
# insert/update, because the index fires first otherwise.
self.env.cr.execute(
"CREATE UNIQUE INDEX IF NOT EXISTS mc_timetable_slot_teacher_no_clash "
"ON mc_timetable_slot (teacher_id, weekday, period, year_id) "
"WHERE teacher_id IS NOT NULL"
)
self.env.cr.execute(
"CREATE UNIQUE INDEX IF NOT EXISTS mc_timetable_slot_room_no_clash "
"ON mc_timetable_slot (room_id, weekday, period, year_id) "
"WHERE room_id IS NOT NULL"
)
self.env.cr.execute(
"CREATE UNIQUE INDEX IF NOT EXISTS mc_timetable_slot_batch_no_clash "
"ON mc_timetable_slot (batch_id, weekday, period, year_id)"
)
def _check_no_conflict(self, vals, exclude_id=None):
weekday = vals.get("weekday")
period = vals.get("period")
year_id = vals.get("year_id")
if not (weekday and period and year_id):
return
base_domain = [
("weekday", "=", weekday), ("period", "=", period), ("year_id", "=", year_id),
]
if exclude_id:
base_domain.append(("id", "!=", exclude_id))
batch_id = vals.get("batch_id")
if batch_id and self.search_count(base_domain + [("batch_id", "=", batch_id)]):
raise ValidationError(_(
"This batch already has a slot on this weekday and period."
))
teacher_id = vals.get("teacher_id")
if teacher_id and self.search_count(base_domain + [("teacher_id", "=", teacher_id)]):
raise ValidationError(_(
"This teacher already has a slot on this weekday and period."
))
room_id = vals.get("room_id")
if room_id and self.search_count(base_domain + [("room_id", "=", room_id)]):
raise ValidationError(_(
"This room is already booked for this weekday and period."
))
@api.model_create_multi
def create(self, vals_list):
for vals in vals_list:
self._check_no_conflict(vals)
return super().create(vals_list)
def write(self, vals):
if any(key in vals for key in ("weekday", "period", "year_id", "batch_id", "teacher_id", "room_id")):
for slot in self:
merged = {
"weekday": vals.get("weekday", slot.weekday),
"period": vals.get("period", slot.period),
"year_id": vals.get("year_id", slot.year_id.id),
"batch_id": vals.get("batch_id", slot.batch_id.id),
"teacher_id": vals.get("teacher_id", slot.teacher_id.id),
"room_id": vals.get("room_id", slot.room_id.id),
}
self._check_no_conflict(merged, exclude_id=slot.id)
return super().write(vals)

View File

@ -0,0 +1,5 @@
id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
access_mc_timetable_slot_administrator,mc.timetable.slot.administrator,model_mc_timetable_slot,mc_education_base.group_school_administrator,1,1,1,1
access_mc_timetable_slot_staff,mc.timetable.slot.staff,model_mc_timetable_slot,mc_education_base.group_school_staff,1,1,1,1
access_mc_timetable_slot_teacher,mc.timetable.slot.teacher,model_mc_timetable_slot,mc_education_base.group_teacher,1,0,0,0
access_mc_timetable_slot_accountant,mc.timetable.slot.accountant,model_mc_timetable_slot,mc_education_base.group_accountant,1,0,0,0
1 id name model_id:id group_id:id perm_read perm_write perm_create perm_unlink
2 access_mc_timetable_slot_administrator mc.timetable.slot.administrator model_mc_timetable_slot mc_education_base.group_school_administrator 1 1 1 1
3 access_mc_timetable_slot_staff mc.timetable.slot.staff model_mc_timetable_slot mc_education_base.group_school_staff 1 1 1 1
4 access_mc_timetable_slot_teacher mc.timetable.slot.teacher model_mc_timetable_slot mc_education_base.group_teacher 1 0 0 0
5 access_mc_timetable_slot_accountant mc.timetable.slot.accountant model_mc_timetable_slot mc_education_base.group_accountant 1 0 0 0

View File

@ -0,0 +1 @@
from . import test_timetable

View File

@ -0,0 +1,135 @@
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)])

View File

@ -0,0 +1,16 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<record id="view_mc_batch_form_inherit_mc_education_timetable" model="ir.ui.view">
<field name="name">mc.batch.form.inherit.mc.education.timetable</field>
<field name="model">mc.batch</field>
<field name="inherit_id" ref="mc_education_base.view_mc_batch_form"/>
<field name="arch" type="xml">
<xpath expr="//div[hasclass('oe_title')]" position="before">
<div class="oe_button_box" name="button_box">
<button name="action_view_timetable" type="object"
string="Timetable" class="oe_stat_button" icon="fa-calendar"/>
</div>
</xpath>
</field>
</record>
</odoo>

View File

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<menuitem id="menu_mc_timetable_slot" name="Timetable"
parent="mc_education_base.menu_school_root"
action="action_mc_timetable_slot" sequence="14"/>
</odoo>

View File

@ -0,0 +1,16 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<record id="view_mc_student_form_inherit_mc_education_timetable" model="ir.ui.view">
<field name="name">mc.student.form.inherit.mc.education.timetable</field>
<field name="model">mc.student</field>
<field name="inherit_id" ref="mc_education_base.view_mc_student_form"/>
<field name="arch" type="xml">
<xpath expr="//field[@name='photo']" position="before">
<div class="oe_button_box" name="button_box">
<button name="action_view_timetable" type="object"
string="Timetable" class="oe_stat_button" icon="fa-calendar"/>
</div>
</xpath>
</field>
</record>
</odoo>

View File

@ -0,0 +1,16 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<record id="view_mc_teacher_form_inherit_mc_education_timetable" model="ir.ui.view">
<field name="name">mc.teacher.form.inherit.mc.education.timetable</field>
<field name="model">mc.teacher</field>
<field name="inherit_id" ref="mc_education_base.view_mc_teacher_form"/>
<field name="arch" type="xml">
<xpath expr="//div[hasclass('oe_title')]" position="before">
<div class="oe_button_box" name="button_box">
<button name="action_view_timetable" type="object"
string="Timetable" class="oe_stat_button" icon="fa-calendar"/>
</div>
</xpath>
</field>
</record>
</odoo>

View File

@ -0,0 +1,65 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<record id="view_mc_timetable_slot_list" model="ir.ui.view">
<field name="name">mc.timetable.slot.list</field>
<field name="model">mc.timetable.slot</field>
<field name="arch" type="xml">
<list string="Timetable" default_order="weekday, period">
<field name="weekday"/>
<field name="period"/>
<field name="batch_id"/>
<field name="subject_id"/>
<field name="teacher_id"/>
<field name="room_id"/>
<field name="year_id"/>
</list>
</field>
</record>
<record id="view_mc_timetable_slot_form" model="ir.ui.view">
<field name="name">mc.timetable.slot.form</field>
<field name="model">mc.timetable.slot</field>
<field name="arch" type="xml">
<form string="Timetable Slot">
<group>
<group>
<field name="batch_id"/>
<field name="year_id"/>
<field name="term_id"/>
</group>
<group>
<field name="weekday"/>
<field name="period"/>
<field name="subject_id"/>
<field name="teacher_id"/>
<field name="room_id"/>
</group>
</group>
</form>
</field>
</record>
<record id="view_mc_timetable_slot_search" model="ir.ui.view">
<field name="name">mc.timetable.slot.search</field>
<field name="model">mc.timetable.slot</field>
<field name="arch" type="xml">
<search string="Timetable">
<field name="batch_id"/>
<field name="teacher_id"/>
<field name="subject_id"/>
<group>
<filter string="Weekday" name="group_weekday" context="{'group_by': 'weekday'}"/>
<filter string="Batch" name="group_batch" context="{'group_by': 'batch_id'}"/>
<filter string="Teacher" name="group_teacher" context="{'group_by': 'teacher_id'}"/>
</group>
</search>
</field>
</record>
<record id="action_mc_timetable_slot" model="ir.actions.act_window">
<field name="name">Timetable</field>
<field name="res_model">mc.timetable.slot</field>
<field name="view_mode">list,form</field>
<field name="context">{'search_default_group_weekday': 1}</field>
</record>
</odoo>