EMS

Technical Reference: ems.meeting.presence (meeting attendance by NFC tag)

Overview

A presence session records who was convened to one meeting (a staff meeting, a department meeting, a training) and who confirmed their attendance. A laptop with a USB NFC reader at the door runs the kiosk, a public page, and each person passes the same tag they already use to clock in. This is not time tracking: nothing is ever written to hr.attendance.

The native attendance kiosk cannot be reused for this. Every scan there toggles check-in/check-out, so someone who clocked in on arrival would be clocked out by passing the tag at the door. What is reused is the tag’s identity (hr.employee.barcode, the tag’s UID) and the shape of the public kiosk (a token in the URL, a JSON scan route, sudo() on the server).

Module files: models/meetings/presence.py, models/meetings/presence_line.py, controllers/meeting_presence.py, static/src/js/frontend/meeting_presence_kiosk.js (+ static/src/xml/frontend/, static/src/scss/frontend/), reports/meetings/report_meeting_presence.xml, views/minutes_agreements/.


Data model

erDiagram
    "ems.meeting.presence" ||--o{ "ems.meeting.presence.line" : "line_ids"
    "ems.meeting.presence" }o--o| "ems.space" : "space_id"
    "ems.meeting.presence" }o--o| "ems.course" : "course_id"
    "ems.meeting.presence" }o--o| "hr.department" : "department_id (scope)"
    "ems.meeting.presence" }o--o| "ems.workgroup" : "workgroup_id (scope)"
    "ems.meeting.presence.line" }o--|| "hr.employee.public" : "employee_id"
    "ems.meeting.presence" }o--o| "hr.employee.public" : "convener_id"
    "ems.meeting.presence" }o--o{ "hr.employee.public" : "manager_ids"

ems.meeting.presence (_inherit = ['ems.base']: chatter, active)

Field Type Description
name Char What the meeting is, e.g. “Claustre”. Required
date Datetime Start of the meeting. Defaults to now
duration Float (hours) Source of truth of the length. Defaults to 2 (DEFAULT_DURATION); constraint: greater than 0
date_end Datetime (computed, stored, precompute, inverse) date + duration. Written by hand, its inverse turns it into the duration, so either can be set; moving date moves the whole window and keeps the length. The form’s onchange('date_end') runs the same inverse so the duration updates as the user types
space_id Many2one → ems.space Optional room
course_id Many2one → ems.course Defaults to the current course
company_id Many2one → res.company Defaults to the current company; the scanned tag is only looked up among this company’s employees
convener_id Many2one → hr.employee.public Who convenes the meeting. Defaults to the creator’s employee, editable (the secretariat may create a meeting on someone else’s behalf). Decides who can open the kiosk from the meetings page, see below
manager_ids Many2many → hr.employee.public Other people who can also open the kiosk from the meetings page
scope Selection Who is convened: all_teachers (default), all_staff, department, workgroup, manual
department_id / workgroup_id Many2one The target of the department / workgroup scope (required by a constraint for those scopes)
state Selection draft → open → closed, tracked
access_token Char Random, unique, copy=False; the kiosk’s only credential
kiosk_lang Selection The language the kiosk page speaks. The visitor is anonymous (the public user is en_US), so the session decides: the language of whoever created it, editable
kiosk_url Char (computed) <host>/ems/presence/<token>; the host is the one the request came in through, not web.base.url (a restored production database keeps production’s)
line_ids One2many One line per person
convened_count, present_count, pending_count, justified_count, absent_count Integer (computed) Counters shown in the form and on the kiosk

ems.meeting.presence.line

Field Type Description
presence_id Many2one ondelete='cascade'
employee_id Many2one → hr.employee.public Same comodel the minutes’ attendee lists use, so the two can be linked without a translation step
state Selection pending (default) / present / justified / absent
checkin_time Datetime When the tag was passed (or when it was marked by hand)
method Selection nfc or manual; empty unless the person is present
is_convened Boolean False for someone who passed the tag without being convened
notes Char e.g. why an absence is justified

UNIQUE (presence_id, employee_id): one line per person and session.


Rules


Kiosk

sequenceDiagram
    participant R as NFC reader (USB keyboard)
    participant P as Kiosk page (OWL public component)
    participant C as /ems/presence/<token>/scan
    participant M as ems.meeting.presence
    R->>P: keydown burst: the tag UID, then Enter
    P->>C: JSON-RPC {barcode}
    C->>M: sudo()._ems_register_scan(barcode)
    M-->>C: {status, employee_name, employee_avatar, counters, convened, attendees}
    C-->>P: same payload
    P->>P: card with name/photo for ~3 s, coloured by status

Meetings page (issue #526)

A fixed public address, /ems/meetings, that the computers with a reader keep open as their home page, so nobody has to carry each meeting’s kiosk link over to them. It reads tags like the kiosk (useTagReader()), and each tag gets the list of meetings its owner can open today, each a link to its kiosk.

sequenceDiagram
    participant R as Reader
    participant P as /ems/meetings (OWL public component)
    participant S as /ems/meetings/scan
    participant K as /ems/presence/<token>?back=1
    R->>P: tag UID + Enter
    P->>S: {barcode}
    S->>S: employee by barcode, _ems_meetings_run_by(employee)
    S-->>P: {status, employee_name, meetings: [{name, url, window, room, status}]}
    P->>K: click on a meeting (same tab)
    K->>P: "Meetings" link in the kiosk header (backUrl)

Access control

Role Sessions and lines
Director, Head of Studies (ems.group_head_of_studies) Full
Secretary (ems.group_secretary) Full
Academic administrator (ems.group_academic_admin) Full
Teacher and the rest None: they only pass their tag

The convener and managers of a meeting get no backend rights from those fields: they only decide who can open the kiosk from the meetings page.

There are no record rules: the sessions are centre-wide by nature (a staff meeting has no branch of the hierarchy to be scoped to), and the roles above are the ones that convene one. The quality administrator is deliberately left out for now: that role cannot read the courses, rooms, departments or workgroups a session points at (it implies neither ems.group_teacher nor ems.group_secretary), so opening a session would fail on them. It joins when the quality work’s minutes land (issue #497), which brings its own quality groups. The Meetings root menu and its Attendance entry are visible to the same groups.


Integration with the minutes (issue #497)

Built independently of the quality work’s minutes (ems.minute), deliberately aligned with it: the Meetings root menu uses the same XML ID (menu_minutes) and views/ folder (views/minutes_agreements/) that phase 2 uses, so both merge into one menu; the convened list uses the same comodel and the same scope semantics as the minutes’ attendee preloading. The steps to join them once #497 lands are in plans/meeting_presence_minute_integration.md.