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:
parent
e5aba291f1
commit
a36c89222b
1
addons/mc_education_timetable/__init__.py
Normal file
1
addons/mc_education_timetable/__init__.py
Normal file
@ -0,0 +1 @@
|
|||||||
|
from . import models
|
||||||
22
addons/mc_education_timetable/__manifest__.py
Normal file
22
addons/mc_education_timetable/__manifest__.py
Normal 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,
|
||||||
|
}
|
||||||
245
addons/mc_education_timetable/demo/mc_timetable_slot_demo.xml
Normal file
245
addons/mc_education_timetable/demo/mc_timetable_slot_demo.xml
Normal 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>
|
||||||
4
addons/mc_education_timetable/models/__init__.py
Normal file
4
addons/mc_education_timetable/models/__init__.py
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
from . import mc_timetable_slot
|
||||||
|
from . import mc_batch
|
||||||
|
from . import mc_teacher
|
||||||
|
from . import mc_student
|
||||||
16
addons/mc_education_timetable/models/mc_batch.py
Normal file
16
addons/mc_education_timetable/models/mc_batch.py
Normal 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},
|
||||||
|
}
|
||||||
22
addons/mc_education_timetable/models/mc_student.py
Normal file
22
addons/mc_education_timetable/models/mc_student.py
Normal 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},
|
||||||
|
}
|
||||||
16
addons/mc_education_timetable/models/mc_teacher.py
Normal file
16
addons/mc_education_timetable/models/mc_teacher.py
Normal 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},
|
||||||
|
}
|
||||||
108
addons/mc_education_timetable/models/mc_timetable_slot.py
Normal file
108
addons/mc_education_timetable/models/mc_timetable_slot.py
Normal 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)
|
||||||
@ -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
addons/mc_education_timetable/tests/__init__.py
Normal file
1
addons/mc_education_timetable/tests/__init__.py
Normal file
@ -0,0 +1 @@
|
|||||||
|
from . import test_timetable
|
||||||
135
addons/mc_education_timetable/tests/test_timetable.py
Normal file
135
addons/mc_education_timetable/tests/test_timetable.py
Normal 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)])
|
||||||
16
addons/mc_education_timetable/views/mc_batch_views.xml
Normal file
16
addons/mc_education_timetable/views/mc_batch_views.xml
Normal 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>
|
||||||
@ -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>
|
||||||
16
addons/mc_education_timetable/views/mc_student_views.xml
Normal file
16
addons/mc_education_timetable/views/mc_student_views.xml
Normal 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>
|
||||||
16
addons/mc_education_timetable/views/mc_teacher_views.xml
Normal file
16
addons/mc_education_timetable/views/mc_teacher_views.xml
Normal 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>
|
||||||
@ -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>
|
||||||
Loading…
x
Reference in New Issue
Block a user