diff --git a/.github/workflows/changelog-clean.yml b/.github/workflows/changelog-clean.yml index 455ad414..7a060d08 100644 --- a/.github/workflows/changelog-clean.yml +++ b/.github/workflows/changelog-clean.yml @@ -49,7 +49,7 @@ jobs: steps: - name: Check Integrators team membership id: check - uses: actions/github-script@v7 + uses: actions/github-script@v9 with: github-token: ${{ secrets.TEAM_READ_TOKEN }} script: | @@ -79,7 +79,7 @@ jobs: steps: - name: Resolve PR head branch id: pr - uses: actions/github-script@v7 + uses: actions/github-script@v9 with: script: | const pr = await github.rest.pulls.get({ @@ -128,7 +128,7 @@ jobs: - name: Report result as a commit status if: always() - uses: actions/github-script@v7 + uses: actions/github-script@v9 with: script: | const originalSha = '${{ steps.pr.outputs.sha }}'; diff --git a/.github/workflows/ci-unit-testing.yml b/.github/workflows/ci-unit-testing.yml index e1aef666..9f54386d 100644 --- a/.github/workflows/ci-unit-testing.yml +++ b/.github/workflows/ci-unit-testing.yml @@ -17,9 +17,20 @@ jobs: matrix: ${{ steps.shards.outputs.matrix }} changelog_only: ${{ steps.diff.outputs.changelog_only }} steps: + # ref: pinned to the PR branch's own head commit - NOT the default for a pull_request + # event, which is refs/pull//merge (a synthetic 2-parent commit GitHub regenerates + # on every push, merging the PR branch into main). The "Detect changelog-only push" step + # below relies on HEAD~1 being the previous commit *on this branch*; against the merge + # ref, HEAD~1 is main's own tip instead, so the diff silently covers the whole PR against + # main rather than just the latest push - confirmed 2026-09-05 (issue #401's own + # /changelog-clean commit), where this caused the full test matrix to run anyway despite + # the commit only touching changelog/. Empty-string-safe for workflow_dispatch (no + # github.event.pull_request context there): actions/checkout treats an empty `ref` as + # "use the default", same as omitting it. - name: Checkout EMS uses: actions/checkout@v7 with: + ref: ${{ github.event.pull_request.head.sha }} fetch-depth: 2 # This required check must always run and record a real result (never be skipped by diff --git a/.github/workflows/deploy-check.yml b/.github/workflows/deploy-check.yml index 0c5e368c..66f1dd72 100644 --- a/.github/workflows/deploy-check.yml +++ b/.github/workflows/deploy-check.yml @@ -28,7 +28,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Post pending status with instructions - uses: actions/github-script@v7 + uses: actions/github-script@v9 with: script: | await github.rest.repos.createCommitStatus({ @@ -46,7 +46,7 @@ jobs: steps: - name: Check Integrators team membership id: check - uses: actions/github-script@v7 + uses: actions/github-script@v9 with: github-token: ${{ secrets.TEAM_READ_TOKEN }} script: | @@ -75,7 +75,7 @@ jobs: steps: - name: Resolve PR head commit id: pr - uses: actions/github-script@v7 + uses: actions/github-script@v9 with: script: | const pr = await github.rest.pulls.get({ @@ -85,7 +85,7 @@ jobs: core.setOutput('sha', pr.data.head.sha); - name: Mark check as pending - uses: actions/github-script@v7 + uses: actions/github-script@v9 with: script: | await github.rest.repos.createCommitStatus({ @@ -165,7 +165,7 @@ jobs: - name: Report result on the commit if: always() - uses: actions/github-script@v7 + uses: actions/github-script@v9 with: script: | const state = '${{ steps.dryrun.outcome }}' === 'success' ? 'success' : 'failure'; diff --git a/.github/workflows/require-changelog-clean.yml b/.github/workflows/require-changelog-clean.yml index e9f2e19f..4859f1ff 100644 --- a/.github/workflows/require-changelog-clean.yml +++ b/.github/workflows/require-changelog-clean.yml @@ -42,7 +42,7 @@ jobs: uses: actions/checkout@v7 - name: Mark status as pending - uses: actions/github-script@v7 + uses: actions/github-script@v9 with: script: | await github.rest.repos.createCommitStatus({ @@ -63,7 +63,7 @@ jobs: - name: Report result on the commit if: always() - uses: actions/github-script@v7 + uses: actions/github-script@v9 with: script: | const clean = '${{ steps.verify.outputs.clean }}' === 'true'; diff --git a/CLAUDE.md b/CLAUDE.md index 1f3ded82..195d24fb 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -225,6 +225,35 @@ echo "$(date +%H:%M:%S) EMS: waiting on you — .md` working files themselves - those are fine hard-wrapped at a fixed +column width, since that's simply how the working file happens to be formatted while it's being +built up over the branch's lifetime. + **Deliver as a file, not pasted into chat — this developer's client renders no download affordance either way** (`SendUserFile` produces no visible card in this VSCode-extension/Claude Code environment, confirmed 2026-08-12 - only pasting is the actual regression to avoid). Write diff --git a/__init__.py b/__init__.py index 88b49834..c714a70d 100755 --- a/__init__.py +++ b/__init__.py @@ -1,5 +1,7 @@ # -*- coding: utf-8 -*- +from psycopg2.extras import Json + from . import controllers from . import models @@ -29,6 +31,7 @@ def post_init_hook(env): env['ems.course']._ems_seed_enrollment_default() _backfill_missing_teacher_calendars(env) _default_strike_family_notification_kicked_out(env) + _seed_notice_email_signature_default(env) def _backfill_default_schedule_framework(env): @@ -73,6 +76,37 @@ def _default_strike_family_notification_kicked_out(env): env['res.company'].search([]).write({'strike_family_notification_mode': 'kicked_out'}) +def _seed_notice_email_signature_default(env): + """res.company.notice_email_signature (Html, translate=True) replaces what used to be a + hardcoded 'Kind regards,
{company name}' baked into ems.mail_notice's own body_html - + seed every company still missing it with the exact same text, in all 3 shipped languages, + so a fresh install's notices keep looking the same as before this became editable (see + migrations//post-migrate.py for the upgrade-path counterpart).""" + for company in env['res.company'].search([('notice_email_signature', '=', False)]): + # Two ORM approaches were tried and rejected before this one - both real gotchas, not + # style choices: (1) a plain sequence of with_context(lang=X).write(...) calls + # auto-cascades a new value to every OTHER language that still looks "not manually + # customized," so writing en_US then ca_ES then es_ES in a loop ends up clobbering + # earlier languages with later ones (confirmed empirically - en_US ended up with the + # Catalan text); (2) record.update_field_translations(field, {lang: value}) - the + # ORM's own multi-lang API - silently returns False and writes nothing here, because + # fields.Html sets `translate` to the html_translate *function*, not the literal + # `True`, so the ORM takes the term-by-term "translate existing content" code path + # (expects {lang: {old_term: new_term}} and requires a pre-existing value to diff + # against) instead of the "set the whole value" path - neither applies when seeding a + # brand-new, still-empty field. A direct SQL write of the full jsonb value sidesteps + # both: correct and simple for a one-time initial seed (not an ongoing translation + # workflow, which is what those ORM APIs are actually built for). + env.cr.execute( + "UPDATE res_company SET notice_email_signature = %s WHERE id = %s", + (Json({ + 'en_US': f"Kind regards,
{company.name}", + 'ca_ES': f"Salutacions cordials,
{company.name}", + 'es_ES': f"Saludos cordiales,
{company.name}", + }), company.id), + ) + + def _enable_unaccent_extension(env): """Once the PostgreSQL 'unaccent' extension is present, Odoo core automatically wraps every ilike/like search domain (list/kanban search bars, name_search, filters...) with diff --git a/__manifest__.py b/__manifest__.py index 68e8be51..5d0f6527 100755 --- a/__manifest__.py +++ b/__manifest__.py @@ -18,7 +18,7 @@ # Check https://github.com/odoo/odoo/blob/16.0/odoo/addons/base/data/ir_module_category_data.xml # for the full list 'category': 'Educational', - 'version': '18.0.0.23.2', #18.0 means the Odoo version; x.y.z means 'breaking.feature.fix'. The '0.y.z' is for alpha/beta pre-release. + 'version': '18.0.0.23.3', #18.0 means the Odoo version; x.y.z means 'breaking.feature.fix'. The '0.y.z' is for alpha/beta pre-release. # any module necessary for this one to work correctly # only 'base_setup', 'hr', 'auth_oauth' are needed. The rest are installed sometimes (and sometimes nor) and I don't know why, so I decided to install all manyally in order to avoid errors. @@ -193,6 +193,7 @@ 'views/communications/menu.xml', 'views/communications/surveys/header/list.xml', + 'views/communications/surveys/header/search.xml', 'views/communications/surveys/header/form.xml', 'views/communications/surveys/header/menu.xml', 'views/communications/surveys/block/form.xml', @@ -233,6 +234,7 @@ 'views/attendance/attendance_correction/menu.xml', 'views/attendance/attendance_correction/list.xml', 'views/attendance/attendance_correction/form.xml', + 'views/attendance/attendance_correction/search.xml', 'views/attendance/attendance_correction/hr_attendance_form.xml', 'views/attendance/guard_duty_board/menu.xml', @@ -250,6 +252,7 @@ 'views/attendance/attendance_reports/wizard.xml', 'views/communications/notice/list.xml', + 'views/communications/notice/search.xml', 'views/communications/notice/form.xml', 'views/coexistence/strike/list.xml', diff --git a/docs/ca/admin/index.md b/docs/ca/admin/index.md index bb869017..5bb96431 100644 --- a/docs/ca/admin/index.md +++ b/docs/ca/admin/index.md @@ -26,6 +26,8 @@ Aquesta secció conté els manuals per a **administradors**. - [L'horari setmanal d'un grup](group-schedule.md) — Consultar l'horari agregat d'un grup (assignatures, docents, aules, patis) i exportar-lo a PDF. - [Preparar el curs següent](course-transition.md) — Tancar el curs: arxivar l'historial acadèmic, graduar i arxivar els exalumnes, col·locar tothom al grup nou i canviar el curs actual. - [Importar les notes des d'Esfera](grade-import.md) — Carregar a l'EMS les notes oficials de cada avaluació i, opcionalment, crear les matrícules que faltin. +- [Comunicats: enviar correus massius a alumnes i famílies](notice.md) — Redactar i enviar un Comunicat, i qui veu quins comunicats. +- [Enquestes: integració amb LimeSurvey](survey.md) — El cicle de vida de l'enquesta (esborrany → destinataris → pujada → oberta → tancada → descàrrega) i qui pot gestionar quines enquestes. --- diff --git a/docs/ca/admin/notice.md b/docs/ca/admin/notice.md new file mode 100644 index 00000000..07a62539 --- /dev/null +++ b/docs/ca/admin/notice.md @@ -0,0 +1,108 @@ +[Català](notice.md) | [Castellano](../../es/admin/notice.md) | [English](../../en/admin/notice.md) + +--- + +# Comunicats: enviar correus massius a alumnes i famílies + +**Rol necessari:** Administrador (o Director, que té la mateixa visibilitat completa — vegeu més avall) + +--- + +## Què és un Comunicat + +Un **Comunicat** és un correu electrònic massiu enviat a un conjunt d'alumnes i/o les seves +famílies — per exemple, un recordatori d'un termini o un avís que afecta un o més grups. Es +troba a **Comunicacions → Comunicats**. + +--- + +## Crear i enviar un comunicat + +1. **Comunicacions → Comunicats → Nou**. +2. Ompliu l'**Assumpte** i el **Missatge** (text enriquit, admet imatges). +3. Reviseu la **Signatura**, just a sota — ve precarregada amb la del vostre centre (vegeu + [Personalitzar la signatura](#personalitzar-la-signatura) més avall), però la podeu editar o + esborrar lliurement només per a aquest comunicat. +4. Trieu **Enviar a**: Alumnes, Famílies, o Tots dos. +5. Si la selecció inclou alumnes, trieu **Correu del destinatari**: **Corporatiu** (l'adreça + institucional de Google Workspace de l'alumne), **Personal** (la seva adreça personal), o + **Ambdós** (per defecte) — si l'alumne té les dues adreces, "Ambdós" envia el comunicat a + cadascuna per separat. Aquesta opció no té cap efecte sobre les famílies, ja que només tenen + una única adreça de correu. +6. Afegiu un o més **Grups** — la llista de destinataris es genera automàticament a partir dels + alumnes de cada grup i, quan se selecciona "Famílies"/"Tots dos", els seus contactes + familiars vinculats (les famílies d'un alumne menor sempre s'inclouen; les d'un alumne major + d'edat només si l'alumne ha autoritzat explícitament compartir-ho). +7. Reviseu la **Llista de destinataris** — també podeu afegir o eliminar files manualment; les + files manuals es conserven encara que canvieu els grups seleccionats després. Si algun + alumne no té cap adreça que coincideixi amb la vostra selecció de **Correu del destinatari** + (p. ex. heu triat "Corporatiu" però encara no té compte institucional creat), apareix un avís + amb els seus noms perquè sapigueu que han quedat exclosos. +8. Feu una de les dues opcions: + - Cliqueu **Enviar** per posar els correus a la cua immediatament, o + - Marqueu **Programar l'enviament** i trieu una data/hora, i cliqueu **Enviar** — el + comunicat passa a **Programat** i els correus surten en aquell moment. +9. L'**Estat** del comunicat segueix el progrés: **Esborrany** → **Programat** → **Enviat** (o + **Fallit** si l'enviament ha fallat per a tots els destinataris). Cada fila de destinatari + mostra el seu propi estat d'enviament, amb el detall de l'error disponible a les files + fallides. + +Un comunicat **programat** (encara no enviat) es pot **cancel·lar**, tornant-lo a Esborrany +perquè el pugueu editar i tornar a enviar. + +Si un destinatari clica **Respondre** al correu que ha rebut, la resposta arriba directament a +qui ha enviat el comunicat — no a una adreça tècnica compartida — així una conversa iniciada +des d'un comunicat arriba a la persona correcta. + +--- + +## Personalitzar la signatura + +Tot correu de comunicat acaba amb una **Signatura** — per defecte, la que estigui configurada +per a tot el centre a **Configuració → EMS Management → Signatura dels correus dels +comunicats**, un camp de text enriquit que podeu escriure com vulgueu (un nom, un càrrec, dades +de contacte — o deixar-lo en blanc per no tenir cap signatura). És traduïble: useu la petita +icona de traducció al costat del camp per escriure una versió diferent per idioma, de manera +que cada destinatari vegi la signatura en el seu propi idioma automàticament. + +Canviar la signatura per defecte del centre només afecta els **comunicats creats a partir +d'ara** — cada comunicat ja existent té la seva pròpia còpia de la signatura (del pas 3 +anterior), que també podeu sobreescriure individualment sense tocar la del centre. + +--- + +## Qui veu quins comunicats + +Tothom amb accés a Comunicats — administradors, Director, Cap d'estudis, Cap d'estudis adjunt +i coordinador de qualitat per igual — veu tots els comunicats de tot el centre, però la llista +sempre s'obre filtrada amb **"Mostra només els meus"** per defecte, de manera que dia a dia +tothom treballa còmodament només amb els seus propis. Si traieu aquest filtre (a la barra de +cerca, a la part superior de la llista) veureu els comunicats de tothom, per quan necessiteu +supervisar. + +- **Els administradors i el Director** poden gestionar completament qualsevol comunicat + independentment del filtre — només afecta què es **mostra** per defecte, no què poden fer. +- El **Cap d'estudis, el Cap d'estudis adjunt** i el **coordinador de qualitat** només poden + editar o eliminar els comunicats que ells mateixos han creat — el comunicat d'una altra + persona s'obre en mode només lectura fins i tot amb el filtre tret. Vegeu el + [manual de Cap d'estudis](../head_of_studies/notice.md) per a la seva perspectiva. + +Si el vostre compte no està vinculat a cap docent (un cas poc habitual — la majoria de comptes +d'Administrador/Director corresponen a un docent real) i preferiu no veure mai marcat "Mostra +només els meus", traieu-lo un cop i utilitzeu **Favorits → Desar cerca actual** a la barra de +cerca, marcant **Filtre per defecte** — l'Odoo ho recordarà des d'aleshores per a aquest +usuari. + +--- + +## Eliminar versus arxivar + +Un comunicat només es pot eliminar de manera permanent mentre estigui en **Esborrany** — un +cop programat, enviat, o fallit, l'EMS bloqueja l'eliminació (té un historial d'enviament real +que val la pena conservar) i us demana que l'**arxiveu** en el seu lloc (menú ⚙ → Arxivar). Els +comunicats arxivats queden ocults de la llista per defecte; utilitzeu **Filtres → Arxivat** per +tornar-los a trobar. + +--- + +[← Tornar als manuals d'Administrador](index.md) diff --git a/docs/ca/admin/survey.md b/docs/ca/admin/survey.md new file mode 100644 index 00000000..577bf0f3 --- /dev/null +++ b/docs/ca/admin/survey.md @@ -0,0 +1,78 @@ +[Català](survey.md) | [Castellano](../../es/admin/survey.md) | [English](../../en/admin/survey.md) + +--- + +# Enquestes: integració amb LimeSurvey + +**Rol necessari:** Administrador o Coordinador de qualitat (vegeu [Visibilitat](#visibilitat-qui-veu-quines-enquestes) més avall per a la diferència entre els dos) + +--- + +## Què és una enquesta + +La funcionalitat d'**Enquestes** de l'EMS (**Comunicacions → Enquestes**) genera i gestiona +qüestionaris de LimeSurvey per a alumnes, docents o personal PAS — enquestes d'avaluació/ +satisfacció enviades i seguides sense sortir de l'EMS. No s'ha de confondre amb l'app nativa +de Surveys d'Odoo, que en aquesta instal·lació està amagada. + +--- + +## El cicle de vida d'una enquesta + +Una enquesta passa per una seqüència fixa d'estats a mesura que hi treballeu: + +1. **Esborrany** — definiu el **Títol**, la **Descripció**, l'**Objectiu** (Alumnes / Docents / + PAS) i els seus **Blocs** de contingut (les preguntes/seccions, com a plantilles separades + per tabuladors). +2. **Calcular destinataris** — l'EMS determina qui ha de rebre l'enquesta (filtrat per Nivell/ + Estudi/Grup, o per regles especials per assignatura/pràctiques en blocs individuals) i + construeix la llista de **Destinataris**, cadascun amb la seva pròpia foto fixa de matrícula. +3. **Pujar** — l'enquesta i els seus destinataris es creen al mateix LimeSurvey mitjançant la + seva API. +4. **Obrir** — l'enquesta queda activa; els destinataris poden respondre. Utilitzeu + **Recordar** per reenviar la invitació a qui encara no hagi respost. +5. **Tancar** — deixa d'acceptar respostes. +6. **Descarregar** — porta les dades de resposta de tornada a l'EMS com a CSV, llestes per a + l'anàlisi (per exemple, a Metabase). + +Podeu tornar una enquesta pujada/calculada a **Esborrany** (recalculant els destinataris des de +zero) en qualsevol moment abans de tancar-la. + +--- + +## Visibilitat: qui veu quines enquestes + +Tothom amb accés a Enquestes — administradors i coordinador de qualitat per igual — veu totes +les enquestes de tot el centre, però la llista sempre s'obre filtrada amb **"Mostra només les +meves"** per defecte (una etiqueta a la barra de cerca), de manera que dia a dia tothom treballa +còmodament només amb les seves. Si traieu aquest filtre veureu totes les enquestes de tot el +centre, per quan necessiteu revisar la feina d'algú altre. + +- Els **Administradors** poden gestionar completament qualsevol enquesta independentment del + filtre — només afecta què es **mostra** per defecte, no què poden fer. +- El **Coordinador de qualitat** només pot **crear, editar o eliminar les enquestes que ell + mateix hagi creat** — l'enquesta d'una altra persona s'obre en mode només lectura fins i tot + amb el filtre tret. +- Un membre normal de l'**equip de qualitat** (que no sigui el coordinador) conserva l'accés + sense restriccions de crear/editar totes les enquestes, igual que abans — aquesta distinció + només s'aplica al rol de coordinador. + +Si el vostre compte no està vinculat a cap docent i preferiu no veure mai marcat "Mostra només +les meves", traieu-lo un cop i utilitzeu **Favorits → Desar cerca actual** a la barra de cerca, +marcant **Filtre per defecte** — l'Odoo ho recordarà des d'aleshores per a aquest usuari. + +--- + +## Eliminar una enquesta + +- Una enquesta es pot eliminar mentre estigui en estat **Esborrany**, **Destinataris + calculats**, o **Tancada**. +- Eliminar una enquesta **Tancada** també l'elimina de manera permanent de LimeSurvey — si les + dades de resposta encara no s'han descarregat, es perden per sempre. L'EMS demana + confirmació abans de fer-ho. +- Una enquesta que estigui **Pujada**, **Oberta**, o en un altre estat intermedi no es pot + eliminar directament — cal tancar-la primer. + +--- + +[← Tornar als manuals d'Administrador](index.md) diff --git a/docs/ca/head_of_studies/attendance-corrections.md b/docs/ca/head_of_studies/attendance-corrections.md index b5eca872..0f4eb360 100644 --- a/docs/ca/head_of_studies/attendance-corrections.md +++ b/docs/ca/head_of_studies/attendance-corrections.md @@ -15,6 +15,8 @@ Els professors poden sol·licitar una correcció d'una hora d'entrada/sortida de Si t'han enviat una sol·licitud (la veuràs com a activitat pendent, i també apareixerà a **Fitxatges dels empleats → Sol·licituds de correcció**): 1. Obre la sol·licitud — des de l'activitat, des de **Fitxatges dels empleats → Sol·licituds de correcció**, o des del botó **Correccions** del propi fitxatge. + + > La llista mostra només les sol·licituds **Pendents** per defecte, perquè no calgui repassar les que ja tenen una decisió. Treu el filtre **Pendent** (o canvia al filtre **Acceptada**/**Rebutjada**) per veure la resta. 2. Revisa l'hora original davant de la sol·licitada, i el motiu indicat. 3. Fes clic a **Acceptar** per aplicar la correcció al fitxatge, o a **Rebutjar** per deixar-lo sense canvis (o restaurar-lo, si estàs desfent una acceptació anterior). Pots deixar una nota opcional per al professor/a. 4. El professor o professora que va fer la sol·licitud rep una notificació automàtica amb la teva decisió. diff --git a/docs/ca/head_of_studies/index.md b/docs/ca/head_of_studies/index.md index 8501cd33..681b4953 100644 --- a/docs/ca/head_of_studies/index.md +++ b/docs/ca/head_of_studies/index.md @@ -16,6 +16,7 @@ Aquesta secció conté els manuals per a **Cap d'Estudis, Cap d'Estudis Adjunt/a - [L'horari setmanal d'un grup](../admin/group-schedule.md) - [Informes d'assistència](attendance-reports.md) - [Crear i editar professorat](staff-management.md) +- [Comunicats: enviar els vostres propis correus massius](notice.md) --- diff --git a/docs/ca/head_of_studies/notice.md b/docs/ca/head_of_studies/notice.md new file mode 100644 index 00000000..aefc8f67 --- /dev/null +++ b/docs/ca/head_of_studies/notice.md @@ -0,0 +1,53 @@ +[Català](notice.md) | [Castellano](../../es/head_of_studies/notice.md) | [English](../../en/head_of_studies/notice.md) + +--- + +# Comunicats: enviar els vostres propis correus massius + +Aquesta pàgina cobreix **Comunicacions → Comunicats** per al Cap d'estudis, el Cap d'estudis +adjunt i el coordinador de qualitat. La pantalla i el flux de crear/enviar és exactament el +mateix que es descriu al [manual d'Administrador](../admin/notice.md) — aquesta pàgina només +cobreix què és diferent sobre qui veu què. + +**Rol necessari:** Cap d'estudis / Cap d'estudis adjunt / Director / Coordinador de qualitat + +--- + +## Visibilitat: la vostra llista comença filtrada als vostres comunicats + +Si teniu el rol de **Cap d'estudis**, **Cap d'estudis adjunt** o **Coordinador de qualitat**, +**Comunicacions → Comunicats** s'obre amb el filtre **"Mostra només els meus"** ja aplicat +(visible com una etiqueta a la barra de cerca), de manera que dia a dia treballeu còmodament +només amb els comunicats que **vosaltres mateixos heu creat** — la mateixa experiència que la +resta. + +Si mai necessiteu comprovar què ha enviat un company amb el mateix rol — per supervisar — +cliqueu la **✕** de l'etiqueta "Mostra només els meus" a la barra de cerca (o obriu el panell +de cerca i desmarqueu-la) per veure tots els comunicats de tot el centre. Només podreu **editar +o eliminar els vostres propis**; el comunicat d'una altra persona s'obre en mode només lectura. + +El **Director** té exactament el mateix filtre per defecte (igual que un Administrador) — la +diferència és només en què li permet fer treure'l, no en qui el veu: un Director pot editar +qualsevol comunicat un cop tret el filtre, mentre que vosaltres només podeu editar o eliminar +els vostres propis independentment del filtre. Vegeu el +[manual d'Administrador](../admin/notice.md#qui-veu-quins-comunicats). + +--- + +## Crear, enviar i eliminar + +Seguiu els mateixos passos del +[manual d'Administrador](../admin/notice.md#crear-i-enviar-un-comunicat): redacteu l'assumpte +i el missatge, reviseu o editeu la **Signatura** precarregada, trieu els grups destinataris, i +envieu-lo immediatament o programeu-lo. Un comunicat que hàgiu creat només es pot eliminar de +manera permanent mentre estigui en **Esborrany** — un cop programat o enviat, arxiveu-lo en el +seu lloc (vegeu [Eliminar versus arxivar](../admin/notice.md#eliminar-versus-arxivar)). + +La Signatura comença precarregada amb la del centre, que només un Administrador pot canviar +(**Configuració → EMS Management**) — però la podeu editar o esborrar lliurement en qualsevol +comunicat que creeu, sense necessitar aquest permís. Qui respongui al vostre comunicat us +arriba directament a vosaltres, no a una adreça tècnica compartida. + +--- + +[← Tornar als manuals de Cap d'estudis](index.md) diff --git a/docs/ca/teachers/attendance-corrections.md b/docs/ca/teachers/attendance-corrections.md index 83574de8..bb5c8553 100644 --- a/docs/ca/teachers/attendance-corrections.md +++ b/docs/ca/teachers/attendance-corrections.md @@ -51,6 +51,8 @@ La teva sol·licitud s'envia automàticament a qui la pot validar — normalment - **Fitxatges dels empleats → Sol·licituds de correcció** mostra totes les sol·licituds que has fet i el seu estat actual (Pendent / Acceptada / Rebutjada). - Des del mateix fitxatge, el botó **Correccions** de la capçalera (només visible si hi ha alguna sol·licitud per aquell registre) t'hi porta directament. +> Per defecte, la llista només mostra les sol·licituds **Pendents**. Treu el filtre **Pendent** de la barra de cerca (o canvia al filtre **Acceptada**/**Rebutjada**) per veure les sol·licituds que ja tenen una decisió. + > Si també ets Cap d'Estudis, Cap d'Estudis Adjunt/a o Direcció, consulta el [manual de Cap d'Estudis](../head_of_studies/attendance-corrections.md) per saber com decidir sobre les sol·licituds que t'arriben. --- diff --git a/docs/en/admin/index.md b/docs/en/admin/index.md index 705b820c..0a42663e 100644 --- a/docs/en/admin/index.md +++ b/docs/en/admin/index.md @@ -26,6 +26,8 @@ This section contains the manuals for **administrators**. - [A Group's Weekly Schedule](group-schedule.md) — Viewing a group's aggregated timetable (subjects, teachers, classrooms, breaks) and exporting it to PDF. - [Setting Up the Next Course](course-transition.md) — Closing the year: archiving the academic history, graduating and archiving former students, placing everyone in their new group and switching the current course. - [Importing Grades from Esfera](grade-import.md) — Loading each evaluation's official grades into EMS, and optionally creating the enrollments that are missing. +- [Notices: Sending Bulk Emails to Students and Families](notice.md) — Composing and sending a Notice, and who sees which notices. +- [Surveys: LimeSurvey Integration](survey.md) — The survey lifecycle (draft → recipients → upload → open → close → download) and who can manage which surveys. --- diff --git a/docs/en/admin/notice.md b/docs/en/admin/notice.md new file mode 100644 index 00000000..5ae55eec --- /dev/null +++ b/docs/en/admin/notice.md @@ -0,0 +1,102 @@ +[Català](../../ca/admin/notice.md) | [Castellano](../../es/admin/notice.md) | [English](notice.md) + +--- + +# Notices: Sending Bulk Emails to Students and Families + +**Required role:** Administrator (or Director, who has the same full visibility — see below) + +--- + +## What a Notice Is + +A **Notice** is a bulk email sent to a set of students and/or their families — for example, a +reminder about an upcoming deadline or an announcement affecting one or more groups. Found +under **Communications → Notices**. + +--- + +## Creating and Sending a Notice + +1. **Communications → Notices → New**. +2. Fill in the **Subject** and the **Message** (rich text, images supported). +3. Review the **Signature** underneath it — pre-filled from your centre's default (see + [Customizing the Signature](#customizing-the-signature) below), but freely editable or + clearable for this one notice only. +4. Choose **Send to**: Students, Families, or Both. +5. If your selection includes students, choose **Recipient email**: **Corporate** (a student's + institutional Google Workspace address), **Personal** (their personal address), or **Both** + (default) — if a student has both addresses, "Both" sends the notice to each one separately. + This option has no effect on families, since they only ever have one email address. +6. Add one or more **Groups** — the recipient list is built automatically from each group's + students and, when "Families"/"Both" is selected, their linked family contacts (a minor + student's families are always included; an adult student's families only if the student has + explicitly authorized sharing). +7. Review the **Recipient list** — you can also add or remove individual rows by hand; manual + rows are preserved even if you change the selected groups afterwards. If any students have no + address matching your **Recipient email** choice (e.g. "Corporate" was picked but a student's + institutional account hasn't been created yet), a warning names them so you know they were + left out. +8. Either: + - Click **Send** to queue the emails immediately, or + - Tick **Schedule sending** and pick a date/time, then click **Send** — the notice moves to + **Scheduled** and the emails go out at that time. +9. The notice's **State** tracks progress: **Draft** → **Scheduled** → **Sent** (or **Failed** + if every recipient's email failed). Each recipient row shows its own delivery status, with + any error detail available on failed rows. + +A **scheduled** notice (not yet sent) can be **cancelled**, returning it to Draft so you can +edit and resend it. + +If a recipient hits **Reply** on the email they received, it goes straight to whoever actually +sent the notice — not to a shared technical address — so a conversation started from a notice +reaches the right person directly. + +--- + +## Customizing the Signature + +Every notice email ends with a **Signature** — by default, whatever is configured centre-wide +under **Settings → EMS Management → Notice email signature**, a rich-text field you can write +however you like (a name, a role, contact details — or leave it blank for no signature at all). +It's translatable: use the small translation icon next to the field to write a different +version per language, so recipients see the signature in their own language automatically. + +Changing the centre-wide default only affects **notices created afterward** — each existing +notice already has its own copy of the signature (from step 3 above), which you can also +override individually without touching the shared default. + +--- + +## Who Sees Which Notices + +Everyone with access to Notices — Administrators, the Director, Head of Studies, Deputy Head +of Studies and the Quality coordinator alike — sees every notice centre-wide, but the list +always opens filtered to **"Show only mine"** by default, so day to day everyone comfortably +works with just their own. Removing that filter (in the search bar, at the top of the list) +reveals everyone's notices, for whenever you need to supervise. + +- **Administrators and the Director** can fully manage every notice regardless of the filter — + it only affects what's *shown* by default, not what they're allowed to do. +- **Head of Studies, Deputy Head of Studies** and the **Quality coordinator** can only edit or + delete the notices they personally created — someone else's notice opens in read-only mode + even with the filter removed. See the + [Head of Studies manual](../head_of_studies/notice.md) for their perspective. + +If your account isn't linked to a teacher (a rare case — most Administrator/Director logins +are held by an actual teacher) and you'd rather never see "Show only mine" checked, remove it +once and use the search bar's **Favorites → Save current search**, ticking **Default filter** — +Odoo remembers that per login from then on. + +--- + +## Deleting vs. Archiving + +A notice can only be permanently deleted while it is still in **Draft** — once it has been +scheduled, sent, or has failed, EMS blocks deletion (it has real delivery history worth +keeping) and asks you to **Archive** it instead (⚙ menu → Archive). Archived notices are +hidden from the default list; use **Filters → Archived** to find them again. + +--- + +[← Back to Admin manuals](index.md) diff --git a/docs/en/admin/survey.md b/docs/en/admin/survey.md new file mode 100644 index 00000000..57837b51 --- /dev/null +++ b/docs/en/admin/survey.md @@ -0,0 +1,73 @@ +[Català](../../ca/admin/survey.md) | [Castellano](../../es/admin/survey.md) | [English](survey.md) + +--- + +# Surveys: LimeSurvey Integration + +**Required role:** Administrator or Quality coordinator (see [Visibility](#visibility-who-sees-which-surveys) below for the difference between the two) + +--- + +## What a Survey Is + +EMS's **Surveys** feature (**Communications → Surveys**) generates and manages LimeSurvey +questionnaires for students, teachers or ASP staff — evaluation/satisfaction surveys sent out +and tracked without leaving EMS. Not to be confused with Odoo's own native Surveys app, which +is hidden in this installation. + +--- + +## The Survey Lifecycle + +A survey moves through a fixed sequence of states as you work through it: + +1. **Draft** — define the survey's **Title**, **Description**, **Target** (Students / Teachers + / ASP) and its content **Blocks** (the questions/sections, as tab-separated templates). +2. **Compute recipients** — EMS works out who should receive the survey (filtered by Level/ + Study/Group, or by special per-subject/per-internship rules on individual blocks) and builds + the **Recipients** list, each with their own enrollment snapshot. +3. **Upload** — the survey and its recipients are created in LimeSurvey itself via its API. +4. **Open** — the survey is live; recipients can respond. Use **Remind** to resend the + invitation to anyone who hasn't answered yet. +5. **Close** — stops accepting responses. +6. **Download** — pulls the response data back into EMS as a CSV, ready for analysis (e.g. in + Metabase). + +You can return an uploaded/computed survey to **Draft** (recomputing recipients from scratch) +at any point before it's closed. + +--- + +## Visibility: Who Sees Which Surveys + +Everyone with access to Surveys — Administrators and the Quality coordinator alike — sees +every survey centre-wide, but the list always opens filtered to **"Show only mine"** by +default (a tag in the search bar), so day to day everyone comfortably works with just their +own. Removing that filter reveals every survey centre-wide, for whenever you need to check on +someone else's work. + +- **Administrators** can fully manage every survey regardless of the filter — it only affects + what's *shown* by default, not what they're allowed to do. +- The **Quality coordinator** can only **create, edit or delete the surveys they personally + created** — someone else's survey opens in read-only mode even with the filter removed. +- A plain **Quality team member** (not the coordinator) keeps unrestricted create/edit access + to every survey, same as before — this distinction only applies to the coordinator role. + +If your account isn't linked to a teacher and you'd rather never see "Show only mine" checked, +remove it once and use the search bar's **Favorites → Save current search**, ticking +**Default filter** — Odoo remembers that per login from then on. + +--- + +## Deleting a Survey + +- A survey can be deleted while in **Draft**, **Recipients computed**, or **Closed** state. +- Deleting a **Closed** survey also permanently deletes it from LimeSurvey — if the response + data hasn't been downloaded yet, it is lost for good. EMS asks for confirmation before doing + this. +- A survey that is **Uploaded**, **Open**, or otherwise mid-flight cannot be deleted directly — + close it first. + +--- + +[← Back to Admin manuals](index.md) diff --git a/docs/en/developers/attendance/attendance_correction.md b/docs/en/developers/attendance/attendance_correction.md index e4af093f..f7ca78e3 100644 --- a/docs/en/developers/attendance/attendance_correction.md +++ b/docs/en/developers/attendance/attendance_correction.md @@ -76,7 +76,7 @@ A teacher opens one of their own `hr.attendance` records and clicks **Request Co ### Read -Teachers see their own requests (`views/attendance/attendance_correction/list.xml`); Head of Studies/Director/Academic Admin see all pending and historic requests, under the native "Attendances" menu. +Teachers see their own requests (`views/attendance/attendance_correction/list.xml`); Head of Studies/Director/Academic Admin see all pending and historic requests, under the native "Attendances" menu. The list defaults to the **Pending** filter (`action_attendance_correction_tree`'s `context: {'search_default_pending': 1}`), matching the same pattern already used by `ems.student.document` — an approver isn't forced to wade through already-decided requests by default. The search view also exposes standalone **Accepted**/**Rejected** filters, so switching to (or combining) any other state, or removing the default filter to see all requests, is a single click. `action_view_corrections()` (the "Corrections" smart button on `hr.attendance`) explicitly resets the context to `{}` when drilling into one specific attendance's requests, so that view is never filtered. ### Update / Decide @@ -121,6 +121,7 @@ Record rules (`security/rules/attendance.xml`): Admin unrestricted; Head of Stud |------|------|-------| | List | `views/attendance/attendance_correction/list.xml` | Employee, attendance, requested times, state; `create="false"` — records can only be created from the "Request Correction" button, never from this list | | Form | `views/attendance/attendance_correction/form.xml` | Statusbar + Accept/Reject buttons (visible to the resolved approver only) | +| Search | `views/attendance/attendance_correction/search.xml` | Pending/Accepted/Rejected filters + group by status/employee; `action_attendance_correction_tree` defaults to `search_default_pending: 1` | | Menu | `views/attendance/attendance_correction/menu.xml` | Under the native "Attendances" root menu, sibling to Overview/Management | | `hr.attendance` header button + smart button | `views/attendance/attendance_correction/hr_attendance_form.xml` | Inherits `hr_attendance.hr_attendance_view_form`: adds "Request Correction" to `//header`, and a "Corrections" stat button (count) before the first `` that opens the same list/form (domain-filtered to that attendance) via `action_view_corrections()` | @@ -136,7 +137,6 @@ Record rules (`security/rules/attendance.xml`): Admin unrestricted; Head of Stud ## Follow-ups (out of scope for v1) -- Browser tour test (`static/tests/tours/attendance_correction_tour.js`) — deferred. - Per-department scoping of Head of Studies visibility, if the flat/global model becomes a real problem in practice. Note: a dedicated `mail.template` for the decision email was considered and dropped — `message_post(partner_ids=...)` already emails the requester as a mail notification (visible in the chatter added to the form), which covers the "notify by email" requirement without extra plumbing. diff --git a/docs/en/developers/communications/limesurvey.md b/docs/en/developers/communications/limesurvey.md index fd0151b9..17e4018e 100644 --- a/docs/en/developers/communications/limesurvey.md +++ b/docs/en/developers/communications/limesurvey.md @@ -293,6 +293,65 @@ repeated once per non-tutorship subject enrollment on the recipient. their model (`rec` → `recipient`/`header`, `std`/`grp` → `study`/`group` in the two onchange methods) per this rollout's convention. Tabs → spaces throughout. +### Access control (updated 2026-09-05) + +Applies identically to all 4 models in this file (`ems.limesurvey_header`/`.block`/ +`.recipient`/`.enrollment`), enforced by `security/rules/communications.xml` (one admin rule + +two quality-coordinator rules per model, mirroring the coexistence/`ems.strike` idiom in +`security/rules/coexistence.xml`): + +| Group | Sees | Creates/edits/deletes | +|-------|------|------------------------| +| `group_academic_admin` | Every survey/block/recipient/enrollment | Everything | +| `group_quality_admin` (Quality coordinator) | Every survey/block/recipient/enrollment (read-only for others') | Only the ones they created | +| `group_quality` (plain Quality team member) | Everything (unchanged, unrestricted) | Everything except unlink (unchanged — `ir.model.access.csv` only, no per-owner `ir.rule`) | + +**Fixed a pre-existing gap:** `group_academic_admin` previously had **no** +`ir.model.access.csv` row at all for any of these 4 models — despite `menu_limesurvey_headers` +(`views/communications/surveys/header/menu.xml`) already listing `group_academic_admin` as one +of the menu's visible groups. An admin clicking "Surveys" would have hit an `AccessError` +immediately. Added `access_ems_limesurvey_{header,block,recipient,enrollment}_admin` rows plus +matching `rule_limesurvey_*_admin` `ir.rule`s (`domain_force=[(1,'=',1)]`), so admin access now +actually matches what the menu already implied. + +**New restriction for the Quality coordinator specifically** (`group_quality_admin` — +previously had unrestricted full CRUD, same as plain `group_quality`): two rules per model, +one read-only with an open domain (`perm_read=True`, `domain_force=[]`) and one write/create/ +unlink-only scoped to `create_uid = user.id` (`perm_read=False`). Every record across all 4 +models is created directly by whichever coordinator is operating that survey — no `sudo()`, +cron, or `queue_job` path exists in this file that creates or writes these on someone else's +behalf (`action_compute`/`action_upload`/etc. all run synchronously or via +[`ems.multithreading`](../shared/multithreading.md)'s `run_in_thread`, which explicitly +captures and reuses `self.env.uid` from the request that triggered it — see +`run_in_thread`'s own docstring) — so `create_uid` reliably identifies the owning coordinator +everywhere, including `ems.limesurvey_block`/`.recipient`/`.enrollment`, which have no +standalone menu and are only ever reached inline through their parent header's form. +Regression-covered by `TestLimesurveyAccessControl` in `tests/test_limesurvey_header.py`. + +**UX for the read-all rule (added same day):** the `ir.rule` itself already granted the +coordinator centre-wide read access from the start, but `views/communications/surveys/header/ +search.xml`'s "Show only mine" filter (`domain=[('create_uid','=',uid)]`) is defaulted **on** +via `action_limesurvey_header_tree`'s `context: {'search_default_only_mine': 1}` — so their +default list view still feels like "just my surveys" (comfortable, same as everyone else), +with the centre-wide view one filter-removal away for supervision. Same idiom as +`ems.attendance_template`'s `only_mine` filter (`views/attendance/attendance_template/ +search.xml`), except that precedent defaults the filter **off** (its `ir.rule` already hard- +restricts teachers, so the filter there is purely an optional narrowing tool for the +already-unrestricted admin group) — here the default is flipped to **on** since the +underlying rule is the open one. + +**Correction (2026-09-05):** the `search_default_only_mine: 1` context lives on +`action_limesurvey_header_tree` itself — there is only one "Surveys" action, shared by every +group that can open it (`group_academic_admin`, `group_quality`/`group_quality_admin` +implied). `group_academic_admin` is **not** exempt: it opens Surveys with "Show only mine" +checked by default too, same as the Quality coordinator. Intentional (developer feedback +2026-09-05): the filter should default on for any teacher-held role, and admin is normally +held by a real teacher as well. The one edge case - a non-teacher administrative login with no +`hr.employee` behind it - isn't special-cased, since a static XML action `context` has no ORM +access to check that; that account gets the same default and removes it manually (or saves the +removal as their own permanent default via Odoo's native "Save current search" star) - accepted +as sufficient per `docs/en/admin/survey.md`. + ### Testing note: `unlink()` and `action_upload()` etc. can reach the real API directly Unlike `run_action()`'s callers (which go through `run_in_thread`, itself easy to patch), @@ -315,6 +374,9 @@ that exercises this path patches `LimesurveyApi` at the module level prove the wiring works without ever touching the network. - `TestComputeSurveyData` — the regression test for the `teacher_name`/`teachers_names` bug above, plus a smoke test of `only_key=True` mode. +- `TestLimesurveyAccessControl` (added 2026-09-05) — admin's full access across all 4 models + (the pre-existing gap above), and the Quality coordinator's read-all/edit-own split, exercised + on `header`/`block`/`recipient`/`enrollment` alike. --- diff --git a/docs/en/developers/communications/notice.md b/docs/en/developers/communications/notice.md index 13b7c233..71693760 100644 --- a/docs/en/developers/communications/notice.md +++ b/docs/en/developers/communications/notice.md @@ -18,6 +18,8 @@ in this phase, which talks to a real LimeSurvey instance). | `state` | `Selection` (draft/scheduled/sent/failed) | `draft` → `action_send()` → `scheduled` → (async, via `_check_and_finalize`) → `sent`/`failed`. `action_cancel()` returns to `draft`. | | `use_schedule`/`scheduled_date` | `Boolean`/`Datetime` | If set, `action_send()` passes `scheduled_date` as the queue job's `eta` — recipients get the email at that time, not immediately. | | `recipient_type` | `Selection` (students/families/both) | Drives `_build_auto_lines`' filtering. | +| `recipient_email_type` | `Selection` (corporate/personal/both, default `both`) | **Added 2026-09-05.** Which of a student's two email addresses to use - see below. Meaningless for a families-only send (`invisible="recipient_type == 'families'"` in the form), since families only ever have one `email` field, no corporate counterpart. | +| `signature` | `Html` | **Added 2026-09-05.** `default=lambda self: self.env.company.notice_email_signature` - copied from the company's own default at creation time, then freely editable (or clearable) per notice. Rendered verbatim by `ems.mail_notice` (see "Email rendering" below) - replaces what used to be a hardcoded "Kind regards, {company name}" baked into the template itself. | | `notice_line_ids` | `One2many → ems.notice.line` | Mix of auto-populated (from `group_ids`, `source_group_id` set) and manually-added (no `source_group_id`) rows — see below. | | `can_cancel` | computed | `True` only when `state == 'scheduled'` **and** `use_schedule` **and** no line's job has reached `started`/`done`/`failed` yet. An immediate send (`use_schedule=False`) can never be cancelled, even before the queue actually processes it — it's treated as already committed the moment it's sent. | @@ -31,21 +33,69 @@ added recipient), `notification_id` (the `queue.job` tracking this line's send). ```mermaid flowchart TD - A["onchange(group_ids, recipient_type)"] --> B["manual_lines = lines with no source_group_id\n(preserved across re-triggers)"] + A["onchange(group_ids, recipient_type, recipient_email_type)"] --> B["manual_lines = lines with no source_group_id\n(preserved across re-triggers)"] B --> C["seen_emails = manual_lines' emails\n(cross-group dedup starts here)"] C --> D["for each group, each student:"] D --> E{"recipient_type in\n(students, both)?"} - E -- yes --> F["add student line if\nstudent has an email\nand it's not already seen"] + E -- yes --> F["_student_emails(): candidate addresses\nfor recipient_email_type\n(corporate/personal/both)"] + F --> F2{"any candidate\naddress found?"} + F2 -- no --> F3["record student name\nin skipped_student_names"] + F2 -- yes --> F4["add one line per candidate\naddress not already seen\n('both' can add 2 lines\nfor one student)"] D --> G{"recipient_type in\n(families, both)?"} G -- yes --> H{"student is a minor,\nOR an adult who\nauth_share = True?"} H -- no --> Z["skip — an adult who hasn't\nauthorized sharing is never\nemailed via a family line"] H -- yes --> I["add one line per family\nrelation with an email,\nnot already seen"] + F3 --> J{"any students\nskipped?"} + J -- yes --> K["onchange returns\n{'warning': {title, message}}"] ``` Re-triggering the onchange (e.g. adding another group) never duplicates or drops a manually added recipient — only the auto-populated set (`source_group_id` set) gets rebuilt from scratch each time; anything without `source_group_id` is untouched. +### `recipient_email_type`: corporate vs. personal student email (added 2026-09-05) + +A student has two independent, optional email addresses (`models/contacts/contact.py`): +`student_email` (the **corporate**/institutional Google Workspace address, auto-provisioned by +the account-creation job in `models/contacts/google_workspace_integration.py:284-285` - +`self.sudo().student_email = email` - and empty until that job has run) and `email` (the +**personal** address, imported/entered manually - see +[`google_workspace_student.md`](../contacts/google_workspace_student.md) for the full +provisioning flow). Before this change, `_build_auto_lines` used a hardcoded, non-configurable +fallback: `student.student_email or student.email` - always preferred corporate, silently fell +back to personal, and could never use both. + +`recipient_email_type` replaces that fallback with an explicit, user-chosen mode, applied per +student via the new `_student_emails()` helper: + +| `recipient_email_type` | Candidate addresses per student | +|---|---| +| `corporate` | `student_email` only (student skipped if empty) | +| `personal` | `email` only (student skipped if empty) | +| `both` (default) | **Both**, if present - a student with both addresses gets **two** `ems.notice.line` rows (two separate emails sent), not one line with a fallback choice. A student with only one of the two still gets exactly one line for it. | + +A student contributes **zero** lines only when none of their candidate addresses (per the +current mode) are set - that student's name is collected into `skipped_student_names` and +`_onchange_groups` surfaces it via the standard Odoo onchange +`{'warning': {'title': ..., 'message': ...}}` dict, the same idiom already used elsewhere in +this codebase for onchange-time validation (`models/enrollment/enrollment.py:354-372`, +`models/employees/employee.py:404-419`) - not `ems.base.notify()` (bus notifications need a DB +commit to deliver, which an onchange on an unsaved/`new()` record never has) and not the +wizard-style `stats['warnings']`/`warning_html` pattern (`models/contacts/student_import_wizard.py` +etc. - built around an explicit "Run" button's result, not live onchange feedback). + +The family branch is unaffected: families only ever have the single base `email` field, no +corporate counterpart, so `recipient_email_type` has no effect on `recipient_type in +('families', 'both')`'s family-line logic - and the field is hidden in the form entirely when +`recipient_type == 'families'`. + +**Unrelated bug fixed in the same pass:** `recipient_type`'s `Both` option had an empty +`msgstr` in both `i18n/ca_ES.po`/`i18n/es_ES.po` (msgid existed, never actually translated) - +reported by the developer while reviewing this feature. Fixed alongside `recipient_email_type`'s +own new `Both` option, since both share the same msgid text and Odoo folds them into the same +`.po` block (`#:` references to both selections' xmlids on one entry) - see +`TestNotice.test_both_selection_labels_are_translated`. + --- ## Sending: `action_send()` → queued jobs → `_check_and_finalize()` @@ -84,6 +134,67 @@ runs, reading that job's state from a fresh cursor is reliable. --- +## Email rendering: signature and Reply-To (`mails/communications/communication.xml`) + +`ems.mail_notice`'s `body_html` used to hardcode a signature block directly in the template: + +```xml + +

Kind regards,

+``` + +**Changed 2026-09-05** (developer feedback after receiving a test notice with an unexpected, +unremovable "Kind regards, {centre name}"): the `

...

` block is gone, replaced by +`` - the template now renders exactly whatever the +notice's own `signature` field holds (nothing, if cleared). `signature` defaults from +`res.company.notice_email_signature` (**Settings → EMS Management → Notice email +signature**, translatable) at notice-creation time, then stays independently editable per +notice - editing the company default only changes what *future* notices start with. + +**`reply_to`** was previously unset on this template at all, so Odoo fell back to `email_from` +(`ir_mail_server.py`'s `msg['Reply-To'] = reply_to or email_from`) - a hardcoded technical +address (`ems@elpuig.xeill.net`), identical for every sender. Now set to +`{{object.notice_id.sent_by.email or object.notice_id.create_uid.email}}` - whoever actually +sent the notice (`sent_by`, set by `action_send()`) or, if that's not populated yet (e.g. a +`queue_job__no_delay` test rendering the template before `action_send()`'s own `write()` +executes), whoever created it. Same pattern as `mails/coexistence/strike_notification.xml`'s +`{{object.teacher_id.email}}`. + +**`email_from` stays hardcoded and must not change**: both configured outgoing mail servers +(`ir.mail_server`, checked via `psql`) have `from_filter = 'ems@elpuig.xeill.net'` - the only +address they're configured to relay mail *as*. Every `mail.template` in this module hardcodes +the same literal for the same reason (`grep -rn "elpuig.xeill.net" mails/`). This is why the +fix targets `reply_to` specifically rather than making the visible sender per-user. + +### Seeding the initial company signature (`__init__.py` / `migrations/18.0.0.23.3/`) + +`res.company.notice_email_signature` is `Html` with `translate=True`. Seeding its initial +value (preserving the old hardcoded English text, but now in all 3 languages) turned out to +need a specific, non-obvious API - two more idiomatic-looking approaches were tried and +silently produced wrong/no data before landing on this one: + +1. **A loop of `company.with_context(lang=X).write(...)` calls, once per language** - Odoo's + translated-field write auto-cascades a new value onto every *other* language that still + looks "not manually customized," so writing `en_US` then `ca_ES` then `es_ES` in sequence + ends up with `en_US` clobbered by the last write (confirmed empirically: `en_US` ended up + holding the Catalan text). +2. **`record.update_field_translations(field_name, {lang: value})`** - the ORM's own intended + multi-language API. Returns `False` and writes nothing here, because `fields.Html` sets + `field.translate` to the `html_translate` *function*, not the literal `True` - the ORM + takes a completely different code path for callable-`translate` fields (term-by-term + `{lang: {old_term: new_term}}`, diffed against an existing value) instead of "set the whole + value," and that path requires a pre-existing value to do the diff against - it can't seed + a still-empty field at all. + +The working approach: a **direct SQL write of the full jsonb value** +(`UPDATE res_company SET notice_email_signature = %s` with a `psycopg2.extras.Json({...})` +parameter) - correct and simple for a one-time initial seed, bypassing both ORM code paths +above entirely. Same logic duplicated in `__init__.py::_seed_notice_email_signature_default` +(fresh installs, via `post_init_hook`) and `migrations/18.0.0.23.3/post-migrate.py` (existing +installs upgrading in) - see "Migrations" in `CLAUDE.md` for why both paths need it. + +--- + ## `_prepare_body_for_email`: images need public URLs, not editor-internal ones The rich-text `message` field can contain images two ways — pasted as a base64 data URI, or @@ -108,16 +219,76 @@ by content hash on the parent `ems.notice` instead), not a normalization fix. ## Access control -Only `ems.group_academic_admin` has any `ir.model.access.csv` row for `ems.notice`/ -`ems.notice.line` — no other group has *any* access. `security/rules/communications.xml`'s -`rule_notice_own` ("users see own", `domain_force=[('create_uid','=',user.id)]`) has no -`groups` restriction, so it nominally applies to everyone — but since only admins have -model-level access at all, and admins are already covered by the unrestricted -`rule_notice_admin`, `rule_notice_own` is currently **inert** (a non-admin has zero access -regardless of this rule, since model access is the ceiling a record rule can only narrow, -never widen). Same pattern already documented for `ems.enrollment`'s secretary access rule -in `docs/en/developers/contacts/enrollment.md` — not a bug, just a rule with no current -audience. +**Updated 2026-09-05** — Head of Studies/Deputy Head of Studies (`ems.group_head_of_studies`, +the same group covers both roles) and the Quality coordinator (`ems.group_quality_admin`) now +have their own `ir.model.access.csv` rows for `ems.notice`/`ems.notice.line`, alongside +`ems.group_academic_admin`'s pre-existing full access. + +| Group | Sees (read) | Creates/edits/deletes | +|-------|------|------------------------| +| `group_academic_admin`, `group_director` | Every notice | Every notice | +| `group_head_of_studies` (HOS/DHOS) | Every notice (for supervision) | Only notices they created | +| `group_quality_admin` (Quality coordinator) | Every notice (for supervision) | Only notices they created | + +Enforced by `security/rules/communications.xml`: `rule_notice_admin`/`rule_notice_line_admin` +(`domain_force=[(1,'=',1)]`, full CRUD, groups `group_academic_admin` + `group_director`), +`rule_notice_read_all`/`rule_notice_line_read_all` (`domain_force=[]`, `perm_read` only, groups +`group_head_of_studies` + `group_quality_admin`) and `rule_notice_own`/`rule_notice_line_own` +(`domain_force=[('create_uid','=',user.id)]` — the line variant reads through +`notice_id.create_uid` instead, since lines have no menu of their own — `perm_write`/ +`perm_create`/`perm_unlink` only, same two groups). + +**Updated again 2026-09-05 (same day):** the Head of Studies/Quality coordinator read +visibility was widened from "own only" to "every notice, read-only for others'" — mirroring +the `only_mine`-filter idiom already used by `ems.attendance_template`/`.attendance_session`/ +`.attendance_justification` (`views/attendance/*/search.xml`). The difference from that +precedent: those three default the filter **off** (their `ir.rule` already does the hard +per-owner restriction for teachers, so the filter is just an optional narrowing tool, mostly +useful to the already-unrestricted admin group); here the `ir.rule` itself was widened to +open read access, and `views/communications/notice/search.xml`'s "Show only mine" filter +(`domain=[('create_uid','=',uid)]`) is instead defaulted **on** via +`action_communication_list`'s `context: {'search_default_only_mine': 1}` — so a HOS/DHOS or +Quality coordinator still gets the same comfortable "just mine" default view as before, but +can remove the filter to supervise everyone else's notices, rather than having no access to +them at all. Write/create/unlink stay hard-restricted to `create_uid = user.id` either way - +only *read* visibility changed. + +**Correction (still 2026-09-05):** the default filter is a single static `context` on +`action_communication_list` itself — there is only one "Notices" menu/action, shared by every +group that can open it. This means `group_academic_admin`/`group_director` also open Notices +with "Show only mine" checked by default, exactly like HOS/DHOS/the Quality coordinator; they +are **not** exempt from it. This is intentional (developer feedback 2026-09-05): the intent is +"every teacher-held role gets a comfortable own-records default", and since admin/director are +normally held by real teachers too, giving them the same default is correct, not an oversight. +The only case that's genuinely different is a non-teacher administrative account (e.g. a plain +system `admin` login with no `hr.employee` behind it) — determining "is this specific user a +teacher" from inside a static XML action `context` isn't possible (it has no ORM access, just +literals like `uid`/`context_today`), so no attempt is made to special-case it. That account +gets the same default-on filter as everyone else and removes it manually the first time (or +uses Odoo's own per-user "Save current search" star, unchecking the filter first and ticking +"Default filter", to make the removal stick permanently for just that login) — accepted as +sufficient, see `docs/en/admin/notice.md`. + +**Bug fixed in this pass:** `rule_notice_own` previously had **no `groups` restriction at +all** (a "global" rule). Odoo combines a global rule with every other rule via **AND**, not as +an alternative OR — so `rule_notice_admin`'s `[(1,'=',1)]` was silently ANDed with +`rule_notice_own`'s `create_uid = user.id`, meaning **an academic admin only ever saw their +own notices too**, contradicting the rule's own name/comment ("Admins see all +communications"). Verified against `odoo/addons/base/models/ir_rule.py::_compute_domain` +(global rules → `global_domains`, always ANDed; group rules the user belongs to → ORed +together, then ANDed onto the global result) and with a real before/after test +(`TestNoticeAccessControl.test_admin_sees_all_notices`, `tests/test_notice.py`). The fix scopes +`rule_notice_own` to the two non-admin groups explicitly instead of leaving it global. Note +this is a *different* trap from `ems.enrollment`'s secretary rule in +`docs/en/developers/contacts/enrollment.md` (an unrestricted **group-scoped** rule made moot by +a `0`-everywhere model-access ceiling, not a global rule ANDing against another group's rule) — +that file's "inert, not a bug" conclusion still holds and wasn't affected by this fix. + +`unlink()` is now also guarded in Python: a notice can only be hard-deleted while in `draft` +state (nothing sent yet); once scheduled/sent/failed, `UserError` tells the caller to +**archive** it instead (`ems.base`'s standard `action_archive()` / `active` field). This +applies to every group, including admins, since a sent notice has real delivery history +(`queue.job` records via `notice_line_id.notification_id`) worth preserving. ## Views diff --git a/docs/en/head_of_studies/attendance-corrections.md b/docs/en/head_of_studies/attendance-corrections.md index 2c858749..99d7d78e 100644 --- a/docs/en/head_of_studies/attendance-corrections.md +++ b/docs/en/head_of_studies/attendance-corrections.md @@ -15,6 +15,8 @@ Teachers can request a correction to a check-in/check-out time on their own atte If a request has been routed to you (you'll see it as an **Activity** to-do, and it will appear in **Employee Attendances → Correction Requests**): 1. Open the request — either from the activity, from **Employee Attendances → Correction Requests**, or from the **Corrections** button on the attendance record itself. + + > The list defaults to showing only **Pending** requests, so you're not wading through already-decided ones. Remove the **Pending** filter (or switch to the **Accepted**/**Rejected** filter) to see the rest. 2. Review the original time against the requested one, and the reason given. 3. Click **Accept** to apply the correction to the attendance record, or **Reject** to leave it unchanged (or restore it, if you're reversing a previous acceptance). You can optionally leave a note for the teacher. 4. The teacher who made the request is notified of your decision automatically. diff --git a/docs/en/head_of_studies/index.md b/docs/en/head_of_studies/index.md index 29249dc0..03eb4b4f 100644 --- a/docs/en/head_of_studies/index.md +++ b/docs/en/head_of_studies/index.md @@ -16,6 +16,7 @@ This section contains the manuals for **Head of Studies, Deputy Head of Studies - [A Group's Weekly Schedule](../admin/group-schedule.md) - [Attendance Reports](attendance-reports.md) - [Creating and Editing Teachers](staff-management.md) +- [Notices: Sending Your Own Bulk Emails](notice.md) --- diff --git a/docs/en/head_of_studies/notice.md b/docs/en/head_of_studies/notice.md new file mode 100644 index 00000000..17bd5f2a --- /dev/null +++ b/docs/en/head_of_studies/notice.md @@ -0,0 +1,50 @@ +[Català](../../ca/head_of_studies/notice.md) | [Castellano](../../es/head_of_studies/notice.md) | [English](notice.md) + +--- + +# Notices: Sending Your Own Bulk Emails + +This page covers **Communications → Notices** for Head of Studies, Deputy Head of Studies and +the Quality coordinator. The screen and the create/send workflow are exactly the same one +described in the [Administrator manual](../admin/notice.md) — this page only covers what's +different about who sees what. + +**Required role:** Head of Studies / Deputy Head of Studies / Director / Quality coordinator + +--- + +## Visibility: Your List Starts Filtered to Your Own Notices + +If you hold the **Head of Studies**, **Deputy Head of Studies** or **Quality coordinator** +role, **Communications → Notices** opens with a **"Show only mine"** filter already applied +(visible as a tag in the search bar), so day to day you comfortably work with just the notices +**you personally created** — the same experience as everyone else. + +If you ever need to check what a colleague holding the same role has sent — for supervision — +click the **✕** on the "Show only mine" tag in the search bar (or open the search panel and +untick it) to see every notice centre-wide. You can still only **edit or delete your own**; +someone else's notice opens in read-only mode. + +The **Director** gets the exact same default filter (so does an Administrator) — the +difference is only in what removing it lets you *do*, not who sees it: a Director can fully +edit any notice once the filter is off, while you can only edit or delete your own regardless. +See the [Administrator manual](../admin/notice.md#who-sees-which-notices). + +--- + +## Creating, Sending and Deleting + +Follow the same steps as the [Administrator manual](../admin/notice.md#creating-and-sending-a-notice): +compose the subject and message, review or edit the pre-filled **Signature**, pick the +recipient groups, and send immediately or schedule it. A notice you created can only be +permanently deleted while still in **Draft** — once scheduled or sent, archive it instead (see +[Deleting vs. Archiving](../admin/notice.md#deleting-vs-archiving)). + +The Signature starts pre-filled from the centre's shared default, which only an Administrator +can change (**Settings → EMS Management**) — but you can freely edit or clear it on any notice +you create, without needing that permission yourself. Whoever replies to your notice reaches +you directly, not a shared technical address. + +--- + +[← Back to Head of Studies manuals](index.md) diff --git a/docs/en/teachers/attendance-corrections.md b/docs/en/teachers/attendance-corrections.md index 5c3f1ccf..f3baa3c3 100644 --- a/docs/en/teachers/attendance-corrections.md +++ b/docs/en/teachers/attendance-corrections.md @@ -51,6 +51,8 @@ Your request is sent automatically to whoever can validate it — normally your - **Employee Attendances → Correction Requests** lists all the requests you've made and their current status (Pending / Accepted / Rejected). - From the attendance record itself, the **Corrections** button in the header (only visible if a request exists for that record) takes you straight to it. +> By default the list only shows **Pending** requests. Remove the **Pending** filter from the search bar (or switch to the **Accepted**/**Rejected** filter instead) to see requests that already have a decision. + > If you're also a Head of Studies, Deputy Head of Studies or Director, see the [Head of Studies manual](../head_of_studies/attendance-corrections.md) for how to decide on requests sent to you. --- diff --git a/docs/es/admin/index.md b/docs/es/admin/index.md index 00bd91ef..fa17755b 100644 --- a/docs/es/admin/index.md +++ b/docs/es/admin/index.md @@ -26,6 +26,8 @@ Esta sección contiene los manuales para **administradores**. - [El horario semanal de un grupo](group-schedule.md) — Consultar el horario agregado de un grupo (asignaturas, docentes, aulas, patios) y exportarlo a PDF. - [Preparar el curso siguiente](course-transition.md) — Cerrar el curso: archivar el historial académico, graduar y archivar a los exalumnos, colocar a todos en su grupo nuevo y cambiar el curso actual. - [Importar las notas desde Esfera](grade-import.md) — Cargar en EMS las notas oficiales de cada evaluación y, opcionalmente, crear las matrículas que falten. +- [Comunicados: enviar correos masivos a alumnos y familias](notice.md) — Redactar y enviar un Comunicado, y quién ve qué comunicados. +- [Encuestas: integración con LimeSurvey](survey.md) — El ciclo de vida de la encuesta (borrador → destinatarios → subida → abierta → cerrada → descarga) y quién puede gestionar qué encuestas. --- diff --git a/docs/es/admin/notice.md b/docs/es/admin/notice.md new file mode 100644 index 00000000..afb645e8 --- /dev/null +++ b/docs/es/admin/notice.md @@ -0,0 +1,109 @@ +[Català](../../ca/admin/notice.md) | [Castellano](notice.md) | [English](../../en/admin/notice.md) + +--- + +# Comunicados: enviar correos masivos a alumnos y familias + +**Rol necesario:** Administrador (o Director, que tiene la misma visibilidad completa — ver más abajo) + +--- + +## Qué es un Comunicado + +Un **Comunicado** es un correo electrónico masivo enviado a un conjunto de alumnos y/o sus +familias — por ejemplo, un recordatorio de un plazo o un aviso que afecta a uno o más grupos. +Se encuentra en **Comunicaciones → Comunicados**. + +--- + +## Crear y enviar un comunicado + +1. **Comunicaciones → Comunicados → Nuevo**. +2. Rellene el **Asunto** y el **Mensaje** (texto enriquecido, admite imágenes). +3. Revise la **Firma**, justo debajo — viene precargada con la de su centro (vea + [Personalizar la firma](#personalizar-la-firma) más abajo), pero puede editarla o borrarla + libremente solo para este comunicado. +4. Elija **Enviar a**: Alumnos, Familias, o Ambos. +5. Si la selección incluye alumnos, elija **Correo del destinatario**: **Corporativo** (la + dirección institucional de Google Workspace del alumno), **Personal** (su dirección + personal), o **Ambos** (por defecto) — si el alumno tiene las dos direcciones, "Ambos" envía + el comunicado a cada una por separado. Esta opción no tiene ningún efecto sobre las + familias, ya que solo tienen una única dirección de correo. +6. Añada uno o más **Grupos** — la lista de destinatarios se genera automáticamente a partir de + los alumnos de cada grupo y, cuando se selecciona "Familias"/"Ambos", sus contactos + familiares vinculados (las familias de un alumno menor siempre se incluyen; las de un alumno + mayor de edad solo si el alumno ha autorizado explícitamente compartirlo). +7. Revise la **Lista de destinatarios** — también puede añadir o eliminar filas manualmente; + las filas manuales se conservan aunque cambie los grupos seleccionados después. Si algún + alumno no tiene ninguna dirección que coincida con su selección de **Correo del + destinatario** (p. ej. eligió "Corporativo" pero aún no tiene cuenta institucional creada), + aparece un aviso con sus nombres para que sepa que se han quedado fuera. +8. Haga una de las dos opciones: + - Pulse **Enviar** para poner los correos en cola inmediatamente, o + - Marque **Programar el envío** y elija una fecha/hora, y pulse **Enviar** — el comunicado + pasa a **Programado** y los correos salen en ese momento. +9. El **Estado** del comunicado sigue el progreso: **Borrador** → **Programado** → **Enviado** + (o **Fallido** si el envío ha fallado para todos los destinatarios). Cada fila de + destinatario muestra su propio estado de envío, con el detalle del error disponible en las + filas fallidas. + +Un comunicado **programado** (aún no enviado) se puede **cancelar**, devolviéndolo a Borrador +para que pueda editarlo y volver a enviarlo. + +Si un destinatario pulsa **Responder** al correo que ha recibido, la respuesta llega +directamente a quien envió el comunicado — no a una dirección técnica compartida — así una +conversación iniciada desde un comunicado llega a la persona correcta. + +--- + +## Personalizar la firma + +Todo correo de comunicado termina con una **Firma** — por defecto, la que esté configurada +para todo el centro en **Ajustes → EMS Management → Firma de los correos de los comunicados**, +un campo de texto enriquecido que puede escribir como quiera (un nombre, un cargo, datos de +contacto — o dejarlo en blanco para no tener ninguna firma). Es traducible: use el pequeño +icono de traducción junto al campo para escribir una versión distinta por idioma, de modo que +cada destinatario vea la firma en su propio idioma automáticamente. + +Cambiar la firma por defecto del centro solo afecta a los **comunicados creados a partir de +ahora** — cada comunicado ya existente tiene su propia copia de la firma (del paso 3 anterior), +que también puede sobrescribir individualmente sin tocar la del centro. + +--- + +## Quién ve qué comunicados + +Todo el mundo con acceso a Comunicados — administradores, Director, Jefe de estudios, Jefe de +estudios adjunto y coordinador de calidad por igual — ve todos los comunicados de todo el +centro, pero la lista siempre se abre filtrada con **"Mostrar solo los míos"** por defecto, de +modo que en el día a día todo el mundo trabaja cómodamente solo con los suyos. Si quita ese +filtro (en la barra de búsqueda, en la parte superior de la lista) verá los comunicados de todo +el mundo, para cuando necesite supervisar. + +- **Los administradores y el Director** pueden gestionar completamente cualquier comunicado + independientemente del filtro — solo afecta a lo que se **muestra** por defecto, no a lo que + pueden hacer. +- El **Jefe de estudios, el Jefe de estudios adjunto** y el **coordinador de calidad** solo + pueden editar o eliminar los comunicados que ellos mismos han creado — el comunicado de otra + persona se abre en modo solo lectura incluso con el filtro quitado. Vea el + [manual de Jefe de estudios](../head_of_studies/notice.md) para su perspectiva. + +Si su cuenta no está vinculada a ningún docente (un caso poco habitual — la mayoría de cuentas +de Administrador/Director corresponden a un docente real) y prefiere no ver nunca marcado +"Mostrar solo los míos", quítelo una vez y use **Favoritos → Guardar búsqueda actual** en la +barra de búsqueda, marcando **Filtro por defecto** — Odoo lo recordará desde entonces para ese +usuario. + +--- + +## Eliminar frente a archivar + +Un comunicado solo se puede eliminar de forma permanente mientras esté en **Borrador** — una +vez programado, enviado, o fallido, EMS bloquea la eliminación (tiene un historial de envío +real que merece la pena conservar) y le pide que lo **archive** en su lugar (menú ⚙ → +Archivar). Los comunicados archivados quedan ocultos de la lista por defecto; use **Filtros → +Archivado** para volver a encontrarlos. + +--- + +[← Volver a los manuales de Administrador](index.md) diff --git a/docs/es/admin/survey.md b/docs/es/admin/survey.md new file mode 100644 index 00000000..fa83c1fd --- /dev/null +++ b/docs/es/admin/survey.md @@ -0,0 +1,79 @@ +[Català](../../ca/admin/survey.md) | [Castellano](survey.md) | [English](../../en/admin/survey.md) + +--- + +# Encuestas: integración con LimeSurvey + +**Rol necesario:** Administrador o Coordinador de calidad (vea [Visibilidad](#visibilidad-quién-ve-qué-encuestas) más abajo para la diferencia entre ambos) + +--- + +## Qué es una encuesta + +La funcionalidad de **Encuestas** de EMS (**Comunicaciones → Encuestas**) genera y gestiona +cuestionarios de LimeSurvey para alumnos, docentes o personal PAS — encuestas de evaluación/ +satisfacción enviadas y seguidas sin salir de EMS. No confundir con la app nativa de Surveys +de Odoo, que en esta instalación está oculta. + +--- + +## El ciclo de vida de una encuesta + +Una encuesta pasa por una secuencia fija de estados a medida que se trabaja en ella: + +1. **Borrador** — defina el **Título**, la **Descripción**, el **Objetivo** (Alumnos / Docentes + / PAS) y sus **Bloques** de contenido (las preguntas/secciones, como plantillas separadas + por tabuladores). +2. **Calcular destinatarios** — EMS determina quién debe recibir la encuesta (filtrado por + Nivel/Estudio/Grupo, o por reglas especiales por asignatura/prácticas en bloques + individuales) y construye la lista de **Destinatarios**, cada uno con su propia foto fija de + matrícula. +3. **Subir** — la encuesta y sus destinatarios se crean en el propio LimeSurvey mediante su + API. +4. **Abrir** — la encuesta queda activa; los destinatarios pueden responder. Use **Recordar** + para reenviar la invitación a quien aún no haya respondido. +5. **Cerrar** — deja de aceptar respuestas. +6. **Descargar** — trae los datos de respuesta de vuelta a EMS como CSV, listos para el + análisis (por ejemplo, en Metabase). + +Puede devolver una encuesta subida/calculada a **Borrador** (recalculando los destinatarios +desde cero) en cualquier momento antes de cerrarla. + +--- + +## Visibilidad: quién ve qué encuestas + +Todo el mundo con acceso a Encuestas — administradores y coordinador de calidad por igual — ve +todas las encuestas de todo el centro, pero la lista siempre se abre filtrada con **"Mostrar +solo las mías"** por defecto (una etiqueta en la barra de búsqueda), de modo que en el día a día +todo el mundo trabaja cómodamente solo con las suyas. Si quita ese filtro verá todas las +encuestas de todo el centro, para cuando necesite revisar el trabajo de otra persona. + +- Los **Administradores** pueden gestionar completamente cualquier encuesta independientemente + del filtro — solo afecta a lo que se **muestra** por defecto, no a lo que pueden hacer. +- El **Coordinador de calidad** solo puede **crear, editar o eliminar las encuestas que él + mismo haya creado** — la encuesta de otra persona se abre en modo solo lectura incluso con el + filtro quitado. +- Un miembro normal del **equipo de calidad** (que no sea el coordinador) conserva el acceso + sin restricciones para crear/editar todas las encuestas, igual que antes — esta distinción + solo se aplica al rol de coordinador. + +Si su cuenta no está vinculada a ningún docente y prefiere no ver nunca marcado "Mostrar solo +las mías", quítelo una vez y use **Favoritos → Guardar búsqueda actual** en la barra de +búsqueda, marcando **Filtro por defecto** — Odoo lo recordará desde entonces para ese usuario. + +--- + +## Eliminar una encuesta + +- Una encuesta se puede eliminar mientras esté en estado **Borrador**, **Destinatarios + calculados**, o **Cerrada**. +- Eliminar una encuesta **Cerrada** también la elimina de forma permanente de LimeSurvey — si + los datos de respuesta aún no se han descargado, se pierden para siempre. EMS pide + confirmación antes de hacerlo. +- Una encuesta que esté **Subida**, **Abierta**, o en otro estado intermedio no se puede + eliminar directamente — hay que cerrarla primero. + +--- + +[← Volver a los manuales de Administrador](index.md) diff --git a/docs/es/head_of_studies/attendance-corrections.md b/docs/es/head_of_studies/attendance-corrections.md index 6319b594..c1b41cd5 100644 --- a/docs/es/head_of_studies/attendance-corrections.md +++ b/docs/es/head_of_studies/attendance-corrections.md @@ -15,6 +15,8 @@ Los profesores pueden solicitar una corrección de una hora de entrada/salida de Si te han enviado una solicitud (la verás como una actividad pendiente, y también aparecerá en **Fichajes de empleados → Solicitudes de corrección**): 1. Abre la solicitud — desde la actividad, desde **Fichajes de empleados → Solicitudes de corrección**, o desde el botón **Correcciones** del propio fichaje. + + > La lista muestra solo las solicitudes **Pendientes** por defecto, para no tener que revisar las que ya tienen una decisión. Quita el filtro **Pendiente** (o cambia al filtro **Aceptada**/**Rechazada**) para ver el resto. 2. Revisa la hora original frente a la solicitada, y el motivo indicado. 3. Haz clic en **Aceptar** para aplicar la corrección al fichaje, o en **Rechazar** para dejarlo sin cambios (o restaurarlo, si estás deshaciendo una aceptación anterior). Puedes dejar una nota opcional para el profesor o profesora. 4. El profesor o profesora que hizo la solicitud recibe una notificación automática con tu decisión. diff --git a/docs/es/head_of_studies/index.md b/docs/es/head_of_studies/index.md index 7a6949fb..696a831c 100644 --- a/docs/es/head_of_studies/index.md +++ b/docs/es/head_of_studies/index.md @@ -16,6 +16,7 @@ Esta sección contiene los manuales para **Jefatura de Estudios, Jefatura de Est - [El horario semanal de un grupo](../admin/group-schedule.md) - [Informes de asistencia](attendance-reports.md) - [Crear y editar profesorado](staff-management.md) +- [Comunicados: enviar tus propios correos masivos](notice.md) --- diff --git a/docs/es/head_of_studies/notice.md b/docs/es/head_of_studies/notice.md new file mode 100644 index 00000000..dc4a449e --- /dev/null +++ b/docs/es/head_of_studies/notice.md @@ -0,0 +1,54 @@ +[Català](../../ca/head_of_studies/notice.md) | [Castellano](notice.md) | [English](../../en/head_of_studies/notice.md) + +--- + +# Comunicados: enviar tus propios correos masivos + +Esta página cubre **Comunicaciones → Comunicados** para el Jefe de estudios, el Jefe de +estudios adjunto y el coordinador de calidad. La pantalla y el flujo de crear/enviar es +exactamente el mismo que se describe en el [manual de Administrador](../admin/notice.md) — +esta página solo cubre qué es diferente sobre quién ve qué. + +**Rol necesario:** Jefe de estudios / Jefe de estudios adjunto / Director / Coordinador de calidad + +--- + +## Visibilidad: tu lista empieza filtrada a tus comunicados + +Si tienes el rol de **Jefe de estudios**, **Jefe de estudios adjunto** o **Coordinador de +calidad**, **Comunicaciones → Comunicados** se abre con el filtro **"Mostrar solo los míos"** +ya aplicado (visible como una etiqueta en la barra de búsqueda), de modo que en el día a día +trabajas cómodamente solo con los comunicados que **tú mismo/a has creado** — la misma +experiencia que el resto. + +Si alguna vez necesitas comprobar qué ha enviado un compañero con el mismo rol — para +supervisar — pulsa la **✕** de la etiqueta "Mostrar solo los míos" en la barra de búsqueda (o +abre el panel de búsqueda y desmárcala) para ver todos los comunicados de todo el centro. Solo +podrás **editar o eliminar los tuyos propios**; el comunicado de otra persona se abre en modo +solo lectura. + +El **Director** tiene exactamente el mismo filtro por defecto (igual que un Administrador) — +la diferencia está solo en lo que le permite hacer al quitarlo, no en quién lo ve: un Director +puede editar cualquier comunicado una vez quitado el filtro, mientras que tú solo puedes editar +o eliminar los tuyos propios independientemente del filtro. Vea el +[manual de Administrador](../admin/notice.md#quién-ve-qué-comunicados). + +--- + +## Crear, enviar y eliminar + +Siga los mismos pasos del +[manual de Administrador](../admin/notice.md#crear-y-enviar-un-comunicado): redacte el asunto +y el mensaje, revise o edite la **Firma** precargada, elija los grupos destinatarios, y envíelo +inmediatamente o prográmelo. Un comunicado que haya creado solo se puede eliminar de forma +permanente mientras esté en **Borrador** — una vez programado o enviado, archívelo en su lugar +(vea [Eliminar frente a archivar](../admin/notice.md#eliminar-frente-a-archivar)). + +La Firma empieza precargada con la del centro, que solo un Administrador puede cambiar +(**Ajustes → EMS Management**) — pero puede editarla o borrarla libremente en cualquier +comunicado que cree, sin necesitar ese permiso. Quien responda a su comunicado le llega +directamente a usted, no a una dirección técnica compartida. + +--- + +[← Volver a los manuales de Jefe de estudios](index.md) diff --git a/docs/es/teachers/attendance-corrections.md b/docs/es/teachers/attendance-corrections.md index 8263215b..f9d34e73 100644 --- a/docs/es/teachers/attendance-corrections.md +++ b/docs/es/teachers/attendance-corrections.md @@ -51,6 +51,8 @@ Tu solicitud se envía automáticamente a quien puede validarla — normalmente - **Fichajes de empleados → Solicitudes de corrección** muestra todas las solicitudes que has hecho y su estado actual (Pendiente / Aceptada / Rechazada). - Desde el propio fichaje, el botón **Correcciones** de la cabecera (solo visible si existe alguna solicitud para ese registro) te lleva directamente a ella. +> De forma predeterminada, la lista solo muestra las solicitudes **Pendientes**. Quita el filtro **Pendiente** de la barra de búsqueda (o cambia al filtro **Aceptada**/**Rechazada**) para ver las solicitudes que ya tienen una decisión. + > Si también eres Jefatura de Estudios, Jefatura de Estudios Adjunta o Dirección, consulta el [manual de Jefatura de Estudios](../head_of_studies/attendance-corrections.md) para saber cómo decidir sobre las solicitudes que te llegan. --- diff --git a/i18n/ca_ES.po b/i18n/ca_ES.po index ab858815..c3f19c83 100644 --- a/i18n/ca_ES.po +++ b/i18n/ca_ES.po @@ -1305,7 +1305,7 @@ msgid "Acceptance Only" msgstr "Només acceptació" #. module: ems -#: model:ir.model.fields.selection,name:ems.selection__ems_authorization__status__yes model_terms:ir.ui.view,arch_db:ems.portal_enrollment_process +#: model:ir.model.fields.selection,name:ems.selection__ems_attendance_correction__state__accepted model:ir.model.fields.selection,name:ems.selection__ems_authorization__status__yes model_terms:ir.ui.view,arch_db:ems.portal_enrollment_process model_terms:ir.ui.view,arch_db:ems.view_attendance_correction_search msgid "Accepted" msgstr "Acceptat" @@ -1714,7 +1714,7 @@ msgid "Approved" msgstr "Aprovat" #. module: ems -#: model_terms:ir.ui.view,arch_db:ems.view_ems_enrollment_items_search model_terms:ir.ui.view,arch_db:ems.view_attendance_template_search model_terms:ir.ui.view,arch_db:ems.view_attendance_session_search +#: model_terms:ir.ui.view,arch_db:ems.view_ems_enrollment_items_search model_terms:ir.ui.view,arch_db:ems.view_attendance_template_search model_terms:ir.ui.view,arch_db:ems.view_attendance_session_search model_terms:ir.ui.view,arch_db:ems.view_notice_search model_terms:ir.ui.view,arch_db:ems.view_limesurvey_header_search msgid "Archived" msgstr "Arxivat" @@ -2214,9 +2214,9 @@ msgid "Bonifications & Exemptions" msgstr "Bonificacions i exempcions" #. module: ems -#: model:ir.model.fields.selection,name:ems.selection__ems_notice__recipient_type__both +#: model:ir.model.fields.selection,name:ems.selection__ems_notice__recipient_type__both model:ir.model.fields.selection,name:ems.selection__ems_notice__recipient_email_type__both msgid "Both" -msgstr "" +msgstr "Ambdós" #. module: ems #: model_terms:ir.ui.view,arch_db:ems.attendance_report_group model_terms:ir.ui.view,arch_db:ems.attendance_report_subject model_terms:ir.ui.view,arch_db:ems.attendance_template_sumary_table @@ -2295,6 +2295,12 @@ msgstr "" msgid "Cannot delete this survey in its current state. Only surveys in 'Draft', 'Recipients computed', or 'Closed' state can be deleted. If the survey is active or in progress, please close it first." msgstr "No es pot eliminar aquesta enquesta en el seu estat actual. Només es poden eliminar enquestes en estat 'Esborrany', 'Destinataris calculats' o 'Tancada'. Si l'enquesta està activa o en curs, tanca-la primer." +#. module: ems +#. odoo-python +#: code:addons/ems/models/communications/notice.py:0 +msgid "Cannot delete a notice that has already been scheduled, sent or failed to send. Please archive it instead." +msgstr "No es pot eliminar un comunicat que ja ha estat programat, enviat o que ha fallat en l'enviament. Arxiva'l en el seu lloc." + #. module: ems #. odoo-python #: code:addons/ems/models/communications/limesurvey.py:0 @@ -2304,7 +2310,7 @@ msgstr "Properament..." #. module: ems #: model:ir.model.fields,field_description:ems.field_res_partner__car_plate model:ir.model.fields,field_description:ems.field_res_users__car_plate msgid "Car Plate" -msgstr "Placa de cotxe" +msgstr "Matrícula del vehicle" #. module: ems #: model:ir.model.fields,field_description:ems.field_ems_student_update_wizard__col_car_plate @@ -2577,6 +2583,11 @@ msgstr "Auxiliar de conversa" msgid "Corporate account credentials" msgstr "Credencials del compte corporatiu" +#. module: ems +#: model_terms:ir.ui.view,arch_db:ems.view_contact_form +msgid "Corporate email" +msgstr "Correu corporatiu" + #. module: ems #: model_terms:ir.ui.view,arch_db:ems.res_config_settings_view_form msgid "Corporate domain for student accounts (e.g. elpuig.xeill.net)." @@ -3193,7 +3204,7 @@ msgid "Emergency" msgstr "Emergència" #. module: ems -#: model:ir.model,name:ems.model_hr_employee +#: model:ir.model,name:ems.model_hr_employee model_terms:ir.ui.view,arch_db:ems.view_attendance_correction_search msgid "Employee" msgstr "Empleat" @@ -4481,9 +4492,9 @@ msgstr "Última actualització el" #. module: ems #. odoo-python -#: code:addons/ems/models/contacts/google_workspace_integration.py:0 model:ir.model.fields,field_description:ems.field_ems_contact_relation_wizard__lastname model:ir.model.fields,field_description:ems.field_ems_grade_outcome_line__student_lastname code:addons/ems/static/src/js/backend/em_matrix_field.js:0 +#: code:addons/ems/models/contacts/google_workspace_integration.py:0 model:ir.model.fields,field_description:ems.field_ems_contact_relation_wizard__lastname model:ir.model.fields,field_description:ems.field_ems_grade_outcome_line__student_lastname model:ir.model.fields,field_description:partner_firstname.field_res_partner__lastname model:ir.model.fields,field_description:partner_firstname.field_res_users__lastname code:addons/ems/static/src/js/backend/em_matrix_field.js:0 msgid "Last name" -msgstr "Cognom" +msgstr "Cognoms" #. module: ems #: model:ir.model.fields,field_description:ems.field_ems_criteria__outcome_id model:ir.model.fields,field_description:ems.field_ems_planning_outcome__valid_outcome_ids model:ir.model.fields,field_description:ems.field_ems_subject__outcome_ids model_terms:ir.ui.view,arch_db:ems.view_subject_form @@ -4998,6 +5009,18 @@ msgstr "No s'ha trobat planificació per a l'estudi i l'assignatura d'aquest gru msgid "No recipients to send to. Add recipients in the 'Sending status' section." msgstr "" +#. module: ems +#. odoo-python +#: code:addons/ems/models/communications/notice.py:0 +msgid "Students excluded" +msgstr "Alumnes exclosos" + +#. module: ems +#. odoo-python +#: code:addons/ems/models/communications/notice.py:0 +msgid "The following students have no email address matching your 'Recipient email' choice, so they were not added to the recipient list:\n%s" +msgstr "Els següents alumnes no tenen cap adreça de correu que coincideixi amb la vostra selecció de 'Correu del destinatari', per la qual cosa no s'han afegit a la llista de destinataris:\n%s" + #. module: ems #. odoo-javascript #: code:addons/ems/static/src/xml/backend/grade_matrix_field.xml:0 @@ -5349,6 +5372,12 @@ msgstr "Gestió d'avaluacions" msgid "You do not tutor any group with an open evaluation." msgstr "No ets tutor de cap grup amb avaluació oberta" +#. module: ems +#. odoo-python +#: code:addons/ems/models/contacts/contact_relation.py:0 +msgid "You are not allowed to manage this student's family contacts." +msgstr "No tens permís per gestionar els contactes familiars d'aquest alumne." + #. module: ems #. odoo-javascript #: code:addons/ems/static/src/xml/backend/grade_tutor_matrix.xml:0 @@ -5449,7 +5478,7 @@ msgid "Payment plan" msgstr "Pla de pagament" #. module: ems -#: model:ir.model.fields,field_description:ems.field_ems_attendance_issue_status__pending model:ir.model.fields.selection,name:ems.selection__ems_authorization__status__pending model:ir.model.fields.selection,name:ems.selection__ems_limesurvey_recipient__state__pending model:ir.model.fields.selection,name:ems.selection__ems_notice_line__display_status__pending model_terms:ir.ui.view,arch_db:ems.portal_enrollment_process model_terms:ir.ui.view,arch_db:ems.view_student_document_search +#: model:ir.model.fields,field_description:ems.field_ems_attendance_issue_status__pending model:ir.model.fields.selection,name:ems.selection__ems_attendance_correction__state__pending model:ir.model.fields.selection,name:ems.selection__ems_authorization__status__pending model:ir.model.fields.selection,name:ems.selection__ems_limesurvey_recipient__state__pending model:ir.model.fields.selection,name:ems.selection__ems_notice_line__display_status__pending model_terms:ir.ui.view,arch_db:ems.portal_enrollment_process model_terms:ir.ui.view,arch_db:ems.view_attendance_correction_search model_terms:ir.ui.view,arch_db:ems.view_student_document_search msgid "Pending" msgstr "Pendent" @@ -5487,7 +5516,7 @@ msgstr "Modificació de dades personals de l'alumne %s" #. module: ems #. odoo-python -#: code:addons/ems/models/contacts/google_workspace_integration.py:0 +#: code:addons/ems/models/contacts/google_workspace_integration.py:0 model_terms:ir.ui.view,arch_db:ems.view_contact_form msgid "Personal email" msgstr "Correu personal" @@ -5859,7 +5888,7 @@ msgid "Reject" msgstr "Rebutjar" #. module: ems -#: model:ir.model.fields.selection,name:ems.selection__ems_authorization__status__no model:ir.model.fields.selection,name:ems.selection__ems_student_document__status__rejected model_terms:ir.ui.view,arch_db:ems.portal_enrollment_process model_terms:ir.ui.view,arch_db:ems.view_student_document_search +#: model:ir.model.fields.selection,name:ems.selection__ems_attendance_correction__state__rejected model:ir.model.fields.selection,name:ems.selection__ems_authorization__status__no model:ir.model.fields.selection,name:ems.selection__ems_student_document__status__rejected model_terms:ir.ui.view,arch_db:ems.portal_enrollment_process model_terms:ir.ui.view,arch_db:ems.view_attendance_correction_search model_terms:ir.ui.view,arch_db:ems.view_student_document_search msgid "Rejected" msgstr "Rebutjat" @@ -6273,6 +6302,21 @@ msgstr "Secretaria" msgid "Secretariat Email" msgstr "Correu de secretaria" +#. module: ems +#: model:ir.model.fields,field_description:ems.field_res_company__notice_email_signature model:ir.model.fields,field_description:ems.field_res_config_settings__notice_email_signature +msgid "Notice email signature" +msgstr "Signatura dels correus dels comunicats" + +#. module: ems +#: model:ir.model.fields,help:ems.field_res_company__notice_email_signature model_terms:ir.ui.view,arch_db:ems.res_config_settings_view_form +msgid "Default sign-off appended to every Notice email. Copied onto each new notice as its own editable signature - editing it here only affects notices created afterward." +msgstr "Comiat per defecte que s'afegeix a tots els correus dels comunicats. Es copia a cada comunicat nou com a signatura pròpia editable - modificar-lo aquí només afecta els comunicats creats a partir d'ara." + +#. module: ems +#: model:ir.model.fields,field_description:ems.field_ems_notice__signature +msgid "Signature" +msgstr "Signatura" + #. module: ems #: model:ems.role,name:ems.role_secretary model:hr.job,name:ems.job_secretary model:ir.model.fields.selection,name:ems.selection__hr_department__top_level_role__secretary model:project.project,name:ems.project_secretary model:res.groups,name:ems.group_secretary model_terms:ir.ui.view,arch_db:ems.view_contact_form msgid "Secretary" @@ -6366,6 +6410,21 @@ msgstr "Enviar recordatoris" msgid "Send to" msgstr "Enviar a" +#. module: ems +#: model:ir.model.fields,field_description:ems.field_ems_notice__recipient_email_type +msgid "Recipient email" +msgstr "Correu del destinatari" + +#. module: ems +#: model:ir.model.fields.selection,name:ems.selection__ems_notice__recipient_email_type__corporate +msgid "Corporate" +msgstr "Corporatiu" + +#. module: ems +#: model:ir.model.fields.selection,name:ems.selection__ems_notice__recipient_email_type__personal +msgid "Personal" +msgstr "Personal" + #. module: ems #: model_terms:ir.ui.view,arch_db:ems.portal_enrollment_process msgid "Send your comments to the secretary's office" @@ -6458,7 +6517,7 @@ msgid "Short Text" msgstr "Text curt" #. module: ems -#: model_terms:ir.ui.view,arch_db:ems.view_attendance_justification_search model_terms:ir.ui.view,arch_db:ems.view_attendance_session_search model_terms:ir.ui.view,arch_db:ems.view_attendance_template_search +#: model_terms:ir.ui.view,arch_db:ems.view_attendance_justification_search model_terms:ir.ui.view,arch_db:ems.view_attendance_session_search model_terms:ir.ui.view,arch_db:ems.view_attendance_template_search model_terms:ir.ui.view,arch_db:ems.view_notice_search model_terms:ir.ui.view,arch_db:ems.view_limesurvey_header_search msgid "Show only mine" msgstr "Mostrar tan sols els meuss" @@ -6629,7 +6688,7 @@ msgid "State / Province" msgstr "Estat / Província" #. module: ems -#: model:ir.model.fields,field_description:ems.field_ems_attendance_issue_tutor__status model:ir.model.fields,field_description:ems.field_ems_attendance_session_line__status_id model:ir.model.fields,field_description:ems.field_ems_authorization__status model:ir.model.fields,field_description:ems.field_ems_notice_line__display_status model:ir.model.fields,field_description:ems.field_ems_student_document__status model_terms:ir.ui.view,arch_db:ems.attendance_report_session model_terms:ir.ui.view,arch_db:ems.portal_documentation model_terms:ir.ui.view,arch_db:ems.portal_enrollment_process model_terms:ir.ui.view,arch_db:ems.view_student_document_search model_terms:ir.ui.view,arch_db:ems.view_attendance_report_analysis_search +#: model:ir.model.fields,field_description:ems.field_ems_attendance_issue_tutor__status model:ir.model.fields,field_description:ems.field_ems_attendance_session_line__status_id model:ir.model.fields,field_description:ems.field_ems_authorization__status model:ir.model.fields,field_description:ems.field_ems_notice_line__display_status model:ir.model.fields,field_description:ems.field_ems_student_document__status model_terms:ir.ui.view,arch_db:ems.attendance_report_session model_terms:ir.ui.view,arch_db:ems.portal_documentation model_terms:ir.ui.view,arch_db:ems.portal_enrollment_process model_terms:ir.ui.view,arch_db:ems.view_attendance_correction_search model_terms:ir.ui.view,arch_db:ems.view_student_document_search model_terms:ir.ui.view,arch_db:ems.view_attendance_report_analysis_search msgid "Status" msgstr "Estat" diff --git a/i18n/es_ES.po b/i18n/es_ES.po index 479a8f1f..ce967fa8 100644 --- a/i18n/es_ES.po +++ b/i18n/es_ES.po @@ -1305,7 +1305,7 @@ msgid "Acceptance Only" msgstr "Solo aceptación" #. module: ems -#: model:ir.model.fields.selection,name:ems.selection__ems_authorization__status__yes model_terms:ir.ui.view,arch_db:ems.portal_enrollment_process +#: model:ir.model.fields.selection,name:ems.selection__ems_attendance_correction__state__accepted model:ir.model.fields.selection,name:ems.selection__ems_authorization__status__yes model_terms:ir.ui.view,arch_db:ems.portal_enrollment_process model_terms:ir.ui.view,arch_db:ems.view_attendance_correction_search msgid "Accepted" msgstr "Aceptado" @@ -1714,7 +1714,7 @@ msgid "Approved" msgstr "Aprobado" #. module: ems -#: model_terms:ir.ui.view,arch_db:ems.view_ems_enrollment_items_search model_terms:ir.ui.view,arch_db:ems.view_attendance_template_search model_terms:ir.ui.view,arch_db:ems.view_attendance_session_search +#: model_terms:ir.ui.view,arch_db:ems.view_ems_enrollment_items_search model_terms:ir.ui.view,arch_db:ems.view_attendance_template_search model_terms:ir.ui.view,arch_db:ems.view_attendance_session_search model_terms:ir.ui.view,arch_db:ems.view_notice_search model_terms:ir.ui.view,arch_db:ems.view_limesurvey_header_search msgid "Archived" msgstr "Archivado" @@ -2214,9 +2214,9 @@ msgid "Bonifications & Exemptions" msgstr "Bonificaciones y exenciones" #. module: ems -#: model:ir.model.fields.selection,name:ems.selection__ems_notice__recipient_type__both +#: model:ir.model.fields.selection,name:ems.selection__ems_notice__recipient_type__both model:ir.model.fields.selection,name:ems.selection__ems_notice__recipient_email_type__both msgid "Both" -msgstr "" +msgstr "Ambos" #. module: ems #: model_terms:ir.ui.view,arch_db:ems.attendance_report_group model_terms:ir.ui.view,arch_db:ems.attendance_report_subject model_terms:ir.ui.view,arch_db:ems.attendance_template_sumary_table @@ -2295,6 +2295,12 @@ msgstr "" msgid "Cannot delete this survey in its current state. Only surveys in 'Draft', 'Recipients computed', or 'Closed' state can be deleted. If the survey is active or in progress, please close it first." msgstr "No se puede eliminar esta encuesta en su estado actual. Solo se pueden eliminar encuestas en estado 'Borrador', 'Destinatarios calculados' o 'Cerrada'. Si la encuesta está activa o en curso, ciérrala primero." +#. module: ems +#. odoo-python +#: code:addons/ems/models/communications/notice.py:0 +msgid "Cannot delete a notice that has already been scheduled, sent or failed to send. Please archive it instead." +msgstr "No se puede eliminar un comunicado que ya ha sido programado, enviado o que ha fallado en el envío. Archívalo en su lugar." + #. module: ems #. odoo-python #: code:addons/ems/models/communications/limesurvey.py:0 @@ -2304,7 +2310,7 @@ msgstr "Próximamente..." #. module: ems #: model:ir.model.fields,field_description:ems.field_res_partner__car_plate model:ir.model.fields,field_description:ems.field_res_users__car_plate msgid "Car Plate" -msgstr "Matrícula de coche" +msgstr "Matrícula del vehículo" #. module: ems #: model:ir.model.fields,field_description:ems.field_ems_student_update_wizard__col_car_plate @@ -2572,6 +2578,11 @@ msgstr "Controla cómo se determina la hora de entrada cuando un profesor crea u msgid "Conversational Auxiliary" msgstr "Auxiliar de conversa" +#. module: ems +#: model_terms:ir.ui.view,arch_db:ems.view_contact_form +msgid "Corporate email" +msgstr "Correo corporativo" + #. module: ems #: model_terms:ir.ui.view,arch_db:ems.report_google_credentials msgid "Corporate account credentials" @@ -3193,7 +3204,7 @@ msgid "Emergency" msgstr "Emergencia" #. module: ems -#: model:ir.model,name:ems.model_hr_employee +#: model:ir.model,name:ems.model_hr_employee model_terms:ir.ui.view,arch_db:ems.view_attendance_correction_search msgid "Employee" msgstr "Empleado" @@ -4481,9 +4492,9 @@ msgstr "Última actualización:" #. module: ems #. odoo-python -#: code:addons/ems/models/contacts/google_workspace_integration.py:0 model:ir.model.fields,field_description:ems.field_ems_contact_relation_wizard__lastname model:ir.model.fields,field_description:ems.field_ems_grade_outcome_line__student_lastname code:addons/ems/static/src/js/backend/em_matrix_field.js:0 +#: code:addons/ems/models/contacts/google_workspace_integration.py:0 model:ir.model.fields,field_description:ems.field_ems_contact_relation_wizard__lastname model:ir.model.fields,field_description:ems.field_ems_grade_outcome_line__student_lastname model:ir.model.fields,field_description:partner_firstname.field_res_partner__lastname model:ir.model.fields,field_description:partner_firstname.field_res_users__lastname code:addons/ems/static/src/js/backend/em_matrix_field.js:0 msgid "Last name" -msgstr "Apellido" +msgstr "Apellidos" #. module: ems #: model:ir.model.fields,field_description:ems.field_ems_criteria__outcome_id model:ir.model.fields,field_description:ems.field_ems_planning_outcome__valid_outcome_ids model:ir.model.fields,field_description:ems.field_ems_subject__outcome_ids model_terms:ir.ui.view,arch_db:ems.view_subject_form @@ -4998,6 +5009,18 @@ msgstr "No se ha encontrado planificación para el estudio y la asignatura de es msgid "No recipients to send to. Add recipients in the 'Sending status' section." msgstr "" +#. module: ems +#. odoo-python +#: code:addons/ems/models/communications/notice.py:0 +msgid "Students excluded" +msgstr "Alumnos excluidos" + +#. module: ems +#. odoo-python +#: code:addons/ems/models/communications/notice.py:0 +msgid "The following students have no email address matching your 'Recipient email' choice, so they were not added to the recipient list:\n%s" +msgstr "Los siguientes alumnos no tienen ninguna dirección de correo que coincida con tu selección de 'Correo del destinatario', por lo que no se han añadido a la lista de destinatarios:\n%s" + #. module: ems #. odoo-javascript #: code:addons/ems/static/src/xml/backend/grade_matrix_field.xml:0 @@ -5349,6 +5372,12 @@ msgstr "Gestión de evaluaciones" msgid "You do not tutor any group with an open evaluation." msgstr "No eres tutor de ningún grupo con evaluación abierta" +#. module: ems +#. odoo-python +#: code:addons/ems/models/contacts/contact_relation.py:0 +msgid "You are not allowed to manage this student's family contacts." +msgstr "No tienes permiso para gestionar los contactos familiares de este alumno." + #. module: ems #. odoo-javascript #: code:addons/ems/static/src/xml/backend/grade_tutor_matrix.xml:0 @@ -5449,7 +5478,7 @@ msgid "Payment plan" msgstr "Plan de pago" #. module: ems -#: model:ir.model.fields,field_description:ems.field_ems_attendance_issue_status__pending model:ir.model.fields.selection,name:ems.selection__ems_authorization__status__pending model:ir.model.fields.selection,name:ems.selection__ems_limesurvey_recipient__state__pending model:ir.model.fields.selection,name:ems.selection__ems_notice_line__display_status__pending model_terms:ir.ui.view,arch_db:ems.portal_enrollment_process model_terms:ir.ui.view,arch_db:ems.view_student_document_search +#: model:ir.model.fields,field_description:ems.field_ems_attendance_issue_status__pending model:ir.model.fields.selection,name:ems.selection__ems_attendance_correction__state__pending model:ir.model.fields.selection,name:ems.selection__ems_authorization__status__pending model:ir.model.fields.selection,name:ems.selection__ems_limesurvey_recipient__state__pending model:ir.model.fields.selection,name:ems.selection__ems_notice_line__display_status__pending model_terms:ir.ui.view,arch_db:ems.portal_enrollment_process model_terms:ir.ui.view,arch_db:ems.view_attendance_correction_search model_terms:ir.ui.view,arch_db:ems.view_student_document_search msgid "Pending" msgstr "Pendiente" @@ -5487,7 +5516,7 @@ msgstr "Modificación de datos personales del alumno %s" #. module: ems #. odoo-python -#: code:addons/ems/models/contacts/google_workspace_integration.py:0 +#: code:addons/ems/models/contacts/google_workspace_integration.py:0 model_terms:ir.ui.view,arch_db:ems.view_contact_form msgid "Personal email" msgstr "Correo personal" @@ -5859,7 +5888,7 @@ msgid "Reject" msgstr "Rechazar" #. module: ems -#: model:ir.model.fields.selection,name:ems.selection__ems_authorization__status__no model:ir.model.fields.selection,name:ems.selection__ems_student_document__status__rejected model_terms:ir.ui.view,arch_db:ems.portal_enrollment_process model_terms:ir.ui.view,arch_db:ems.view_student_document_search +#: model:ir.model.fields.selection,name:ems.selection__ems_attendance_correction__state__rejected model:ir.model.fields.selection,name:ems.selection__ems_authorization__status__no model:ir.model.fields.selection,name:ems.selection__ems_student_document__status__rejected model_terms:ir.ui.view,arch_db:ems.portal_enrollment_process model_terms:ir.ui.view,arch_db:ems.view_attendance_correction_search model_terms:ir.ui.view,arch_db:ems.view_student_document_search msgid "Rejected" msgstr "Rechazado" @@ -6273,6 +6302,21 @@ msgstr "Secretaría" msgid "Secretariat Email" msgstr "Correo de secretaría" +#. module: ems +#: model:ir.model.fields,field_description:ems.field_res_company__notice_email_signature model:ir.model.fields,field_description:ems.field_res_config_settings__notice_email_signature +msgid "Notice email signature" +msgstr "Firma de los correos de los comunicados" + +#. module: ems +#: model:ir.model.fields,help:ems.field_res_company__notice_email_signature model_terms:ir.ui.view,arch_db:ems.res_config_settings_view_form +msgid "Default sign-off appended to every Notice email. Copied onto each new notice as its own editable signature - editing it here only affects notices created afterward." +msgstr "Despedida por defecto que se añade a todos los correos de los comunicados. Se copia en cada comunicado nuevo como firma propia editable - modificarla aquí solo afecta a los comunicados creados a partir de ahora." + +#. module: ems +#: model:ir.model.fields,field_description:ems.field_ems_notice__signature +msgid "Signature" +msgstr "Firma" + #. module: ems #: model:ems.role,name:ems.role_secretary model:hr.job,name:ems.job_secretary model:ir.model.fields.selection,name:ems.selection__hr_department__top_level_role__secretary model:project.project,name:ems.project_secretary model:res.groups,name:ems.group_secretary model_terms:ir.ui.view,arch_db:ems.view_contact_form msgid "Secretary" @@ -6366,6 +6410,21 @@ msgstr "Enviar recordatorios" msgid "Send to" msgstr "Enviar a" +#. module: ems +#: model:ir.model.fields,field_description:ems.field_ems_notice__recipient_email_type +msgid "Recipient email" +msgstr "Correo del destinatario" + +#. module: ems +#: model:ir.model.fields.selection,name:ems.selection__ems_notice__recipient_email_type__corporate +msgid "Corporate" +msgstr "Corporativo" + +#. module: ems +#: model:ir.model.fields.selection,name:ems.selection__ems_notice__recipient_email_type__personal +msgid "Personal" +msgstr "Personal" + #. module: ems #: model_terms:ir.ui.view,arch_db:ems.portal_enrollment_process msgid "Send your comments to the secretary's office" @@ -6458,7 +6517,7 @@ msgid "Short Text" msgstr "Texto corto" #. module: ems -#: model_terms:ir.ui.view,arch_db:ems.view_attendance_justification_search model_terms:ir.ui.view,arch_db:ems.view_attendance_session_search model_terms:ir.ui.view,arch_db:ems.view_attendance_template_search +#: model_terms:ir.ui.view,arch_db:ems.view_attendance_justification_search model_terms:ir.ui.view,arch_db:ems.view_attendance_session_search model_terms:ir.ui.view,arch_db:ems.view_attendance_template_search model_terms:ir.ui.view,arch_db:ems.view_notice_search model_terms:ir.ui.view,arch_db:ems.view_limesurvey_header_search msgid "Show only mine" msgstr "Mostrar solamente los míos" @@ -6629,7 +6688,7 @@ msgid "State / Province" msgstr "Estado / Provincia" #. module: ems -#: model:ir.model.fields,field_description:ems.field_ems_attendance_issue_tutor__status model:ir.model.fields,field_description:ems.field_ems_attendance_session_line__status_id model:ir.model.fields,field_description:ems.field_ems_authorization__status model:ir.model.fields,field_description:ems.field_ems_notice_line__display_status model:ir.model.fields,field_description:ems.field_ems_student_document__status model_terms:ir.ui.view,arch_db:ems.attendance_report_session model_terms:ir.ui.view,arch_db:ems.portal_documentation model_terms:ir.ui.view,arch_db:ems.portal_enrollment_process model_terms:ir.ui.view,arch_db:ems.view_student_document_search model_terms:ir.ui.view,arch_db:ems.view_attendance_report_analysis_search +#: model:ir.model.fields,field_description:ems.field_ems_attendance_issue_tutor__status model:ir.model.fields,field_description:ems.field_ems_attendance_session_line__status_id model:ir.model.fields,field_description:ems.field_ems_authorization__status model:ir.model.fields,field_description:ems.field_ems_notice_line__display_status model:ir.model.fields,field_description:ems.field_ems_student_document__status model_terms:ir.ui.view,arch_db:ems.attendance_report_session model_terms:ir.ui.view,arch_db:ems.portal_documentation model_terms:ir.ui.view,arch_db:ems.portal_enrollment_process model_terms:ir.ui.view,arch_db:ems.view_attendance_correction_search model_terms:ir.ui.view,arch_db:ems.view_student_document_search model_terms:ir.ui.view,arch_db:ems.view_attendance_report_analysis_search msgid "Status" msgstr "Estado" diff --git a/mails/communications/communication.xml b/mails/communications/communication.xml index e020e444..427bbafd 100644 --- a/mails/communications/communication.xml +++ b/mails/communications/communication.xml @@ -6,15 +6,13 @@ {{object.notice_id.subject}} ems@elpuig.xeill.net + {{object.notice_id.sent_by.email or object.notice_id.create_uid.email}} -

- Kind regards,
- -

+ ]]>
diff --git a/migrations/18.0.0.23.3/post-migrate.py b/migrations/18.0.0.23.3/post-migrate.py new file mode 100644 index 00000000..adbd9bc0 --- /dev/null +++ b/migrations/18.0.0.23.3/post-migrate.py @@ -0,0 +1,45 @@ +# -*- coding: utf-8 -*- +import logging + +from psycopg2.extras import Json + +from odoo import SUPERUSER_ID, api + +_logger = logging.getLogger(__name__) + + +def _seed_notice_email_signature_default(env): + """res.company.notice_email_signature (new in this version, Html, translate=True) + replaces what used to be a hardcoded 'Kind regards,
{company name}' baked into + ems.mail_notice's own body_html - seed every company with the exact same text, in all + 3 shipped languages, so existing notices keep looking the same as before this became + editable. Same logic as __init__.py's post_init_hook counterpart + (_seed_notice_email_signature_default), duplicated here per this repo's migration + convention (each migration script is self-contained, not cross-imported from the + module's own __init__.py). + + Uses a direct SQL jsonb write, not record.update_field_translations(): fields.Html sets + `translate` to the html_translate *function*, not the literal `True`, so the ORM's + multi-lang API takes the term-by-term "translate existing content" code path (expects + {lang: {old_term: new_term}} and requires a pre-existing value to diff against) instead + of "set the whole value" - it silently returns False and writes nothing on a still-empty + field like this one. Confirmed empirically (logged the return value + a read-back before + switching to this approach).""" + companies = env['res.company'].search([('notice_email_signature', '=', False)]) + for company in companies: + env.cr.execute( + "UPDATE res_company SET notice_email_signature = %s WHERE id = %s", + (Json({ + 'en_US': f"Kind regards,
{company.name}", + 'ca_ES': f"Salutacions cordials,
{company.name}", + 'es_ES': f"Saludos cordiales,
{company.name}", + }), company.id), + ) + _logger.info( + "Migration 18.0.0.23.3: seeded notice_email_signature (3 languages) for %d " + "compan(y/ies).", len(companies)) + + +def migrate(cr, _version): + env = api.Environment(cr, SUPERUSER_ID, {}) + _seed_notice_email_signature_default(env) diff --git a/models/communications/notice.py b/models/communications/notice.py index fcb83700..64a17656 100644 --- a/models/communications/notice.py +++ b/models/communications/notice.py @@ -17,9 +17,19 @@ class EmsNotice(models.Model): required=True, default='both', ) + recipient_email_type = fields.Selection( + string="Recipient email", + selection=[('corporate', 'Corporate'), ('personal', 'Personal'), ('both', 'Both')], + required=True, + default='both', + ) use_schedule = fields.Boolean(string="Schedule sending", default=False) scheduled_date = fields.Datetime(string="Scheduled on") message = fields.Html(string="Message", required=True, sanitize=True) + signature = fields.Html( + string="Signature", + default=lambda self: self.env.company.notice_email_signature, + ) state = fields.Selection( string="State", selection=[('draft', 'Draft'), ('scheduled', 'Scheduled'), ('sent', 'Sent'), ('failed', 'Failed')], @@ -77,22 +87,41 @@ def _compute_display_name(self): for notice in self: notice.display_name = notice.subject or _("(New notice)") - def _build_auto_lines(self, groups, recipient_type, seen_emails): - """Return a list of ems.notice.line virtual records for the given groups.""" + def _student_emails(self, student, recipient_email_type): + """Return the list of candidate email addresses for a student, per recipient_email_type.""" + emails = [] + if recipient_email_type in ('corporate', 'both') and student.student_email: + emails.append(student.student_email) + if recipient_email_type in ('personal', 'both') and student.email: + emails.append(student.email) + return emails + + def _build_auto_lines(self, groups, recipient_type, recipient_email_type, seen_emails): + """Return (new_lines, skipped_student_names) for the given groups. + + skipped_student_names lists students who ended up with no candidate address at + all for the current recipient_email_type (e.g. 'corporate' selected but the + student has no student_email yet) - not students who simply got fewer lines + than 'both' would have produced. + """ new_lines = self.env['ems.notice.line'] + skipped_student_names = [] for group in groups: for student in group.main_student_ids.filtered(lambda s: s.contact_type == 'student'): if recipient_type in ('students', 'both'): - email = student.student_email or student.email - if email and email not in seen_emails: - seen_emails.add(email) - new_lines |= self.env['ems.notice.line'].new({ - 'partner_id': student.id, - 'email': email, - 'student_id': student.id, - 'recipient_type': 'student', - 'source_group_id': group.id, - }) + student_emails = self._student_emails(student, recipient_email_type) + if not student_emails: + skipped_student_names.append(student.name) + for email in student_emails: + if email not in seen_emails: + seen_emails.add(email) + new_lines |= self.env['ems.notice.line'].new({ + 'partner_id': student.id, + 'email': email, + 'student_id': student.id, + 'recipient_type': 'student', + 'source_group_id': group.id, + }) if recipient_type in ('families', 'both'): if not student.is_adult or student.auth_share: @@ -108,15 +137,35 @@ def _build_auto_lines(self, groups, recipient_type, seen_emails): 'recipient_type': 'family', 'source_group_id': group.id, }) - return new_lines + return new_lines, skipped_student_names - @api.onchange('group_ids', 'recipient_type') + @api.onchange('group_ids', 'recipient_type', 'recipient_email_type') def _onchange_groups(self): # Manual lines: those not linked to any auto-populated group manual_lines = self.notice_line_ids.filtered(lambda l: not l.source_group_id) seen_emails = set(manual_lines.mapped('email')) - new_auto_lines = self._build_auto_lines(self.group_ids, self.recipient_type, seen_emails) + new_auto_lines, skipped_student_names = self._build_auto_lines( + self.group_ids, self.recipient_type, self.recipient_email_type, seen_emails + ) self.notice_line_ids = manual_lines | new_auto_lines + if skipped_student_names: + return {'warning': { + 'title': _("Students excluded"), + 'message': _( + "The following students have no email address matching your " + "'Recipient email' choice, so they were not added to the recipient " + "list:\n%s" + ) % "\n".join(skipped_student_names), + }} + + def unlink(self): + sent = self.filtered(lambda notice: notice.state != 'draft') + if sent: + raise UserError(_( + "Cannot delete a notice that has already been scheduled, sent or failed to " + "send. Please archive it instead." + )) + return super().unlink() def action_send(self): self.ensure_one() diff --git a/models/contacts/contact_relation.py b/models/contacts/contact_relation.py index d714682b..62aeabf2 100644 --- a/models/contacts/contact_relation.py +++ b/models/contacts/contact_relation.py @@ -1,5 +1,5 @@ from odoo import _, api, fields, models -from odoo.exceptions import ValidationError +from odoo.exceptions import AccessError, ValidationError class ems_partner_relation_all(models.AbstractModel): _inherit = 'res.partner.relation.all' @@ -44,20 +44,27 @@ def _onchange_student_id(self): self.country_id = self.student_id.country_id def action_save(self): + # res.partner and res.partner.relation are locked down at the ir.model.access/ + # ir.rule level for teachers (see security/ir.model.access.csv and + # security/rules/contacts.xml) - a tutor has no create rights on either. This + # wizard is the controlled entry point that's allowed to bypass that, but only + # for a student the current user is actually authorized to manage: the same + # check that drives the "Add contact" button's own visibility + # (views/community/contact/form.xml), so both stay in sync automatically. + if self.student_id._get_read_only_user(): + raise AccessError(_("You are not allowed to manage this student's family contacts.")) + if not self.type_selection_id: raise ValidationError(_("Please select a relation type.")) if not self.partner_id and not (self.firstname or self.lastname): raise ValidationError(_("Please select an existing contact or enter a first/last name for the new one.")) - if not self.partner_id: - if not (self.document_id or self.passport_id): - raise ValidationError(_("Please provide at least one identification document (DNI/NIE or Passport).")) - if not (self.phone or self.mobile or self.email): - raise ValidationError(_("Please provide at least one contact method (phone, mobile or email).")) + if not self.partner_id and not (self.phone or self.mobile or self.email): + raise ValidationError(_("Please provide at least one contact method (phone, mobile or email).")) if self.partner_id: partner = self.partner_id else: - partner = self.env['res.partner'].create({ + partner = self.env['res.partner'].sudo().create({ 'firstname': self.firstname, 'lastname': self.lastname, 'phone': self.phone, @@ -74,7 +81,7 @@ def action_save(self): 'country_id': self.country_id.id, }) - self.env['res.partner.relation'].create({ + self.env['res.partner.relation'].sudo().create({ 'left_partner_id': partner.id, 'type_id': self.type_selection_id.id, 'right_partner_id': self.student_id.id, diff --git a/models/settings/company.py b/models/settings/company.py index 5b070b53..f54f30a5 100644 --- a/models/settings/company.py +++ b/models/settings/company.py @@ -69,6 +69,12 @@ class ems_company(models.Model): secretariat_email = fields.Char() + notice_email_signature = fields.Html( + string="Notice email signature", translate=True, + help="Default sign-off appended to every Notice email. Copied onto each new notice " + "as its own editable signature - editing it here only affects notices created " + "afterward.") + # Official Departament d'Educació center code (e.g. '8028047'). Used by the GEDAC # applicant import to keep only the rows assigned to this center. center_code = fields.Char(string="Center code") diff --git a/models/settings/settings.py b/models/settings/settings.py index 54897119..65c8567e 100755 --- a/models/settings/settings.py +++ b/models/settings/settings.py @@ -27,6 +27,7 @@ class ems_settings(models.TransientModel): secretariat_email = fields.Char(related="company_id.secretariat_email", readonly=False) center_code = fields.Char(related="company_id.center_code", readonly=False) + notice_email_signature = fields.Html(related="company_id.notice_email_signature", readonly=False) limesurvey_api = fields.Char(related="company_id.limesurvey_api", readonly=False) limesurvey_usr = fields.Char(related="company_id.limesurvey_usr", readonly=False) diff --git a/plans/curriculum_unique_code_duplicate_action.md b/plans/curriculum_unique_code_duplicate_action.md deleted file mode 100644 index cd589ce8..00000000 --- a/plans/curriculum_unique_code_duplicate_action.md +++ /dev/null @@ -1,43 +0,0 @@ -Status: not started - found while resolving a merge conflict (2026-08-01), not yet fixed. - -# Problem - -Six `curriculum` models have a unique `code` `_sql_constraints` entry but no `copy()` override: - -- `models/curriculum/study.py` (`unique_code`) -- `models/curriculum/subject.py` (`unique_code`) -- `models/curriculum/level.py` -- `models/curriculum/content.py` -- `models/curriculum/criteria.py` -- `models/curriculum/outcome.py` - -None of their form/list views set `duplicate="false"`, so Odoo's standard "Duplicate" action -(Action menu on the form, or the list's context menu) is exposed for all of them - and clicking -it raises a raw `psycopg2.errors.UniqueViolation` (`duplicate key value violates unique -constraint "..._unique_code"`) instead of either working correctly or failing with a clear -message, since `copy()`'s default behavior copies `code` verbatim. - -# How this was found - -Not found by deliberately auditing these models - surfaced while merging in -`353-add-course-transition-wizard-setup-next-course` (Juan's branch): his new -`test_transition_state_is_not_copied` (`tests/test_course_transition.py`) calls -`self.study.copy()` (to check `transition_state` resets, unrelated to `code`) and hit this exact -constraint, because `ems.study.unique_code` was added on this branch after Juan's branch had -already diverged - the two changes never collided until the merge. Fixed *for that one test* by -passing an explicit `code` override in the `copy()` call (not a model fix) - see the git history -of `tests/test_course_transition.py` around that date for the exact change. - -# Not yet done - -Decide and implement, for all six models above, either: -- a `copy()` override that generates a genuinely unique `code` (e.g. appending a counter/suffix, - same idea as Odoo's own default `name` "(copy)" suffix behavior), so "Duplicate" actually works, - or -- explicitly disabling duplication (`duplicate="false"` on the relevant views, matching the - pattern already used by `ems.attendance_session.copy()` which raises a friendly `UserError` - instead) if duplicating one of these doesn't actually make sense for the model in question. - -Worth checking, per model, whether an admin ever has a real reason to duplicate one (a new study -that's mostly the same as an existing one might be a real use case; duplicating a `criteria`/ -`outcome` row is less obviously useful) rather than applying the same fix uniformly to all six. diff --git a/plans/family_contacts_missing_firstname.md b/plans/family_contacts_missing_firstname.md new file mode 100644 index 00000000..e712944d --- /dev/null +++ b/plans/family_contacts_missing_firstname.md @@ -0,0 +1,50 @@ +Status: not started - found while investigating point 2.3 of a developer bug report on the +"Add contact" wizard (2026-09-05). Developer decided to leave it as-is for now (asked, chose +"Dejarlo como está por ahora" over migrating). Kept here in case it's revisited later. + +# Problem + +347 of 1168 `res.partner` records with `contact_type='family'` (~30%) in this dev DB have no +`firstname` at all - only a single word stored in `lastname`, e.g. "Rafael", "Mohamed", +"Najoua", "Songbin", "Antonio" - values that read as first names, not surnames, across several +cultures. `firstname IS NULL/'' AND lastname` single word, confirmed via: + +```sql +SELECT id, name, firstname, lastname, is_company FROM res_partner +WHERE contact_type='family' AND (firstname IS NULL OR firstname='') + AND lastname IS NOT NULL AND lastname != ''; +``` + +This is why the "Existing contact" search in `ems.contact.relation.wizard` (the "Add contact" +button on a student's form) often shows only one word instead of "firstname lastname" for these +contacts - `display_name` is correctly showing exactly what's stored; there's no more data to +show. Not a wizard bug, not a search/display bug. + +# Root cause of *why* it lands in `lastname` specifically + +`partner_firstname`'s own name-splitting heuristic +(`FirstNameMixin._get_inverse_name` in `partner-contact/partner_firstname/models/firstname_mixin.py`) +pads a single-word `name` value as `[word, False]` and returns +`{"lastname": parts[0], "firstname": parts[1]}` - i.e. whenever only one word is known, it +always lands in `lastname`, never `firstname`. This is upstream OCA behavior, not something to +patch in EMS. + +# Possible fix (not applied) + +A one-off migration script moving `lastname` → `firstname` for exactly these 347 records +(`UPDATE res_partner SET firstname = lastname, lastname = NULL WHERE contact_type='family' AND +(firstname IS NULL OR firstname='') AND lastname IS NOT NULL AND lastname != ''`, or the ORM +equivalent so `name`/`display_name` recompute correctly) - **not run**, since: + +- It's a judgment call on 347 real people's personal data, not something to infer purely from a + column heuristic (a family surname genuinely could be a single word too, in principle). +- The developer was asked directly (2026-09-05) and chose to leave it as-is for now rather than + migrate. + +# How to revisit + +If asked again later: re-run the query above to get the current count/sample (the number may +have grown or shrunk since 2026-09-05 as new contacts are added or existing ones corrected by +staff), then re-propose the migration with a fresh sample for confirmation before touching any +real record. Delete this file once resolved either way (migrated, or the developer decides +permanently not to) - see CLAUDE.md's "Design plans" section. diff --git a/plans/test_guard_sessions_returns_other_teachers_sessions_flake.md b/plans/test_guard_sessions_returns_other_teachers_sessions_flake.md deleted file mode 100644 index c0d8b123..00000000 --- a/plans/test_guard_sessions_returns_other_teachers_sessions_flake.md +++ /dev/null @@ -1,54 +0,0 @@ -# `test_guard_sessions_returns_other_teachers_sessions` full-suite flake - -**Status: current as of 2026-09-02.** Not yet investigated in depth — found incidentally -while running the full, unscoped `./test.sh` as the final gate for an unrelated branch -(386-strike-warn-the-family-only-when-kicked-out). Unrelated to that branch's own changes -(strike/company/settings files only) — this is a pre-existing gap in a different model's -test. - -## What happened - -`tests/test_attendance_session.py::TestAttendanceSessionHeader.test_guard_sessions_returns_other_teachers_sessions` -failed with `AssertionError: 2 != 1` when running the full `./test.sh` (all classes, one -shard, `ems_shard_fast`), but is presumably fine when run scoped/in isolation (not directly -re-verified here, but this class is not in any known flaky-test list). - -## Root cause (traced from the code, not yet from a reproduction) - -`ems.attendance_session_header.get_guard_sessions(date)` -(`models/attendance/attendance_session.py:451`) queries **every** session in the database -for the given date, excluding only sessions owned by the calling user's own employee — with -no scoping to a specific schedule, company, or test fixture: - -```python -domain = [['date', '=', date]] -if own_emp: - domain += ['!', '|', ['template_teacher_ids', 'in', own_emp.id], ['session_teacher_id', '=', own_emp.id]] -``` - -The test creates exactly one session dated `date.today()` and asserts `get_guard_sessions` -returns exactly 1 — but this only holds if no *other* test class running earlier in the same -shard/DB has also left behind (or is concurrently holding open, depending on transaction -timing) a session dated today that isn't owned by `other_teacher_user`'s employee. Any other -test creating an `ems.attendance_session_header` with today's date (there are several across -the suite — attendance sessions are dated `date.today()` by convention in many test fixtures) -is a candidate. `TransactionCase` rolls back each test method's own writes, so the leak would -have to come from `setUpClass`-level data in some other class that's still live during this -test's run (class execution order within a shard is deterministic per run but not obviously -tied to this test file). - -## Not yet done - -- Identify which other test class's `setUpClass` (or similar) leaves a `date.today()` session - around during this test's execution — bisect by running `./test.sh` with an increasing - subset of classes, or add a temporary print of the extra session's id/teacher in - `get_guard_sessions` during a local repro. -- Decide the fix: either scope the test's own query more precisely (assert on the specific - session id created, not just `len(result) == 1`), or scope `get_guard_sessions` itself if - the "any session today, globally" domain turns out to be wrong for production too (unlikely - — its own comment says "Guard teachers need to see all sessions for the day regardless of - ownership" is deliberate; the test assertion is the more likely thing to fix). - -## Next step - -Investigate and fix in a dedicated branch/task — not scoped to 386's strike work. diff --git a/security/ir.model.access.csv b/security/ir.model.access.csv index 71922189..a73d4d2d 100755 --- a/security/ir.model.access.csv +++ b/security/ir.model.access.csv @@ -249,6 +249,10 @@ ems.access_ems_limesurvey_block_quality,ems.access_ems_limesurvey_block_quality, ems.access_ems_limesurvey_header_quality,ems.access_ems_limesurvey_header_quality,ems.model_ems_limesurvey_header,ems.group_quality,1,1,1,0 ems.access_ems_limesurvey_enrollment_quality,ems.access_ems_limesurvey_enrollment_quality,ems.model_ems_limesurvey_enrollment,ems.group_quality,1,1,1,0 ems.access_ems_limesurvey_recipient_quality,ems.access_ems_limesurvey_recipient_quality,ems.model_ems_limesurvey_recipient,ems.group_quality,1,1,1,0 +ems.access_ems_limesurvey_block_admin,ems.access_ems_limesurvey_block_admin,ems.model_ems_limesurvey_block,ems.group_academic_admin,1,1,1,1 +ems.access_ems_limesurvey_header_admin,ems.access_ems_limesurvey_header_admin,ems.model_ems_limesurvey_header,ems.group_academic_admin,1,1,1,1 +ems.access_ems_limesurvey_enrollment_admin,ems.access_ems_limesurvey_enrollment_admin,ems.model_ems_limesurvey_enrollment,ems.group_academic_admin,1,1,1,1 +ems.access_ems_limesurvey_recipient_admin,ems.access_ems_limesurvey_recipient_admin,ems.model_ems_limesurvey_recipient,ems.group_academic_admin,1,1,1,1 ems.access_ems_contact_relation_wizard_admin,ems.access_ems_contact_relation_wizard_admin,ems.model_ems_contact_relation_wizard,ems.group_academic_admin,1,1,1,1 ems.access_ems_contact_relation_wizard_teacher,ems.access_ems_contact_relation_wizard_teacher,ems.model_ems_contact_relation_wizard,ems.group_teacher,1,1,1,1 ems.access_ems_student_import_wizard_admin,ems.access_ems_student_import_wizard_admin,ems.model_ems_student_import_wizard,ems.group_academic_admin,1,1,1,1 @@ -266,6 +270,10 @@ ems.access_ems_enrollment_proposal_wizard_secretary,ems.access_ems_enrollment_pr ems.access_ems_enrollment_proposal_wizard_tutor,ems.access_ems_enrollment_proposal_wizard_tutor,ems.model_ems_enrollment_proposal_wizard,ems.group_tutor,1,1,1,1 ems.access_ems_notice_admin,ems.access_ems_notice_admin,ems.model_ems_notice,ems.group_academic_admin,1,1,1,1 ems.access_ems_notice_line_admin,ems.access_ems_notice_line_admin,ems.model_ems_notice_line,ems.group_academic_admin,1,1,1,1 +ems.access_ems_notice_hos,ems.access_ems_notice_hos,ems.model_ems_notice,ems.group_head_of_studies,1,1,1,1 +ems.access_ems_notice_line_hos,ems.access_ems_notice_line_hos,ems.model_ems_notice_line,ems.group_head_of_studies,1,1,1,1 +ems.access_ems_notice_quality_admin,ems.access_ems_notice_quality_admin,ems.model_ems_notice,ems.group_quality_admin,1,1,1,1 +ems.access_ems_notice_line_quality_admin,ems.access_ems_notice_line_quality_admin,ems.model_ems_notice_line,ems.group_quality_admin,1,1,1,1 ems.access_ems_portal_access_wizard_admin,ems.access_ems_portal_access_wizard_admin,ems.model_ems_portal_access_wizard,ems.group_academic_admin,1,1,1,1 ems.access_ems_portal_access_wizard_secretary,ems.access_ems_portal_access_wizard_secretary,ems.model_ems_portal_access_wizard,ems.group_secretary,1,1,1,1 ems.access_ems_portal_access_wizard_tutor,ems.access_ems_portal_access_wizard_tutor,ems.model_ems_portal_access_wizard,ems.group_tutor,1,1,1,1 diff --git a/security/rules/communications.xml b/security/rules/communications.xml index 77316056..0e7cb2c8 100644 --- a/security/rules/communications.xml +++ b/security/rules/communications.xml @@ -2,11 +2,16 @@ - + + + - Notice: admin sees all + Notice: admin/director see all - + [(1, '=', 1)] @@ -14,16 +19,228 @@ - + + + Notice: HOS/quality coordinator see all (read only) + + + [] + + + + + + + - Notice: user sees own + Notice: user edits own + [('create_uid', '=', user.id)] + + + + + + + + + Notice line: admin/director see all + + + [(1, '=', 1)] + + + + + + + + Notice line: HOS/quality coordinator see all (read only) + + + [] + + + + + + + + Notice line: user edits own (via parent notice) + + + [('notice_id.create_uid', '=', user.id)] + + + + + + + + + + Survey: admin sees all + + + [(1, '=', 1)] + + Survey: quality coordinator sees all (read only) + + + [] + + + + + + + + Survey: quality coordinator edits own + + + [('create_uid', '=', user.id)] + + + + + + + + Survey block: admin sees all + + + [(1, '=', 1)] + + + + + + + + Survey block: quality coordinator sees all (read only) + + + [] + + + + + + + + Survey block: quality coordinator edits own + + + [('create_uid', '=', user.id)] + + + + + + + + Survey recipient: admin sees all + + + [(1, '=', 1)] + + + + + + + + Survey recipient: quality coordinator sees all (read only) + + + [] + + + + + + + + Survey recipient: quality coordinator edits own + + + [('create_uid', '=', user.id)] + + + + + + + + Survey enrollment: admin sees all + + + [(1, '=', 1)] + + + + + + + + Survey enrollment: quality coordinator sees all (read only) + + + [] + + + + + + + + Survey enrollment: quality coordinator edits own + + + [('create_uid', '=', user.id)] + + + + + + diff --git a/static/tests/tours/attendance_correction_tour.js b/static/tests/tours/attendance_correction_tour.js index 8e36e68a..4aac3d62 100644 --- a/static/tests/tours/attendance_correction_tour.js +++ b/static/tests/tours/attendance_correction_tour.js @@ -43,3 +43,68 @@ registry.category("web_tour.tours").add("ems_attendance_correction_accept", { }, ], }); + +// ems.attendance_correction search view: the list defaults to showing only Pending +// requests (action context search_default_pending), matching the same pattern already +// used by ems.student.document — an approver reviewing this list should not have to +// wade through already-decided requests by default. This tour seeds one request per +// state and confirms only the pending one shows by default, that removing the default +// filter reveals all three, and that the Accepted filter can be used on its own to see +// only accepted requests. +registry.category("web_tour.tours").add("ems_attendance_correction_pending_filter", { + test: true, + url: "/odoo/action-ems.action_attendance_correction_tree", + steps: () => [ + { + trigger: ".o_searchview_facet:contains('Pending')", + content: "Pending filter applied by default", + }, + { + trigger: ".o_list_view .o_data_row td:contains('Filter Tour Teacher Pending')", + content: "The pending request is shown by default", + }, + { + trigger: ".o_list_view:not(:has(.o_data_row td:contains('Filter Tour Teacher Accepted')))", + content: "The accepted request is hidden under the default Pending filter", + }, + { + trigger: ".o_list_view:not(:has(.o_data_row td:contains('Filter Tour Teacher Rejected')))", + content: "The rejected request is hidden under the default Pending filter", + }, + { + trigger: ".o_searchview_facet:contains('Pending') .o_facet_remove", + content: "Remove the default Pending filter", + run: "click", + }, + { + trigger: ".o_list_view .o_data_row td:contains('Filter Tour Teacher Accepted')", + content: "All requests are shown once the default filter is removed", + }, + { + trigger: ".o_list_view .o_data_row td:contains('Filter Tour Teacher Rejected')", + content: "The rejected request is also shown", + }, + { + trigger: ".o_searchview_dropdown_toggler", + content: "Open the search dropdown", + run: "click", + }, + { + trigger: ".o_filter_menu .o_menu_item:contains('Accepted')", + content: "Enable the Accepted filter", + run: "click", + }, + { + trigger: ".o_list_view .o_data_row td:contains('Filter Tour Teacher Accepted')", + content: "Only the accepted request is shown", + }, + { + trigger: ".o_list_view:not(:has(.o_data_row td:contains('Filter Tour Teacher Pending')))", + content: "The pending request is hidden while only the Accepted filter is active", + }, + { + trigger: ".o_list_view:not(:has(.o_data_row td:contains('Filter Tour Teacher Rejected')))", + content: "The rejected request is hidden while only the Accepted filter is active", + }, + ], +}); diff --git a/static/tests/tours/contact_tour.js b/static/tests/tours/contact_tour.js index c17906a2..f671f905 100644 --- a/static/tests/tours/contact_tour.js +++ b/static/tests/tours/contact_tour.js @@ -25,6 +25,18 @@ registry.category("web_tour.tours").add("ems_contact_tabs_and_relation_wizard", content: "Open the seeded student", run: "click", }, + { + trigger: ".o_form_view label:contains('Personal email')", + content: "The generic 'Email' row is relabeled for a student (no ambiguity with the institutional address)", + }, + { + trigger: ".o_form_view label:contains('Corporate email')", + content: "The read-only 'Corporate email' row mirrors student_email", + }, + { + trigger: ".o_form_view .o_field_widget[name='student_email']:contains('contact.tour.student@example.com')", + content: "...and shows the same address stored on the student", + }, { trigger: ".o_form_view .o_notebook .nav-link:contains('Student data')", content: "Open the Student data tab", diff --git a/static/tests/tours/level_tour.js b/static/tests/tours/level_tour.js index d8adeb75..2d798e3d 100644 --- a/static/tests/tours/level_tour.js +++ b/static/tests/tours/level_tour.js @@ -95,6 +95,15 @@ registry.category("web_tour.tours").add("ems_level_crud", { content: "Open action menu", run: "click", }, + { + // ems.level has a unique 'acronym' constraint but no copy() override - the stock + // "Duplicate" action would raise a raw UniqueViolation instead of working or + // failing cleanly, so it's disabled (duplicate="0") until a real copy() is written. + // Matched by icon, not label, for the same language-safety reason as the Delete + // step right below. + trigger: "body:not(:has(.o_menu_item .fa-clone))", + content: "Duplicate is not offered (would crash on the unique acronym constraint)", + }, { // Matched by its icon, not by its label: "Delete" is translated, so the // step failed for any user whose language is not English (the admin of a diff --git a/static/tests/tours/notice_tour.js b/static/tests/tours/notice_tour.js index 36eff163..6da45d4a 100644 --- a/static/tests/tours/notice_tour.js +++ b/static/tests/tours/notice_tour.js @@ -44,9 +44,17 @@ registry.category("web_tour.tours").add("ems_notice_create_and_send", { trigger: ".o_form_view .o_field_widget[name='group_ids'] .o_tag", content: "Group tag added", }, + { + // A plain Selection field renders as a /