Skip to content

[WIP] 18.0 support release - #84

Open
sebastienbeau wants to merge 8 commits into
18.0from
18.0-support-release
Open

sebastienbeau wants to merge 8 commits into
18.0from
18.0-support-release

Conversation

@sebastienbeau

Copy link
Copy Markdown
Member

No description provided.

Design of the release tracking tied to the tickets: two modules (one
per side), the migration contract, the push queue, the stage rules and
the verification results. Validated with the user before coding.
Track the releases of a customer project and link the tickets they
ship, with one module per side.

support_release (customer side) only records what the release script
hands it and relays it: a single entry point
_register_deployment(version, tickets) called from the migration the
pipeline generates, a cron pushing the oldest unsent release, and a
button to flush the queue. No git logic and no network call in the
migration; the entry point never raises, because a failing migration
breaks the module update and therefore the deployment. There is
nothing to configure either: the environment comes from running_env,
so only preprod and prod make the module speak.

support_server_release (our side) adds a POST route on the existing
cross_connect endpoint, creates one release per version, links the
tickets it ships and moves them to the stage configured for the
deployed environment. It deliberately depends on cross_connect_server
and project only: ticket ids arrive in the payload and the partner is
taken from the project of the tickets, so the module carries no GitLab
logic and no gitlab_integration dependency.

The key of a release is its version; before the first numbered version
the key is 'next', which the first numbered version absorbs and
removes. Both models stay minimal: version, tickets and is_sent on the
customer side, version, client, partner, tasks and the two deployment
dates on ours.

Assisted-by: Hermes Agent (Nous Research)
@ak-git-bot ak-git-bot added series:18.0 mod:support_release Module support_release mod:support_server_release Module support_server_release labels Oct 31, 2026

@rob-beau rob-beau left a comment •

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Résumé : 11 points, dont deux bugs de données sur l'absorption de la next (tickets non déplacés, horodatages perdus), un trou d'isolation des tickets par client, et huit points de convention, de robustesse et de test.
Deux points sont arbitrés par le plan et ne sont donc pas commentés : is_sent jamais remis à faux (contrainte 7 et §16), et le silence hors preprod/prod.

Détail

Vérifié en exécution sur un banc jetable (odoo 18.0 + postgres 16, le dépôt de la PR monté tel quel avec --test-enable) : les 28 tests des deux modules passent. Trois tests jetables ont servi à prouver les deux bugs de données (absorption après application des étapes, horodatages perdus à l'absorption) et à vérifier que le chemin « étape absente de project.type_ids » fonctionne (il est correct, il n'est juste pas testé).

Non vérifié, à savoir : le job CI test with OCB est rouge pour une raison préexistante (odoo-addon-gitlab_integration==18.0.* absent de PyPI, tiré par project_customer_access, fichier non touché par la PR). La CI ne prouve donc rien sur ces deux modules, seul le banc le fait.

Les commentaires suivent l'ordre des fichiers.

):
continue
with self.env.cr.savepoint():
if stage not in task.project_id.type_ids:

@rob-beau rob-beau Oct 31, 2026 •

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Dette : le rattachement de l'étape au projet réimplémente un helper du core.
_set_stage_on_project_from_task() fait exactement ce bloc, et la copie figera une règle qui peut évoluer en amont.
Correctif : task.sudo()._set_stage_on_project_from_task() puis le write de stage_id.

Détail
addons/project/models/project_task.py:1074 : le helper écrit project.write({'type_ids': [Command.link(stage_id)]}) pour chaque étape courante absente du projet, soit exactement la ligne 105 de ce module.

Vérifié sur le banc : le code actuel fonctionne (étape rattachée, tâche déplacée), ce n'est pas un bug, seulement de la dette.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Non appliqué, avec la raison : _set_stage_on_project_from_task() rattache au projet l'étape courante du ticket, pas l'étape cible. Appelé avant l'écriture, il rattache l'étape de départ, et le calcul de stage_id peut ensuite remplacer la cible. Le module rattache la cible avec Command.link, exactement le même geste pour la bonne étape ; test_stage_missing_from_project_is_attached couvre le cas.

stage = self._get_stage(environment)
if stage:
release._apply_stages(stage)
if version != NEXT_VERSION:

