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>
109 lines
4.7 KiB
Python
109 lines
4.7 KiB
Python
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)
|