ems.teachingems.teaching is the ternary relation teacher–group–subject: “this teacher teaches this subject to this group.” It is derived from, and kept in sync with, the teacher’s weekly schedule (_sync_from_schedule()) — not normally hand-maintained, though its own CRUD screen exists for direct edits.
Module file: models/employees/teaching.py — _inherit = ['ems.base'], EMS’s shared mixin (models/shared/base.py, itself built on mail.thread/mail.activity.mixin) used by several models across the app. It contributes active (archive flag), action_archive(), get_user_is_admin()/get_user_is_tutor()/get_user_is_tutor_of_self(), notify() (bus toast) and chatter()/chatter_exception() (mail-thread logging helpers) — none of which ems.teaching overrides or uses directly beyond inheriting active. ems.base doesn’t have its own dedicated technical doc yet (per the DTON roadmap, shared mixins are documented at their first consumer rather than getting a dedicated phase); this is that first mention.
| Field | Type | Required | Stored | Description |
|---|---|---|---|---|
teacher_id |
Many2one → hr.employee |
Yes | Yes | ondelete='cascade'; domain restricted to employee_type = 'teacher' |
group_id |
Many2one → ems.group |
Yes | Yes | ondelete='cascade' |
subject_id |
Many2one → ems.subject |
Yes | Yes | ondelete='cascade' |
inuse_group_ids |
Many2many → ems.group (computed, compute_sudo=True) |
— | No | Groups already assigned to this teacher for the same subject — used to filter the group selector in the form so the same pair can’t be picked twice |
display_name |
Char (computed) |
— | No | The subject’s own display_name |
_check_unique_active ConstraintOne active teaching entry per exact (teacher, group, subject) triple — a duplicate must be archived first, not silently allowed to coexist.
flowchart TD
A[Save ems.teaching] --> B{Another active record with the\nsame teacher_id/group_id/subject_id?}
B -- Yes --> C[ValidationError]
B -- No --> D[OK]
_sync_from_schedule(teacher, entries) — the real source of truthShared by the working-schedule XML importer and the employee “Schedule” tab’s grid widget (see Working schedules) so teaching_ids always reflects what’s actually on the schedule instead of being maintained separately by hand in two places. Diffs the teacher’s current teaching_ids against the (subject_id, group_ids) pairs found in entries: unchanged pairs are left alone, new pairs are created, pairs no longer present are unlinked (not archived — the schedule is the single source of truth, so a stale teaching row has no reason to persist even as history).
Also called from the course transition wizard now (_apply_teaching_resync, see
course_transition_wizard.md) and from ems.attendance_template._regenerate_all_from_calendars()
— both read hr.employee._teaching_entries_from_calendar() (a teacher’s current calendar,
translated into this same entries shape) as their source of truth, added 2026-09-01 after
ems.teaching was found to never get resynced by either (see
plans/course_transition_stale_teacher_assignments.md).
Runs as superuser, and is private for that reason (issue #531). Who may change a teacher’s
teaching assignments is decided by who may write that teacher’s calendar
(resource.calendar.attendance, Department Chief and above, plus TAC), which every caller has
already done with the user’s own rights before calling this. The sync that follows is derived
data: it creates and deletes ems.teaching rows, writes hr.employee.teaching_ids and, through
unlink() below, can clear an ems.group.tutor_id, none of which a schedule editor holds direct
rights on. So the method runs on teacher.sudo(), and has a leading underscore so it can’t be
called over RPC with an arbitrary teacher. Don’t fix a schedule editor’s AccessError here by
adding rights to this model’s ACL: Head of Studies broke twice that way, each fix granting only
the one right that happened to fail (the save deletes rows whenever a subject or group is dropped
from the grid). tests/test_schedule_edit_roles.py saves real schedules as every editor role, and
tests/test_working_schedule_role_edit_tour.py does it through the browser as Department Chief.
unlink() — clearing a stale group tutor (2026-09-01)A group’s tutoring assignment is itself recorded as an ordinary ems.teaching row on the
group’s own tutoring subject (ems.subject.is_tutorship) — there is no stored relation between
this model and ems.group.tutor_id, which predates the calendar-driven sync above and is set
directly on the group form. unlink() is overridden to check, for every row being removed whose
subject_id.is_tutorship is true, whether group_id.tutor_id still equals teacher_id — if so,
it clears it (a plain write(), so EmsGroup._sync_tutor_role() runs exactly as it would for a
manual reassignment). Captured before the actual delete (fields are unreadable on a gone
record), applied only after, and only if tutor_id hasn’t already moved on to someone else in
the meantime. Since unlink() is the one choke point every removal path already goes through —
_sync_from_schedule()’s own drop, the two calendar-driven resyncs above, or a direct admin
delete — this single override is what keeps a group’s displayed tutor honest everywhere, without
any “group has 0 students” heuristic anywhere in the codebase.
Defined in security/ir.model.access.csv. These rights only govern direct edits (the “Subject
assignation” screen); the schedule-driven sync above doesn’t depend on them.
| Role | Create | Read | Write | Delete | Group XML ID |
|---|---|---|---|---|---|
| Administrator | ✓ | ✓ | ✓ | ✓ | ems.group_academic_admin |
| Head of Studies / Deputy / Director | ✓ | ✓ | ✓ | — | ems.group_head_of_studies |
| Teacher | — | ✓ | — | — | ems.group_teacher |
| Secretary | — | ✓ | — | — | ems.group_secretary |
| View | File | Notes |
|---|---|---|
| List | views/community/teaching/list.xml |
Also embedded read-only in hr.employee’s own “Teaching” tab (views/community/employee/form.xml) |
| Form | views/community/teaching/form.xml |
— |
| Action + Menu | views/community/teaching/menu.xml |
— |