@rob-beau rob-beau Oct 31, 2026 •

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Bug de données : l'absorption est faite après _apply_stages(), les tickets absorbés ne reçoivent jamais l'étape de l'environnement.
Un ticket arrivé par next reste à l'étape préprod dans une release déployée en prod.
Correctif : appeler _absorb_next_release() juste après release.write(vals).

Détail
Les tickets de next sont ajoutés à task_ids après la boucle d'étapes, donc hors de son périmètre. Reproduit sur le banc : next avec le ticket A reçu en préprod, puis 14.0.1.0 avec le ticket B reçu en prod ; après traitement, B est à l'étape prod et A reste à l'étape préprod, tous les deux dans la même release.

Le flux nominal le masque (le client renvoie l'union des tickets), mais pas une base restaurée, une next locale déjà absorbée, ou un payload écrit à la main.

Comment thread support_server_release/schemas.py Outdated

class ReleasePayload(BaseModel):
environment: Literal["preprod", "prod"]
version: str

@rob-beau rob-beau Oct 31, 2026 •

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Robustesse : version: str accepte la chaîne vide.
{"version": ""} crée une release sans nom et absorbe la next au passage.
Correctif : version: str = Field(min_length=1).

Détail
Le client ne peut pas l'envoyer (la garde de _register_deployment), mais la route répond à toute clé valide, et l'unicité est par client : un émetteur qui envoie une version vide verrouille la clé ("", client) et absorbe la next sans rien y mettre.

@@ -0,0 +1,2 @@
id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
access_support_server_release_user,support.server.release user,model_support_server_release,base.group_user,1,1,1,1

@rob-beau rob-beau Oct 31, 2026 •

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sécurité : base.group_user a create et unlink sur la release.
N'importe quel utilisateur interne peut supprimer l'historique de déploiement et les liens aux tickets.
Correctif : 1,1,0,0.

Détail
Le plan §9 Sécurité prévoit lecture/écriture. Le create est de toute manière inutile (seul _process crée, en sudo()), et unlink ne devrait pas être ouvert : supprimer une release efface preprod_deployed_at et prod_deployed_at, qui sont les seules traces du déploiement côté central.

)
self.assertEqual(release.task_ids, task)
self.assertEqual(release.partner_id, self.partner)
self.assertEqual(foreign.stage_id, self.stage_preprod)

@rob-beau rob-beau Oct 31, 2026 •

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Test : cette assertion ne peut pas échouer.
foreign est créé sur stage_preprod, qui est l'étape cible du payload : elle n'a pas bougé parce qu'elle était déjà là, donc le test passe même si la tâche est traitée comme les autres.
Correctif : créer foreign sur une étape en amont et asserter qu'elle y est restée.

Détail
Seule la ligne 145 (release.task_ids == task) prouve réellement l'exclusion.

À noter aussi : le plan §9 Tests liste « étape absente de project.type_ids : rattachée puis appliquée », et ce test n'existe pas. Le chemin fonctionne (vérifié sur le banc : étape rattachée, tâche déplacée), il mérite son test.

data-key="support_server_release"
groups="base.group_system"
>
<div

@rob-beau rob-beau Oct 31, 2026 •

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Convention : le bloc de réglages garde la structure 16.0.
En 17/18 c'est le template du noeud <app> qui rend le div.app_settings_block, le div ajouté ici le duplique.
Correctif : supprimer le div et écrire deux <setting> dans un <block title="Ticket stages">.

Détail
web/static/src/webclient/settings_form_view/settings/settings_app.xml:4 rend ce div.app_settings_block pour chaque app, donc le data-key se retrouve en double dans le formulaire, alors que le filtre de recherche des réglages travaille sur l'app. Les div.o_setting_box / div.o_setting_right_pane écrits à la main sont remplacés en 18 par des <setting id="..." help="...">, comme dans le core et account_usability.

When a payload says the version reached production, send the mail
template 'Support Release: release in production' to the users of the
client: the portal users the cross connection created, filtered on an
existing email address. Nothing is configured and no field is added:
an empty list simply sends nothing.

The mail lists the title of every ticket of the release, each title
linking to the ticket form through the action, and folds the
description of the ticket under a details/summary element. Apple Mail
and Thunderbird fold it, Gmail and Outlook drop the element and show
the description unfolded, which is the only degradation available in
an email.

The mail is queued (force_send=False) so a broken mail server never
fails the reception of the release, and no Odoo notification layout is
applied: both provided layouts add an unfollow link without a token
and the Odoo branding, which make no sense in a customer
notification. The template is editable in the interface; the module
only renders it.

