Serves Demo Scene 4: mc.attendance (student x date x session, unique constraint so resubmission can never duplicate) plus a bulk-marking wizard - pick a batch/date/session, the roster loads pre-filled Present (or whatever was already recorded, if reopening), tap to change an exception, Submit. Submitting again for the same batch/date/session updates the same rows rather than creating duplicates - verified with a real test that marks a batch present, then reopens and corrects one student, then asserts there are still exactly two rows, not three. The "teachers may only mark their own batches" rule is a record rule (ir.rule scoped to group_teacher via batch_id.class_teacher_id. user_id), not a UI check, per CLAUDE.md sec 3 and the O4 spec line verbatim. Verified for real, not just declared: a teacher user who is not the class teacher of a batch gets AccessError on create *and* on reading an existing attendance row by id directly (the actual "cannot open another teacher's batch by editing the URL" scenario), while Administrator remains unrestricted since the rule's `groups` field scopes it to teacher only. Caught two Odoo 19 search-view schema changes while installing against a live odoo:19.0 container - a plain read of the view XML wouldn't have caught these, only trying to actually load it did: neither the group-by `<group>` element nor the filter groups inside a `<search>` view accept a `string` or `expand` attribute anymore (confirmed against hr's own search views, which use bare `<group>`). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
91 lines
4.0 KiB
Python
91 lines
4.0 KiB
Python
from odoo.exceptions import AccessError
|
|
from odoo.tests.common import TransactionCase
|
|
|
|
|
|
class TestAttendanceAccess(TransactionCase):
|
|
|
|
@classmethod
|
|
def setUpClass(cls):
|
|
super().setUpClass()
|
|
cls.year = cls.env["mc.academic.year"].create({
|
|
"name": "TEST-ATT-ACC-2026-27",
|
|
"date_start": "2026-06-01", "date_end": "2027-04-30",
|
|
})
|
|
cls.program = cls.env["mc.program"].create({
|
|
"name": "TEST ATT ACC Program", "code": "TEST-ATT-ACC-P1", "sequence_no": 1,
|
|
"display_label": "Test Grade",
|
|
})
|
|
|
|
teacher_group = cls.env.ref("mc_education_base.group_teacher").id
|
|
cls.user_a = cls.env["res.users"].create({
|
|
"name": "Teacher A", "login": "test_att_teacher_a",
|
|
"email": "test_att_teacher_a@example.com",
|
|
"group_ids": [(6, 0, [teacher_group])],
|
|
})
|
|
cls.employee_a = cls.env["hr.employee"].create({
|
|
"name": "Teacher A", "user_id": cls.user_a.id,
|
|
})
|
|
cls.user_b = cls.env["res.users"].create({
|
|
"name": "Teacher B", "login": "test_att_teacher_b",
|
|
"email": "test_att_teacher_b@example.com",
|
|
"group_ids": [(6, 0, [teacher_group])],
|
|
})
|
|
cls.employee_b = cls.env["hr.employee"].create({
|
|
"name": "Teacher B", "user_id": cls.user_b.id,
|
|
})
|
|
|
|
cls.batch_a = cls.env["mc.batch"].create({
|
|
"name": "TEST ATT ACC Batch A", "program_id": cls.program.id,
|
|
"year_id": cls.year.id, "class_teacher_id": cls.employee_a.id,
|
|
})
|
|
cls.batch_b = cls.env["mc.batch"].create({
|
|
"name": "TEST ATT ACC Batch B", "program_id": cls.program.id,
|
|
"year_id": cls.year.id, "class_teacher_id": cls.employee_b.id,
|
|
})
|
|
|
|
partner = cls.env["res.partner"].create({"name": "ATT ACC Student"})
|
|
cls.student = cls.env["mc.student"].create({
|
|
"partner_id": partner.id, "name": "ATT ACC Student",
|
|
})
|
|
cls.env["mc.enrollment"].create({
|
|
"student_id": cls.student.id, "program_id": cls.program.id,
|
|
"batch_id": cls.batch_a.id, "year_id": cls.year.id, "state": "active",
|
|
})
|
|
|
|
def test_teacher_can_mark_own_batch(self):
|
|
attendance = self.env["mc.attendance"].with_user(self.user_a).create({
|
|
"student_id": self.student.id, "batch_id": self.batch_a.id,
|
|
"date": "2026-09-01", "state": "present",
|
|
})
|
|
self.assertTrue(attendance)
|
|
attendance.with_user(self.user_a).write({"state": "absent"})
|
|
self.assertEqual(attendance.state, "absent")
|
|
|
|
def test_teacher_cannot_mark_another_teachers_batch(self):
|
|
with self.assertRaises(AccessError):
|
|
self.env["mc.attendance"].with_user(self.user_b).create({
|
|
"student_id": self.student.id, "batch_id": self.batch_a.id,
|
|
"date": "2026-09-01", "state": "present",
|
|
})
|
|
|
|
def test_teacher_cannot_read_another_teachers_batch_attendance(self):
|
|
# Created as admin (no record rule applies), then confirm teacher B
|
|
# cannot read it even by browsing the known record id directly -
|
|
# this is the "cannot open another teacher's batch by editing the
|
|
# URL" requirement from shared/DEMO_SCRIPT.md Scene 4.
|
|
attendance = self.env["mc.attendance"].create({
|
|
"student_id": self.student.id, "batch_id": self.batch_a.id,
|
|
"date": "2026-09-02", "state": "present",
|
|
})
|
|
with self.assertRaises(AccessError):
|
|
attendance.with_user(self.user_b).read(["state"])
|
|
|
|
def test_administrator_is_not_restricted_by_the_rule(self):
|
|
# The record rule is scoped to group_teacher only - Administrator
|
|
# (superuser in this test env) must remain unrestricted.
|
|
attendance = self.env["mc.attendance"].create({
|
|
"student_id": self.student.id, "batch_id": self.batch_b.id,
|
|
"date": "2026-09-03", "state": "present",
|
|
})
|
|
self.assertTrue(attendance)
|