Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions .github/workflows/changelog-clean.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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: |
Expand Down Expand Up @@ -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({
Expand Down Expand Up @@ -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 }}';
Expand Down
11 changes: 11 additions & 0 deletions .github/workflows/ci-unit-testing.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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/<pr>/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
Expand Down
10 changes: 5 additions & 5 deletions .github/workflows/deploy-check.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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({
Expand All @@ -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: |
Expand Down Expand Up @@ -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({
Expand All @@ -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({
Expand Down Expand Up @@ -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';
Expand Down
4 changes: 2 additions & 2 deletions .github/workflows/require-changelog-clean.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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({
Expand All @@ -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';
Expand Down
37 changes: 37 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -225,6 +225,35 @@ echo "$(date +%H:%M:%S) EMS: waiting on you — <short summary of what's being a
- Python side: `@tagged('post_install', '-at_install')`, `self.start_tour("/odoo", "tour_name", login="admin")`.
- To watch a tour run in a real browser during development: add `watch=True` to `start_tour`.

**Tour tests and language:** a tour that asserts on literal English text (a `:contains('Send
now')`-style trigger, an `[title='...']` selector matching a translatable label, a status/
selection name typed nowhere by the tour itself) only works if the account driving it actually
renders in English — never assume that's true. `login="admin"` logs in as this box's real,
pre-existing `admin` account, whose language is whatever this dev box happens to have (this
box's is `es_ES`) — **not** guaranteed `en_US`, regardless of environment. A freshly created
`res.users` record is not automatically safe either: without an explicit `'lang'` key, it does
not reliably default to `en_US` on every box (confirmed on this one: it defaults to `ca_ES`).
Found twice already from the exact same root cause (`TestAttendanceStatusTour`,
`TestNoticeTour`) — a tour step timing out after 10s with **no** console error and **no**
Python traceback initially looked like a hang/race condition, but was actually a translated
label silently never matching a hardcoded English selector; the tour's own auto-saved failure
screenshot (`/tmp/odoo_tests/ems/screenshots/`) is what actually revealed it, not the logs.
**How to apply:**
- Logging in as a real pre-existing account (`login="admin"` or similar) and asserting on
translatable text: call `force_user_language_to_english(self, self.env.ref('base.user_admin'))`
(`tests/common.py`) at the start of the test method, before `start_tour(...)` — sets the
account's language to `en_US` for that test only, restored via `addCleanup`.
- Creating a fresh `res.users` fixture for the tour to log in as: just pass `'lang': 'en_US'`
explicitly in its `create()` vals — no restore needed, the record itself is test-scoped.
- Prefer structural/position-based selectors (CSS classes, `nth-child`, data attributes) over
text content wherever the tour doesn't specifically need to assert on a label's value — sidesteps
the whole class of risk, same fix already applied to `attendance_session_tour.js`'s status
selectors.
- A tour that only asserts on strings the tour itself typed in (a fixture subject, a computed
code) is unaffected — the risk is specifically standard Odoo or EMS-module vocabulary
(button labels, status/selection names) that gets translated out from under a hardcoded
English selector.

## Coding standards

Follow the official Odoo v18 coding guidelines:
Expand Down Expand Up @@ -598,6 +627,14 @@ explicar de forma genérica"*):
required by the "read every file" step above), then write fresh, short prose that captures what a
PR reviewer actually needs to know from it.

**No manual line wraps within a paragraph/bullet in the final delivered document — each one
must be a single continuous line in the file** (developer feedback 2026-09-04: *"Este texto de
PR tiene saltos de linea manuales? No debes hacer eso."*). This applies specifically to the
final reassembled/condensed PR document handed to the developer, not to the per-branch
`changelog/<branch>.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
Expand Down
34 changes: 34 additions & 0 deletions __init__.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
# -*- coding: utf-8 -*-

from psycopg2.extras import Json

from . import controllers
from . import models

Expand Down Expand Up @@ -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):
Expand Down Expand Up @@ -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,<br/>{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/<version>/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,<br/>{company.name}",
'ca_ES': f"Salutacions cordials,<br/>{company.name}",
'es_ES': f"Saludos cordiales,<br/>{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
Expand Down
5 changes: 4 additions & 1 deletion __manifest__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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',
Expand Down Expand Up @@ -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',
Expand All @@ -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',
Expand Down
2 changes: 2 additions & 0 deletions docs/ca/admin/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.

---

Expand Down
108 changes: 108 additions & 0 deletions docs/ca/admin/notice.md
Original file line number Diff line number Diff line change
@@ -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)
Loading