EMS

Technical Reference: ems.convalidation

Overview

A convalidation request is a student asking for some subjects (vocational training modules) of their study to be recognised because they already passed them elsewhere. It is filed from the portal during the yearly request period set in the EMS settings (or the secretariat registers one received on paper, at any time): an adult student files it himself, and so does his family when he authorized sharing with it; a minor’s family files it for him (see Portal).

Resolving it follows the centre’s official circuit. The Deputy Head of Studies reviews every request and keeps it until it is resolved, deciding subject by subject (the grade the previous studies hold, or the reason for refusing). Each request is then resolved one of two ways:

Every resolution, a full refusal included, then goes to the secretariat, who registers it in Esfera (the Departament d’Educació’s own system, outside EMS) and closes the request. Only a completed request reaches the student’s grades.

Not yet legally signed: the resolution PDF carries no qualified electronic signature. Signing it with the Director’s certificate is future work, tracked in issue #530.

Only studies whose level has allows_convalidation set can receive requests. data/cat/ems.level.csv sets it for CFGM and CFGS, the cycles the centre’s secretariat publishes convalidation forms for.

Module files: models/grades/convalidation.py, models/grades/convalidation_info_wizard.py, models/grades/convalidation_return_wizard.py, reports/grades/report_convalidation_resolution.xml, models/curriculum/level.py (allows_convalidation), models/curriculum/study.py (_ems_convalidable_subjects), models/grades/grade_subject_line.py, models/grades/grade_session.py, models/grades/year_record.py, models/grades/grade_review_wizard.py, models/grades/em_grading_wizard.py, models/contacts/contact.py (stat button), models/settings/company.py (request period), models/settings/settings.py, views/settings/form.xml, models/contacts/portal.py (_ems_portal_can_act_for), controllers/portal_convalidation.py, views/academic_management/convalidations/{views,menu}.xml, views/portal/portal_convalidations.xml, mails/grades/convalidation_resolved.xml, mails/grades/convalidation_info_request.xml, data/main/mail.activity.type.csv, static/src/js/backend/grade_matrix_field.js, static/src/js/backend/grade_tutor_matrix.js, tests/test_convalidation.py, tests/test_convalidation_period.py, tests/test_portal_convalidation.py, tests/test_convalidation_tour.py, static/tests/tours/convalidation_tour.js

See also: grade_session.md, year_record.md, em_grading_wizard.md.

Hierarchy and relations

