From 93b6947febaf2b7c7713b090859e5c9dbf37efc6 Mon Sep 17 00:00:00 2001 From: pcosson Date: Fri, 24 Jul 2026 10:06:18 +0200 Subject: [PATCH] feat: enrich ticket update notifications --- .gitignore | 1 + README.md | 27 +++++++++++- TRAVAIL.md | 8 ++++ WAPT/control | 8 ++-- setup.py | 95 +++++++++++++++++++++++++++++++++++++---- tests/test_ticketing.py | 36 ++++++++++++++++ ticketing.ini.example | 4 ++ 7 files changed, 166 insertions(+), 13 deletions(-) diff --git a/.gitignore b/.gitignore index 6bb1b3f..3e12e1e 100644 --- a/.gitignore +++ b/.gitignore @@ -3,3 +3,4 @@ /WAPT/certificate.crt /WAPT/manifest.sha256 /WAPT/signature.sha256 +__pycache__/ diff --git a/README.md b/README.md index dfeaa77..43fb673 100644 --- a/README.md +++ b/README.md @@ -50,9 +50,13 @@ database = CHANGE_ME_ODOO_DATABASE username = CHANGE_ME_ODOO_USERNAME api_key = CHANGE_ME_ODOO_API_KEY model = helpdesk.ticket +fields = id,name,stage_id,partner_id,user_id,write_date [http] timeout = 30 + +[notification] +max_tickets = 50 ``` Au deploiement, `install()` copie ce modele vers `WAPT.private_dir/ticketing.ini` uniquement si le fichier n'existe pas deja. Il faut ensuite renseigner le fichier prive sur la machine cible. Les mises a jour du paquet n'ecrasent donc pas une configuration deja remplie. @@ -86,6 +90,10 @@ Le runtime lit et ecrit ce fichier dans `WAPT.private_dir`. Il n'est pas version La cle API est utilisee comme mot de passe d'API Odoo pour authentifier `username`. Il est aussi possible de renseigner `uid` dans la section `[odoo]` pour eviter l'appel d'authentification. +`odoo.fields` controle les champs recuperes depuis Odoo pour enrichir la notification. Les champs `id` et `write_date` sont toujours ajoutes s'ils sont absents, car ils sont necessaires a la comparaison d'etat. + +`notification.max_tickets` limite le nombre de lignes detaillees envoyees dans Rocket.Chat. Le message indique toujours le nombre total de tickets modifies. + ## Protocole Odoo Le script utilise l'endpoint configure sous la forme `/jsonrpc` avec le protocole JSON-RPC Odoo standard. @@ -114,12 +122,29 @@ Lecture des tickets : "params": { "service": "object", "method": "execute_kw", - "args": ["database", "uid", "api_key", "helpdesk.ticket", "search_read", [[]], {"fields": ["id", "write_date"]}] + "args": ["database", "uid", "api_key", "helpdesk.ticket", "search_read", [[]], {"fields": ["id", "name", "stage_id", "partner_id", "user_id", "write_date"]}] }, "id": 1 } ``` +## Notification + +Quand des tickets changent, Rocket.Chat recoit un message avec : + +- le nombre total de tickets modifies ; +- une ligne par ticket affiche, limitee par `notification.max_tickets` ; +- l'ID, le titre, le type de changement, le statut, le client, l'assigne, la nouvelle date de modification et l'ancienne date quand elle existe. + +Exemple : + +```text +Tickets Odoo mis a jour depuis la derniere verification : 2 + +- #187 | Probleme imprimante | type: modifie | statut: En cours | client: Comitari | assigne: Support | date: 2026-07-24 10:00:00 | ancienne date: 2026-07-24 09:00:00 +- #251 | Nouveau ticket | type: nouveau | statut: - | client: - | assigne: - | date: 2026-07-24 11:00:00 +``` + ## Tests Les tests unitaires peuvent etre executes hors WAPT : diff --git a/TRAVAIL.md b/TRAVAIL.md index 08e9964..4ffe0ca 100644 --- a/TRAVAIL.md +++ b/TRAVAIL.md @@ -112,3 +112,11 @@ Ce fichier consigne les interventions Codex sur le projet afin de garder une tra - Correction de version demandee : passage de `0.1.0-3` a `0.1-10` pour rattraper les increments precedents. - Attention : `target_os` fait partie des attributs signes, le paquet doit etre regenere/resigne avant publication. - Regle projet retenue : incrementer `WAPT/control:version` a chaque nouvelle version du paquet. + +### Notification Rocket.Chat plus detaillee + +- Recuperation de champs Odoo supplementaires par defaut : `name`, `stage_id`, `partner_id`, `user_id`. +- Ajout de l'option `odoo.fields` pour ajuster les champs lus par `search_read`. +- Ajout de l'option `notification.max_tickets` pour limiter le nombre de lignes detaillees dans Rocket.Chat. +- Remplacement du message brut contenant seulement une liste d'IDs par un message avec nombre total, titre, statut, client, assigne, date courante et ancienne date quand disponible. +- Incrementation de la version WAPT locale de `0.1-11` a `0.1-12`. diff --git a/WAPT/control b/WAPT/control index 10706f8..a663cb1 100644 --- a/WAPT/control +++ b/WAPT/control @@ -1,5 +1,5 @@ package : comi-odoo-ticketing -version : 0.1-10 +version : 0.1-12 architecture : all section : base priority : optional @@ -29,7 +29,7 @@ editor : keywords : licence : homepage : -package_uuid : 0afd3de2-c939-918b-67b0-d79a8ccba84b +package_uuid : 94cab12e-61fb-4a49-fcd2-c1d160d18b22 valid_from : valid_until : forced_install_on : @@ -39,6 +39,6 @@ max_os_version : icon_sha256sum : signer : pcosson_key signer_fingerprint: a25582410cf03bad179a60c189f459a0b03821c92c0cedf209e82448a66a9b4e -signature_date : 2026-07-24T07:19:02.000000 +signature_date : 2026-07-24T07:48:26.000000 signed_attributes : package,version,architecture,section,priority,name,categories,maintainer,description,depends,conflicts,maturity,locale,target_os,min_wapt_version,sources,installed_size,impacted_process,description_fr,description_pl,description_de,description_es,description_pt,description_it,description_nl,description_ru,audit_schedule,editor,keywords,licence,homepage,package_uuid,valid_from,valid_until,forced_install_on,changelog,min_os_version,max_os_version,icon_sha256sum,signer,signer_fingerprint,signature_date,signed_attributes -signature : dBwE3rudjuJOvYhduNbzizfvql8Fqo13fs6anaHSoRoBCnRteqzJy4JRmaA6OV+YJOtfC89E7OnNBKAOvO1YErsaanKzFG+wkZFBZEgGkBcJZsb0xec5iLHoslEt6ZJw9ldsSs0dzRH1tzIb7nthMi5AOkPPqICKf5mXkYiOtAcBG9HjFa8YeYtBwyFM5ME2XeKkqpsESh03cOseNLpQ9DB2vMe0qZHd14nPZe5TpOFen+F4xklHSfemiMU8AFPclewTuhkYC8mqps2+WkHmTWl35ilJGg8BC2nLxiydotmOwc+QqpAYw90N6r9lVnPUFa1Bh2QqnmJnk5bfePYKTQ== +signature : FGra+/RipOk5jmR4CkBmu4s9DQkzJ3u8bGhssL3zpRgzynMdnfNDSgVkBIM5XoQsqb+GluNnvqzGT19jvhOr1OVxF4Fk6ErMK/oXezLe/lX2vezTfRGoSUzVG+KPZrzqcuM0Q0g6gu2sctASMw0RuMMdMVgXWrZt/zVu+wS6P6O5MpCoOVRLMoV22jXAuXSz888rwBmSOyVU860Un1wPm4zin+gaJgvsy3GZQSSMhJBR8CGAj8QmeL6cjJrspJ0LDiE8m4UJXHJvlAsRIGxs9Y928L6x2Tg0pl6j02wNnZTKeh3mAM5KZRy5UJR8fbqXbBDuJJR7R1gux2WTw7mEeQ== diff --git a/setup.py b/setup.py index 7eca65f..b1474ad 100644 --- a/setup.py +++ b/setup.py @@ -12,6 +12,8 @@ CONFIG_FILE = "ticketing.ini" CONFIG_TEMPLATE_FILE = "ticketing.ini.example" STATE_FILE = "tickets_state.json" DEFAULT_TIMEOUT = 30 +DEFAULT_TICKET_FIELDS = ["id", "name", "stage_id", "partner_id", "user_id", "write_date"] +DEFAULT_MAX_NOTIFIED_TICKETS = 50 def get_config_path(): @@ -33,12 +35,12 @@ def audit(): conf_wapt = load_config() tickets = fetch_odoo_tickets(conf_wapt) previous_tickets_state = load_tickets_state(get_state_path()) - tickets_updated = get_updated_ticket_ids(tickets, previous_tickets_state) - - if tickets_updated: - message = "Les tickets suivants ont ete mis a jour depuis la derniere verification : %s" % tickets_updated - else: - message = "Aucun ticket n'a ete mis a jour depuis la derniere verification." + tickets_updated = get_updated_tickets(tickets, previous_tickets_state) + message = build_notification_message( + tickets_updated, + previous_tickets_state, + get_max_notified_tickets(conf_wapt), + ) send_to_rocket(message, conf_wapt=conf_wapt) save_tickets_state(get_state_path(), build_tickets_state(tickets)) @@ -92,6 +94,22 @@ def get_http_timeout(conf_wapt): return conf_wapt.getint("http", "timeout", fallback=DEFAULT_TIMEOUT) +def get_odoo_fields(conf_wapt): + fields = [ + field.strip() + for field in conf_wapt.get("odoo", "fields", fallback=",".join(DEFAULT_TICKET_FIELDS)).split(",") + if field.strip() + ] + for required_field in ["id", "write_date"]: + if required_field not in fields: + fields.append(required_field) + return fields + + +def get_max_notified_tickets(conf_wapt): + return conf_wapt.getint("notification", "max_tickets", fallback=DEFAULT_MAX_NOTIFIED_TICKETS) + + def build_odoo_payload(service, method, args): return { "jsonrpc": "2.0", @@ -160,7 +178,7 @@ def fetch_odoo_tickets(conf_wapt): conf_wapt.get("odoo", "model", fallback="helpdesk.ticket"), "search_read", [[]], - {"fields": ["id", "write_date"]}, + {"fields": get_odoo_fields(conf_wapt)}, ], ) @@ -186,6 +204,13 @@ def build_tickets_state(tickets): def get_updated_ticket_ids(tickets, previous_tickets_state): + return [ + ticket["id"] + for ticket in get_updated_tickets(tickets, previous_tickets_state) + ] + + +def get_updated_tickets(tickets, previous_tickets_state): tickets_updated = [] for ticket in tickets: @@ -193,11 +218,65 @@ def get_updated_ticket_ids(tickets, previous_tickets_state): ticket_write_date = ticket["write_date"] if previous_tickets_state.get(ticket_id) != ticket_write_date: - tickets_updated.append(ticket["id"]) + tickets_updated.append(ticket) return tickets_updated +def display_odoo_value(value): + if value in (False, None, ""): + return "-" + if isinstance(value, (list, tuple)): + if len(value) >= 2: + return str(value[1]) + if len(value) == 1: + return str(value[0]) + return "-" + return str(value) + + +def format_ticket_line(ticket, previous_tickets_state): + ticket_id = str(ticket["id"]) + previous_write_date = previous_tickets_state.get(ticket_id) + change_type = "nouveau" if previous_write_date is None else "modifie" + parts = [ + "#%s" % ticket["id"], + display_odoo_value(ticket.get("name", "Sans titre")), + "type: %s" % change_type, + "statut: %s" % display_odoo_value(ticket.get("stage_id")), + "client: %s" % display_odoo_value(ticket.get("partner_id")), + "assigne: %s" % display_odoo_value(ticket.get("user_id")), + "date: %s" % display_odoo_value(ticket.get("write_date")), + ] + if previous_write_date is not None: + parts.append("ancienne date: %s" % previous_write_date) + return "- " + " | ".join(parts) + + +def build_notification_message(tickets_updated, previous_tickets_state, max_tickets): + if not tickets_updated: + return "Aucun ticket n'a ete mis a jour depuis la derniere verification." + + displayed_tickets = tickets_updated[:max_tickets] + hidden_count = len(tickets_updated) - len(displayed_tickets) + lines = [ + "Tickets Odoo mis a jour depuis la derniere verification : %s" % len(tickets_updated), + "", + ] + lines.extend([ + format_ticket_line(ticket, previous_tickets_state) + for ticket in displayed_tickets + ]) + + if hidden_count > 0: + lines.extend([ + "", + "... %s ticket(s) supplementaire(s) non affiche(s)." % hidden_count, + ]) + + return "\n".join(lines) + + def send_to_rocket(message_text, attachments=None, conf_wapt=None): """ Envoie un message a Rocket.Chat via un webhook. diff --git a/tests/test_ticketing.py b/tests/test_ticketing.py index 40ee802..934c591 100644 --- a/tests/test_ticketing.py +++ b/tests/test_ticketing.py @@ -69,6 +69,38 @@ class TicketingStateTest(unittest.TestCase): self.assertEqual(ticketing.get_updated_ticket_ids(tickets, previous_state), [2, 3]) + def test_build_notification_message_is_verbose_and_limited(self): + previous_state = { + "1": "2026-07-24 09:00:00", + } + tickets = [ + { + "id": 1, + "name": "Probleme imprimante", + "stage_id": [4, "En cours"], + "partner_id": [8, "Comitari"], + "user_id": [9, "Support"], + "write_date": "2026-07-24 10:00:00", + }, + { + "id": 2, + "name": "Nouveau ticket", + "stage_id": False, + "partner_id": False, + "user_id": False, + "write_date": "2026-07-24 11:00:00", + }, + ] + + message = ticketing.build_notification_message(tickets, previous_state, 1) + + self.assertIn("Tickets Odoo mis a jour depuis la derniere verification : 2", message) + self.assertIn("#1 | Probleme imprimante | type: modifie", message) + self.assertIn("statut: En cours", message) + self.assertIn("ancienne date: 2026-07-24 09:00:00", message) + self.assertIn("1 ticket(s) supplementaire(s) non affiche(s)", message) + self.assertNotIn("Nouveau ticket", message) + def test_load_tickets_state_returns_empty_dict_when_file_is_missing(self): missing_path = os.path.join(tempfile.gettempdir(), "missing-ticket-state.json") @@ -102,6 +134,10 @@ class TicketingStateTest(unittest.TestCase): self.assertEqual(post.call_args.kwargs["timeout"], 5) self.assertEqual(post.call_args.kwargs["json"]["params"]["service"], "object") self.assertEqual(post.call_args.kwargs["json"]["params"]["method"], "execute_kw") + self.assertEqual( + post.call_args.kwargs["json"]["params"]["args"][6]["fields"], + ["id", "name", "stage_id", "partner_id", "user_id", "write_date"], + ) @patch("ticketing_setup.requests.post") def test_fetch_odoo_tickets_rejects_jsonrpc_error(self, post): diff --git a/ticketing.ini.example b/ticketing.ini.example index 4295748..2c2251a 100644 --- a/ticketing.ini.example +++ b/ticketing.ini.example @@ -7,6 +7,10 @@ database = CHANGE_ME_ODOO_DATABASE username = CHANGE_ME_ODOO_USERNAME api_key = CHANGE_ME_ODOO_API_KEY model = helpdesk.ticket +fields = id,name,stage_id,partner_id,user_id,write_date [http] timeout = 30 + +[notification] +max_tickets = 50