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>
43 lines
1.6 KiB
Python
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,
|
|
)
|