erDiagram
    RES_PARTNER ||--o{ EMS_CONVALIDATION : "student_id (restrict)"
    RES_PARTNER |o--o{ EMS_CONVALIDATION : "requester_id (set null)"
    EMS_COURSE ||--o{ EMS_CONVALIDATION : "course_id (restrict)"
    EMS_STUDY ||--o{ EMS_CONVALIDATION : "study_id (restrict)"
    EMS_LEVEL ||--o{ EMS_STUDY : "allows_convalidation"
    EMS_CONVALIDATION ||--|{ EMS_CONVALIDATION_LINE : "line_ids (cascade)"
    EMS_SUBJECT ||--o{ EMS_CONVALIDATION_LINE : "subject_id (restrict)"
    EMS_CONVALIDATION }o--o{ IR_ATTACHMENT : "attachment_ids"
    EMS_CONVALIDATION ||--o{ EMS_CONVALIDATION_INFO_WIZARD : "convalidation_id (cascade)"
    EMS_CONVALIDATION ||--o{ EMS_CONVALIDATION_RETURN_WIZARD : "convalidation_id (cascade)"
    EMS_CONVALIDATION |o--o| IR_ATTACHMENT : "resolution_pdf_id (set null)"
    EMS_CONVALIDATION_LINE ..> EMS_GRADE_SUBJECT_LINE : "is_convalidated + grade (sync)"
    EMS_CONVALIDATION_LINE ..> EMS_STUDENT_YEAR_RECORD_SUBJECT : "is_convalidated + grade (sync)"

ems.convalidation (request)

Field Type Notes
name Char Registration number, CONV-<start>-<end, two digits>-<4-digit counter> (e.g. CONV-2026-27-0001), assigned on creation by _ems_next_registration_number(course). The counter starts again every course: one ir.sequence per course (code ems.convalidation.course.<id>, the prefix baked in), created the first time that course gets a request, so nothing has to be prepared before a year opens. Leads display_name.
student_id M2o res.partner Required. Student or applicant (an applicant enrolling into a cycle is the typical requester).
requester_id M2o res.partner The portal user who submitted it: the student or a family contact.
course_id M2o ems.course Required. Defaults to the enrollment course (is_enrollment_default), else the current one: requests are made while enrolling.
study_id M2o ems.study Required. Its level must allow convalidations (_check_study_allows_convalidation).
basis Selection prior_studies, certificate, other.
student_notes, resolution_notes Text Applicant’s comments, and comments sent to the student with the resolution.
attachment_ids M2m ir.attachment Supporting documents, optional. Linked to the request (res_model/res_id) on create/write, so they follow its access rights. The portal’s own answers add to this same field.
(what was filed)   student_id, course_id, study_id, basis and student_notes (FILED_FIELDS) are set on creation — from the portal, or by the secretariat registering a paper request — and cannot be written afterwards, except through sudo; the form shows them read-only once saved.
line_ids O2m At least one (_check_has_lines, also triggered by study_id since a request created without lines carries no line_ids in vals).
state Selection, stored pending, ministry (In process at the Ministry), direction (Pending the Director), in_progress (Pending the secretariat), completed, rejected, cancelled. Written by the actions only (with sudo; any other write is refused in write()), never computed: the circuit is driven by people, not by the lines’ own states.
resolved_by_ministry, ministry_date Boolean, Date Set by action_send_to_ministry.
ministry_resolution (+ _filename) Binary (attachment) The Ministry’s own resolution, optional; editable only while ministry.
info_request, info_request_date Text, Date The last request for information (ems.convalidation.info_wizard), shown on the portal above the answer form while the request is pending or ministry, and on its own tab in the form.
return_reason Text The Director’s reason for sending the last proposal back; shown on the form while pending, cleared by the next proposal.
validation_date, validated_by_id Date, M2o Proposal date / Proposed by: stamped by action_propose and action_ministry_resolved.
signature_date, signed_by_id Date, M2o Resolution date / Resolved by: stamped by action_resolve (whoever pressed it).
resolution_pdf_id M2o ir.attachment The official resolution the student gets: the centre’s PDF (action_resolve), or a copy of ministry_resolution under its own file name (action_ministry_resolved).
resolution_pdf_link Html compute The file name as a link to /web/content/<id> opening in a new tab, which the form shows instead of the many2one (that one would open the attachment’s own form).
resolution_date, resolved_by_id Date, M2o Registration date / Registered by: stamped by the secretariat’s action_complete.
granted_count, pending_count Integer compute List columns. pending_count is what a proposal requires to be zero.
has_centre_title Boolean compute True when the student’s academic history holds a title_obtained record: a hint that their previous grades can be looked up here. Its absence proves nothing (only recent years are in EMS), so nothing is shown in that case.

ems.convalidation.line (subject)

Field Type Notes
convalidation_id M2o Cascade.
student_id, course_id related, stored Used by the grades sync and searches.
request_state related The request’s state, so the embedded list can gate its own cells and buttons.
subject_id M2o ems.subject Must be one of study_id._ems_convalidable_subjects() (the study’s subjects minus the tutorship). Unique per request.
state Selection pending, granted, rejected.
grade Integer The grade a granted subject is recorded with. Defaults to CONVALIDATED_GRADE (5) and is constrained to 5..10: a convalidated subject is passed by definition.
rejection_reason Text Why the subject is refused. Required on every refused line before a proposal or a Ministry resolution (_ems_check_decided); printed on the resolution.
resolution_notes Char Optional remarks shown to the student (hidden column in the form).

Workflow

stateDiagram-v2
    [*] --> pending: portal / secretariat
    pending --> direction: action_propose (Head of Studies)
    direction --> pending: action_return (Director, with reason)
    direction --> in_progress: action_resolve (Director, PDF)
    pending --> ministry: action_send_to_ministry (Head of Studies)
    ministry --> in_progress: action_ministry_resolved (Head of Studies)
    in_progress --> completed: action_complete (secretariat, something granted)
    in_progress --> rejected: action_complete (secretariat, nothing granted)
    pending --> cancelled: action_cancel (applicant)
    cancelled --> pending: action_reopen
    completed --> [*]
    rejected --> [*]

There is no whole-request “reject” action: a refusal is a resolution like any other, decided line by line, issued by the Director (or the Ministry) and registered by the secretariat.

Who may do what. _ems_is_head_of_studies() / _ems_is_director() / _ems_is_secretary() (with group_academic_admin counting as all three) gate the request’s actions. On the lines, _ems_check_can_decide() guards state, subject_id, grade and rejection_reason: the Head of Studies, only while the request is in REVIEW_STATES. Once proposed, nobody changes the decision — the Director resolves or returns it, and the secretariat only registers it. sudo bypasses the check: the request’s own actions write their lines that way, after checking who is acting on the request as a whole.

Tasks. Each step puts the request in the to-do list of whoever owns the next one: ems.mail_activity_convalidation_review for the Deputy Head of Studies (holder of ems.role_dhos), on creation, on reopening and when the Director returns a proposal; ems.mail_activity_convalidation_resolution for the Director (holder of ems.role_director) on a proposal; and ems.mail_activity_convalidation_registration for every member of the secretariat (ems.group_secretary minus ems.group_academic_admin) once resolved. _ems_task_recipients() resolves them from the organisation (_EMS_TASK_ROLES), so there is nothing to configure: the three types carry ems_task_assignment = False and stay out of Academic Management → Configuration → Task Assignment on purpose, the same choice task_assignment.md makes for attendance corrections, whose recipient also comes from the org chart. The administrator is subtracted because it implies every group — exactly why that screen stopped deriving recipients from groups. Each step closes the previous task (_ems_close_tasks, via _ems_move_on), assignees are unsubscribed from the thread so the task is their only notice, and when nobody holds the position a warning is logged and no task is created.

Resolution notice. action_complete calls _ems_send_resolution(), which queues ems.email_template_convalidation_resolved (force_send=False) with resolution_pdf_id attached to student_id._ems_convalidation_recipients() filtered by email — the student always, plus the family while the student is a minor or when an adult authorized sharing (auth_share) — and logs the recipients, or the lack of any, as a chatter note. The intermediate steps send no email: the student sees the new state on the portal.

Communications page. The portal’s Communications page (controllers/portal_comms.py) lists the comments posted on the student’s requests, never their internal notes. _ems_post_communication() posts one comment each time the request is created, proposed, sent to the Ministry, resolved (by the Director or the Ministry), cancelled, reopened, answered from the portal, or registered. Requests are created with mail_create_nosubscribe, and every post (_ems_poster(): comments and internal notes alike) carries it too — message_post() otherwise subscribes whoever posts a comment, which made the staff who acted on a request followers, emailed every later message. So a request has no followers, these comments email nobody, and the only emails are the resolution and the request for information, through the mail queue.

Resolution document

ems.report_convalidation_resolution (reports/grades/report_convalidation_resolution.xml, not bound to the Print menu) is rendered by _ems_generate_resolution_pdf() when the Director resolves, always in Catalan (_ems_resolution_lang()), and kept as resolution_pdf_id (named Resolució <number>.pdf). It replaces any earlier one. Contents:

The configurable texts live on res.company (see Request period for the settings block): convalidation_legal_prior_studies, convalidation_legal_certificate, convalidation_legal_other, convalidation_appeal_text (Text, empty = standard text, written in Catalan) and convalidation_sign_by_delegation (Boolean).

Withdrawal from the subject

Completing a request means the student no longer takes the subjects it convalidated. _ems_withdraw_convalidated_subjects() deletes their ems.enrollment for each granted subject, with ems_bypass_grade_guard — grades already written included, the convalidation replaces them. That runs the enrollment’s own cascade: the student leaves the subject’s attendance schedules and loses their lines in its open grade sessions. Rounds already at the board or finalised keep their line, which the sync below turns into the convalidation’s grade.

Grades integration

ems.grade_subject_line and ems.student.year_record.subject carry is_convalidated + convalidation_grade as mirrors kept in sync by ems.convalidation.line._ems_sync_grades(). The source of truth is _ems_convalidation_line(student, subject): a granted line of a completed request (_ems_convalidation_grade returns its grade, or None). The history subject also stores the request’s registration number as convalidation_number — frozen text, like the rest of the history, since most of its readers (teachers) cannot open a request.

flowchart LR
    L["convalidation line<br/>create / write state, grade or subject / unlink"] --> S["_ems_sync_grades()"]
    C["request write state<br/>(validate, complete, reject, cancel)"] --> S
    S --> G["every ems.grade_subject_line<br/>of (student, subject)"]
    S --> Y["ems.student.year_record.subject<br/>of (student, request course, subject)"]
    N["grade_session._ems_add_student_lines()"] -- "initial value" --> G

Portal

controllers/portal_convalidation.py (/my/convalidaciones) always acts on get_portal_student(): the student, or the child a family has selected. It uses sudo() because portal users have no ACL on these models.

Who files requests has its own rule, res.partner._ems_convalidation_can_request(student) (models/contacts/portal.py), independent from the rest of the portal’s _ems_portal_can_act_for():

Student Who files and follows up (answers, cancels) Who only reads
Adult, no auth_share The student -
Adult, auth_share The student and the family -
Minor with a family contact The family The student
Minor without a family contact (a GEDAC applicant included) Nobody: the page tells the student to fill in the family’s contact details from the profile page The student

A student with no birth date counts as a minor. _ems_convalidation_portal_visible() decides whether the page (and its home card and header entry, which live outside the view-only block) is shown: to whoever can file, and to the student himself; anyone else is sent back to /my/home. The Communications page shows the convalidation threads to a view-only account (the family of an adult who shares) when it can file them.

Route Behaviour
GET /my/convalidaciones Requests of the student, plus the new-request form when the viewer can file, _ems_portal_study() finds a study and the request period is open. The form is a Bootstrap collapse, folded by default; it opens with ?new=1 or when the page comes back with a validation ?error=. It says when the period closes. While closed, a notice with the next opening replaces the form. Otherwise, a notice explains why the viewer cannot file.
POST /my/convalidaciones/submit Only whoever can file. Refused with ?error=closed outside the request period. Then checks that at least one subject in _ems_portal_requestable_subjects() and a valid basis are sent, and creates the request and its attachments. Documents are optional: the form says per case which ones are needed, and the Head of Studies can ask for more.
POST /my/convalidaciones/reply/<id> The applicant’s answer: files and/or text, while the request is pending or ministry, whatever the date. The files join attachment_ids and the text is posted as a comment (_ems_portal_add_documents).
POST /my/convalidaciones/cancel/<id> Only while pending, whatever the date.
GET /my/convalidaciones/resolution/<id> Downloads resolution_pdf_id of a completed/rejected request, for whoever sees the page.

Request period

A yearly window, with no year, stored on res.company and edited in Settings → EMS Management → Convalidations Settings (the settings form, so only base.group_system):

Field Default Notes
convalidation_start_day, convalidation_start_month, convalidation_start_time 1, October, 08:00 Opening. month is a Selection '1'..'12'; time a float hour (float_time).
convalidation_end_day, convalidation_end_month, convalidation_end_time 31, March, 23:59 Closing. The closing minute is still inside the period.

Access control

Role Request Lines Review / propose / Ministry Resolve / return Register (complete)
Academic admin CRUD CRUD Yes Yes Yes
Director CRU CRUD Yes (implies Head of Studies) Yes No
Head of Studies / Deputy CRU CRUD Yes No No
Secretary CRU CRUD (no decision, no grade) No No Yes
Teacher / tutor none none No No No
Portal through the controller only, per the table in Portal; new requests only during the request period through the controller only No No No
Settings administrator Configures the request period and the resolution texts - - - -