Assisted-by: Hermes Agent (Nous Research)
Comment on lines +51 to +55
def _normalize_version(self, version):
version = (version or "").strip()
if version.lower() == NEXT_VERSION:
return NEXT_VERSION
return version

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Tu peux simplifier avec un simple version.lower().strip(), pas besoin d'une fonction pour ça

Comment on lines +58 to +65
def _normalize_tickets(self, tickets):
normalized = set()
for ticket in tickets or ():
try:
normalized.add(int(ticket))
except (TypeError, ValueError):
_logger.warning("Ignoring non integer ticket reference %r", ticket)
return sorted(normalized)

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

pas besoin d'autant de complexité, tu peux juste raise une erreur si ce que l'on te passe n'est pas une liste d'ID

if release:
merged = sorted(set(release._get_ticket_ids()) | set(ticket_ids))
if merged != release._get_ticket_ids():
release.tickets = json.dumps(merged)

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

simplify fait simplement un release.tickets = ticket_ids.

A chaque update on renvoie la totalité des tickets

return {
"environment": self._get_environment(),
"version": self.version,
"tickets": self._get_ticket_ids(),

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

un simple self.tickets est suffisant

Comment on lines +69 to +77
def _get_partner(self, tasks):
"""Partner of the projects of the tickets, when there is one."""
partners = tasks.project_id.partner_id.filtered(lambda partner: partner)
if len(partners) > 1:
_logger.warning(
"Tickets belong to several partners %s, keeping %s",
partners.ids,
partners[0].id,
)

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

jamais de choses silencieuse, le mieux c'est de mettre un message dans la release coté server

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Même traitement que pour les tickets inconnus : les tickets écartés sont listés dans le champ message de la release, plus aucun avertissement uniquement dans les logs.

current_stage.id,
):
continue
with self.env.cr.savepoint():

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Réfléchie avant de mettre un savepoint, c'est très rare d'en avoir besoin, ici, ça ne sert à rien

