metatroncubeswdev e5aba291f1 O4: mc_education_attendance - mobile-first bulk marking, own batches only
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>
2026-09-11 12:49:04 -04:00

43 lines
1.6 KiB
Python

from odoo import api, fields, models
class McAttendance(models.Model):
_name = "mc.attendance"
_inherit = ["mail.thread"]
_description = "Attendance"
_order = "date desc, student_id"
_rec_name = "display_name"
student_id = fields.Many2one("mc.student", string="Student", required=True, ondelete="restrict")
batch_id = fields.Many2one("mc.batch", string="Batch", required=True, ondelete="restrict")
date = fields.Date(string="Date", required=True, default=fields.Date.context_today)
session = fields.Char(
string="Session", required=True, default="Daily",
help='e.g. "Daily" for a single roll call, or "Period 2" for period-wise attendance.',
)
subject_id = fields.Many2one("mc.subject", string="Subject", ondelete="restrict")
state = fields.Selection(
[
("present", "Present"),
("absent", "Absent"),
("late", "Late"),
("on_leave", "On Leave"),
],
string="Status", required=True, default="present", tracking=True,
)
marked_by = fields.Many2one(
"res.users", string="Marked By", default=lambda self: self.env.user, readonly=True,
)
_student_date_session_uniq = models.Constraint(
"unique(student_id, date, session)",
"Attendance for this student on this date and session has already been recorded.",
)
@api.depends("student_id.name", "date", "session")
def _compute_display_name(self):
for attendance in self:
attendance.display_name = "%s - %s (%s)" % (
attendance.student_id.name or "?", attendance.date, attendance.session,
)