current_stage = task.stage_id
if current_stage and (stage.sequence, stage.id) <= (
current_stage.sequence,
current_stage.id,

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

pas besoin de se prendre la tete avec ce "never back" modifie le plan, on applique la release et on met a jours l'état sans condition

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Appliqué : la comparaison de séquence disparaît, l'étape de l'environnement s'écrit sur chaque ticket sans condition. Plan mis à jour (D9), et le test vérifie maintenant qu'un ticket déjà à l'étape prod revient à l'étape préprod.

if not next_release:
return
if next_release.task_ids:
self.write(

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

trop de complexité pour rien ici, on s'en moque de la next, on la supprime direct pas besoin


self.search(
            [
                ("cross_connect_client_id", "=", self.cross_connect_client_id.id),
                ("version", "=", NEXT_VERSION),
            ]
        ).unlink()

Apply the review of the pull request, the comments of sebastienbeau
first when they contradict the others.

Customer side: drop the two normalisation helpers, the version and
the ticket list are checked inline and raise on a value that cannot be
used; a deployment replaces the tickets of the release instead of
merging them, since each payload carries the complete list; drop the
savepoint and the try/except of the cron, a failing push must show up
in the log and the release is retried; drop the manual push button and
its view; the tickets field becomes a Json, so the payload reads it
directly.

Central side: order on id desc, a Char version compared as text puts
14.0.2610.9.0 above 14.0.2610.26.0; apply the stage of the environment
without comparing it with the current one, the payload describes the
state after the deployment; drop the savepoint and the per-ticket
try/except; delete the pending next release instead of merging its
tickets; split _process into small helpers, it had grown to 45 lines.

Nothing is silent anymore: a new message field on the release carries
the tickets that are unknown, of another partner (a release links no
ticket when its project has no partner), the missing stage and the
missing recipient, instead of a log line nobody reads.

Also from the review: the customer menu was under the Technical menu,
so only visible in debug mode; version is now min_length=1 in the
schema; neither model is creatable nor deletable from the interface;
the settings block uses the 18.0 setting tag; and two tests that could
not fail are fixed, with the case of a stage missing from the project.

Assisted-by: Hermes Agent (Nous Research)
@rob-beau

Copy link
Copy Markdown

Revue traitée en local (commit aa74df0, pas encore poussé sur la branche), en suivant la priorité demandée : les commentaires de sebastienbeau d'abord, les points de rob-beau qu'ils ne contredisent pas ensuite.

Côté client (support_release)

  • _normalize_version et _normalize_tickets supprimés : version strip()ée et next replié en minuscules sur place ; une version vide ou une liste de tickets qui n'est pas une liste d'ids entiers lève un ValueError. Une migration générée cassée doit se voir.
  • release.tickets = ticket_ids sans fusion, chaque déploiement renvoyant la liste complète.
  • tickets passe en Json, d'où "tickets": self.tickets dans le payload et la disparition de _get_ticket_ids().
  • Pas de savepoint dans _register_deployment (un search précède, pas de concurrence à couvrir) et pas de try/except dans la cron : l'exception remonte dans son log, is_sent reste faux et la release est retentée.
  • action_push_pending, son bouton et ses deux tests supprimés : la cron est le seul chemin d'envoi.
  • Le menu sort de base.menu_custom (visible seulement en debug) pour Réglages en base.group_system.

Côté central (support_server_release)

  • _order = "id desc" : version desc comparait du texte, 14.0.2610.9.0 passait au-dessus de 14.0.2610.26.0.
  • L'étape de l'environnement s'applique sans condition, plus de comparaison de séquence : le payload décrit l'état après déploiement. Plan mis à jour (D9).
  • Savepoint et try/except par ticket supprimés.
  • next simplement supprimée, sans reprendre ses tickets : ils sont dans le payload de la version.
  • _process découpée en _get_release, _filter_tickets, _release_vals, _add_message, _apply_stages, _remove_next_release, _notify_prod_release ; elle fait 19 lignes.
  • Plus rien de silencieux : nouveau champ message sur la release, en alerte dans le formulaire et en colonne optionnelle dans la liste, qui reçoit les tickets inconnus, les tickets d'un autre partenaire, l'absence de partenaire, l'absence d'étape configurée et l'absence de destinataire.
  • Isolation : sans partenaire sur les projets, plus aucun ticket n'est lié, et message le dit. La clé identifiait l'émetteur, pas le propriétaire des tickets.
  • version: str = Field(min_length=1), avec le test 422.
  • Droits 1,1,0,0 des deux côtés : seul le code crée une release, et la supprimer effacerait la seule trace du déploiement.
  • Bloc de réglages en <setting> dans un <block>.
  • Deux tests qui ne pouvaient pas échouer sont corrigés : le ticket étranger est créé sur une étape en amont, et un test couvre l'étape absente de project.type_ids.

Deux points non appliqués

  • _set_stage_on_project_from_task() (proposé ci-dessus) : ce helper rattache au projet l'étape courante du ticket, pas la cible ; appelé avant l'écriture il rattache l'étape de départ, puis le calcul de stage_id peut remplacer la cible. Le module rattache la cible avec Command.link, le même geste pour la bonne étape, et un test le couvre.
  • Savepoint de _register_deployment : écarté, comme répondu sur place.

Vérifications : 32 tests verts sur le banc odoo 18 (14 client, 14 central, 4 router) et pre-commit vert. Quatre changements de comportement, reportés dans le plan : les tickets d'un payload remplacent les précédents (D3), l'étape s'applique sans condition (D9), les anomalies vont dans message et non dans les logs (D15), le bouton d'envoi disparaît (D5).

@rob-beau rob-beau left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Résumé : 3 points sur les correctifs (les tickets ne sont jamais remplacés côté central, le mail de prod repart à chaque payload, et les destinataires de ce mail restent à trancher) ; les 11 points de la revue précédente sont traités, 8 corrigés et 3 arbitrés.
Vérifié en exécution sur le banc, pas seulement relu : 32 tests verts sur ce head, plus trois tests jetables pour les trois points ci-dessous.

Détail

Le banc rejoue la suite du dépôt de la PR (odoo 18.0 + postgres 16, --test-enable) : 32 tests, 0 échec, ce qui confirme le plan §18. Les trois tests jetables de cette passe prouvent les points 1 et 2 (la liste de tickets ne rétrécit pas, deux mail.mail pour deux payloads identiques) et mesurent l'étendue des destinataires du point 3.

Les 11 conversations de la revue précédente sont fermées : corrigées pour le menu, le tri, l'isolation sans partenaire, min_length, l'ACL, le test qui ne pouvait pas échouer et le bloc de réglages ; arbitrées pour le savepoint, le helper du core (à juste titre, il rattache l'étape courante et non la cible) et l'absorption de next, remplacée par une suppression avec un payload complet. Je ne rouvre pas les décisions documentées en D3, D5, D9 et D15.

Les commentaires suivent l'ordre des fichiers.

"message": "\n".join(messages) or False,
}
if tasks:
vals["task_ids"] = [Command.link(task.id) for task in tasks]

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Bug de données : le central ajoute les tickets, il ne les remplace pas.
Le client remplace sa liste à chaque payload (D3) et next est supprimée au motif que « le payload porte déjà ses tickets », mais Command.link ne fait qu'ajouter : un ticket retiré d'un payload rejoué reste lié, remonté à l'étape de l'environnement, et annoncé dans le mail de mise en prod.
Correctif : vals["task_ids"] = [Command.set(tasks.ids)], et vider le partenaire quand la liste est vide.

Détail
Prouvé sur le banc : payload preprod 14.0.1.0 avec [A, B], puis la même version avec [A] ; la release garde [A, B]. test_second_payload_is_idempotent ne le voit pas, il renvoie la même liste les deux fois.

L'impact est concret : mail_template_release_in_prod boucle sur object.task_ids, donc le client reçoit l'annonce d'un ticket qui n'est pas dans la version, et ce ticket a été déplacé vers l'étape de l'environnement au passage.

Si l'ajout est le comportement voulu côté central, c'est D3 et le commentaire de _remove_next_release() (« the payload already carries its tickets ») qu'il faut aligner : aujourd'hui les deux côtés ne racontent pas la même chose.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

effectivement il faut écraser les task mettre la commande qui fait un [(6, 0, [])]

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Corrigé : vals["task_ids"] = [Command.set(tasks.ids)]. Un ticket absent du payload est détaché de la release, donc il n'est plus déplacé ni annoncé dans le mail. test_payload_replaces_tickets couvre le cas (payload qui rétrécit de [A, B] à [A]).

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Appliqué, Command.set : le payload fait foi, la liste peut aussi se vider.


def _get_notification_emails(self):
"""Emails of the users of the client, the recipients of the release mail."""
users = self.cross_connect_client_id.user_ids.filtered(

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Question : le mail part à tous les utilisateurs du client, alors que la consigne était un opt-in.
_get_notification_emails() notifie chaque utilisateur actif du client porteur d'un email ; la remarque du 31/10 sur cette méthode demandait une option sur l'utilisateur, ou un groupe, pour ne prévenir que les volontaires.
Correctif : un booléen support_release_notify sur res.users (ou un groupe dédié) filtré ici, ou confirmer que « tous les utilisateurs du client » est la décision et retirer la consigne.

Détail
Prouvé sur le banc : deux utilisateurs sur le client, deux adresses dans email_to.

Le plan §17 documente aujourd'hui les user_ids du cross.connect.client, donc code et plan sont d'accord entre eux, mais pas avec le commentaire de la revue. À trancher : un projet avec dix utilisateurs envoie dix fois le mail à chaque mise en production, et rien ne permet à un utilisateur de s'y soustraire.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Laissé en l'état pour le moment, l'arbitrage est en cours : les user_ids du client restent les destinataires, et c'est ce que documente le plan §17.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

alors ce que l'on va faire c'est que sur le partner qui est sur la release on va mettre un champs m2m, qui listera les personnes a notifier.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fait : support_release_notification_user_ids, Many2many vers res.users sur res.partner, éditable dans l'onglet Notes internes de la fiche client. _get_notification_emails() lit cette liste sur le partenaire de la release, filtre les comptes actifs avec un email et dédoublonne ; sans utilisateur, aucun mail et une ligne dans message.

_name = "support.release"
_description = "Support Release"
_rec_name = "version"
_order = "create_date, id"

@sebastienbeau sebastienbeau Nov 2, 2026 •

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Trie sur l'id est suffisant

Suggested change
_order = "create_date, id"
_order = "id desc"

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Appliqué : _order = "id desc". _next_release_to_send() garde un order="id" explicite, sinon la file enverrait la plus récente d'abord.

"message": "\n".join(messages) or False,
}
if tasks:
vals["task_ids"] = [Command.link(task.id) for task in tasks]

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

effectivement il faut écraser les task mettre la commande qui fait un [(6, 0, [])]

"""Move the tickets of the release to the stage of the environment."""
for task in self.task_ids:
if stage not in task.project_id.type_ids:
task.project_id.sudo().write({"type_ids": [Command.link(stage.id)]})

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

pas besoin d'ajouter le stage au projet

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Appliqué : le bloc qui rattachait l'étape au projet est parti, _apply_stages devient self.task_ids.sudo().stage_id = stage. Le README précise que les deux étapes configurées doivent être rattachées aux projets du client pour apparaître dans leur kanban.

if environment == "preprod" and release.prod_deployed_at:
return release
tasks, messages = self._filter_tickets(release, payload.get("tickets") or [])
release.write(self._release_vals(environment, tasks, messages))

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

la convention dans odoo c'est de nommer ce genre de method "_prepare_release_vals" au lieu de _release_vals

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Renommée en _prepare_release_vals.

Central side, the tickets of a payload now replace the previous ones:
a ticket dropped from the release is unlinked, so the production mail
never announces it. The partner of the release comes from the projects
of the client (project.project.cross_connect_client_id) instead of the
projects of the tickets, and several partners on those projects raise:
the client is one customer, an inconsistent configuration has to be
seen rather than arbitrated silently in a log. A ticket is linked only
when its project belongs to the client sending the payload, which
closes the hole left by the previous filter on the partner alone.

A release already deployed in production ignores any further payload,
whatever the environment: a payload replayed after a timeout cannot
send the production mail twice.

The stage is written on the tickets in a single assignment and is no
longer added to the projects; the two configured stages have to be
linked to the projects of the client, which is the normal setup.

The method computing the values of the release is renamed
_prepare_release_vals, following the Odoo convention.

The manifest depends on project_customer_access, the owner of
cross_connect_client_id on projects, so the client can be resolved to
its projects. That reverses the earlier decision to keep the module
free of that dependency, which is consistent: filtering by project of
the client replaces filtering by partner.

Customer side, the default order is id desc; the queue keeps an
explicit order on id so the oldest pending release is still sent
first.

The icon of both modules becomes the Akretion logo, the repository
being an Akretion one rather than an OCA one.

Assisted-by: Hermes Agent (Nous Research)
Same icon as support_server_release: the repository is an Akretion one, the
OCA icon was a mistake.

Assisted-by: Hermes Agent (Nous Research)
@rob-beau

rob-beau commented Nov 2, 2026

Copy link
Copy Markdown

Deuxième passe traitée en local (commit 2cbeb7e, pas encore poussé). 34 tests verts sur le banc, avec project_customer_access et sa chaîne réellement installées cette fois, donc sur le vrai project.project.cross_connect_client_id.

Les trois points de la revue précédente :

  • les tickets sont désormais remplacés à chaque payload (Command.set) : un ticket retiré de la release est détaché, donc plus annoncé dans le mail de mise en prod ;
  • le mail de mise en prod ne part qu'une fois : une release qui a déjà un prod_deployed_at ignore tout payload suivant, préprod comme prod, ce qui couvre le rejeu après timeout ;
  • les destinataires : rien de changé, en attente de ton arbitrage.

Deux changements de fond, documentés dans le plan (D7, D8 amendés) : la release prend son partenaire sur les projets du client et lève si ces projets portent plusieurs partenaires, et un ticket n'est lié que si son projet porte le cross_connect_client_id du client émetteur. Cela fait entrer project_customer_access dans le manifeste, seul propriétaire de ce champ.

"""Push the oldest pending release, one per cron run."""
if not self._get_environment():
return
release = self._next_release_to_send()

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

au final la fonction ne fait qu'un ligne donc autant faire l'appel direct ici
et pas besoin de mettre le order car c'est déjà celui par défaut

Suggested change
release = self._next_release_to_send()
release = self.search([("is_sent", "=", False)], limit=1)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Appliqué : la méthode disparaît, la cron fait self.search([("is_sent", "=", False)], limit=1). À noter, conséquence directe : l'ordre par défaut du modèle étant id desc, la file envoie désormais la release la plus récente d'abord et la plus ancienne en dernier. Si tu voulais garder la plus ancienne en premier, il suffit de remettre un order="id".

if unknown:
messages.append(_("Unknown tickets: %s", unknown))
kept = tasks.filtered(
lambda task: task.project_id.cross_connect_client_id == client

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

attention, corner case. On peut avoir un cas ou

  • le projet n'a pas de cross_connect_client_id
  • mais le projet a bien un partner_id de défini (qui est le meme que sur les autres projets)

Et dans ce cas la tache doit bien etre affecté a la release mais pas notifier par mail au client.

Ce sont des taches internes de maintenance non visible par le client (donc un projet sans cross_connect_id)

Donc l'idée c'est de trouver le partenaire a partir des projets ayant le cross connect et ensuite de filtrer les taches par rapport a ce partenaire qui doit se retrouver sur tout les projets lié au tache.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Appliqué : le filtrage porte maintenant sur le partenaire, plus sur le projet du client. _filter_tickets(partner, ...) garde les tickets dont le projet porte ce partenaire, donc un projet interne de maintenance (sans cross_connect_client_id, même partenaire) voit bien ses tickets liés à la release et annoncés dans le mail, alors qu'ils restent invisibles côté portail. Un ticket d'un autre partenaire n'est pas lié et son id est écrit dans message. Cas couvert par test_ticket_of_internal_project_is_linked.


def _get_notification_emails(self):
"""Emails of the users of the client, the recipients of the release mail."""
users = self.cross_connect_client_id.user_ids.filtered(

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

alors ce que l'on va faire c'est que sur le partner qui est sur la release on va mettre un champs m2m, qui listera les personnes a notifier.

Customer side, _next_release_to_send() only did a search, so the cron
calls it directly and drops the explicit order: the queue follows the
default order of the model, which means the most recently registered
pending release goes first.

Central side, the tickets are filtered on the partner of the client
instead of the cross connect client of their project. An internal
project has no cross connect client but carries the partner of the
customer, and its tickets are part of the release, they simply are not
visible in the portal. A ticket of another partner is left untouched
and listed in the message field. The partner is resolved once in
_process and passed down to the filter and to the values, instead of
being computed twice.

The recipients of the production mail move to the customer: a new
many2many of users, support_release_notification_user_ids, on
res.partner, editable in the Internal Notes tab of the partner form.
The mail is addressed to the users flagged on the partner of the
release, so the list is maintained once per customer instead of
following the portal accounts created by the cross connection.

Assisted-by: Hermes Agent (Nous Research)
@rob-beau

rob-beau commented Nov 2, 2026

Copy link
Copy Markdown

Troisième passe traitée en local (commit 8409d54, pas encore poussé). 34 tests verts sur le banc, avec la chaîne project_customer_access installée pour de vrai.

  • la file du client : _next_release_to_send() disparaît, la cron fait le search elle-même sans order. Conséquence à valider : l'ordre par défaut du modèle étant id desc, la release la plus récente part maintenant en premier, la plus ancienne en dernier. Si c'est l'inverse que tu veux, un order="id" suffit.
  • le filtrage des tickets se fait sur le partenaire du client et non plus sur le cross_connect_client_id du projet : les tickets des projets internes de maintenance (même partenaire, pas de cross connect) sont liés et donc dans le mail, ceux d'un autre partenaire restent à leur étape et sont listés dans message.
  • les destinataires du mail sont portés par le partenaire : support_release_notification_user_ids, un Many2many de res.users éditable dans l'onglet Notes internes de la fiche client.

<field name="model_id" ref="model_support_server_release" />
<field
name="subject"
>{{ object.cross_connect_client_id.name }}: version {{ object.version }} in production</field>

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

tu peux rajouter la traduction du mail ?

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fait : le template porte maintenant <field name="lang">{{ object.partner_id.lang }}</field>, le champ que Odoo utilise pour rendre un mail par langue. Sans lui le mail part toujours en anglais, même avec un contexte fr_FR, puisqu'il n'y a pas de langue à choisir.

i18n/fr.po contient le nom du template, le sujet et le corps en français (Bonjour, la version X de Y est désormais déployée en production, Cordialement).

Test ajouté : test_prod_mail_is_sent_in_the_language_of_the_customer, qui active le français, charge les termes du module et vérifie le sujet et le corps du mail généré. 35 tests verts sur le banc.

…r language

The template now carries the lang placeholder of the partner of the release,
which is the field Odoo uses to render a mail per language: without it the
mail always goes out in English, whatever the language of the customer.

The French translation of the name, the subject and the body is shipped in
i18n/fr.po, and a test activates the language to check the subject and the
body of the generated mail.

Assisted-by: Hermes Agent (Nous Research)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

mod:support_release Module support_release mod:support_server_release Module support_server_release series:18.0

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants