From 9126d2016ad37cce0d5bdb6092317b65dd060c61 Mon Sep 17 00:00:00 2001 From: pcosson Date: Mon, 27 Jul 2026 09:06:06 +0200 Subject: [PATCH] feat: throttle no-change notifications daily --- .gitignore | 1 + README.md | 8 ++++++++ TRAVAIL.md | 8 ++++++++ WAPT/control | 8 ++++---- setup.py | 45 ++++++++++++++++++++++++++++++++++++++++- tests/test_ticketing.py | 29 ++++++++++++++++++++++++++ 6 files changed, 94 insertions(+), 5 deletions(-) diff --git a/.gitignore b/.gitignore index 3e12e1e..e87395f 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,6 @@ /ticketing.ini /tickets_state.json +/no_change_notification_state.json /WAPT/certificate.crt /WAPT/manifest.sha256 /WAPT/signature.sha256 diff --git a/README.md b/README.md index 0a55e7a..e5e3f13 100644 --- a/README.md +++ b/README.md @@ -97,6 +97,8 @@ Hors WAPT, le script ne peut pas etre lance tel quel sans fournir les objets et Le runtime lit et ecrit ce fichier dans `WAPT.private_dir`. Il n'est pas versionne dans le depot. Les anciens etats au format simple `ticket_id -> write_date` restent lisibles. +`no_change_notification_state.json` est aussi stocke dans `WAPT.private_dir`. Il garde la date de derniere notification "aucun changement" afin de ne l'envoyer qu'une fois par jour. + 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. @@ -158,6 +160,12 @@ Les tickets suivants ont ete mis a jour depuis la derniere verification : 251 : Sujet : Nouveau ticket gere par Personne : [cliquer ici pour ouvrir le ticket](https://odoo.example.test/web#id=251&model=helpdesk.ticket&view_type=form) ``` +Quand aucun ticket ne change, Rocket.Chat recoit au maximum une notification par jour : + +```text +Aucun ticket n'a ete mis a jour depuis la derniere verification. +``` + ## Tests Les tests unitaires peuvent etre executes hors WAPT : diff --git a/TRAVAIL.md b/TRAVAIL.md index ca3c87b..214fc0e 100644 --- a/TRAVAIL.md +++ b/TRAVAIL.md @@ -151,3 +151,11 @@ Ce fichier consigne les interventions Codex sur le projet afin de garder une tra - L'etat local stocke maintenant `write_date` du ticket et le dernier marqueur de message chatter connu. - Ajout de `odoo.chatter_model = mail.message` dans le modele de configuration. - Incrementation de la version WAPT de `0.1-17` a `0.1-18`. + +### Notification quotidienne sans changement + +- Ajout d'un etat `no_change_notification_state.json` dans `WAPT.private_dir`. +- La notification "aucun ticket mis a jour" est envoyee une seule fois par jour. +- Les notifications de vrais changements restent envoyees a chaque detection. +- Ajout de tests unitaires sur l'envoi unique par jour. +- Incrementation de la version WAPT de `0.1-19` a `0.1-20`. diff --git a/WAPT/control b/WAPT/control index 43e2778..86ba6e6 100644 --- a/WAPT/control +++ b/WAPT/control @@ -1,5 +1,5 @@ package : comi-odoo-ticketing -version : 0.1-18 +version : 0.1-20 architecture : all section : base priority : optional @@ -29,7 +29,7 @@ editor : keywords : licence : homepage : -package_uuid : f974e9d0-c356-77d6-4e13-273510424e1f +package_uuid : 2456a8e9-6f8c-ea82-c058-e52636509fec 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-24T08:37:11.000000 +signature_date : 2026-07-24T08:45:27.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 : o1xwbr7V5kTvRJ7GrHHWd0xzEjrPovHQjVcEOj4N7j56/m0MaiwScoBAYJ+CaNEfiP2MuMPGqJz+WuYLZrKZvXNZhTfP0fdP6ligApowHvjUJEdwJ/cpzwKcXd/yD8Q0bLLChNrFFQz8wRoaBNsvmcJCPfPp353aNOemKe8+JubgAZTdF4VdyxMbglMDBRy4ECnK5YNpZWYFzHEamVWyoQ3LTo92UHL/PdZrvefQ88MOfGH055npv1YdR/AD235BrjDm1AD0CqIMMe1UnFD41XcBwB9mxD7Wh0jLmzDqRCnCbzfuq98VgNp0L1PZQJXgeu1tVrG9ycGrexdQrDWPzA== +signature : TonQ2rHBMLmtP8I6ZgJDESg9TSpyZ42wwpkq34ii3IDvk4XMeuFUFnrDRazrGAAqVn+UVCpoRw+Ne19UOeM/Z/csqSYKYqmdfQpGwEmHjsoAb5A6+oSTDVHa+I6+Pv2iI+6hXAnMediGPfT7y10i+OGCqeKJQxNzsZW5MR+9GjgJQE7YN6lFJ/hDD7v/NG/ry5YL2cv0xEDmqgd5dThPmfHGSOxiqyjZWOHhW4kYrl7HjSHRG4ewdXoUELW4Eo+gNxZnLI8GOt8xVWBu625DnLVaxrhFQ59d4mcQA0xVy3TdxmLL2XiDJ9Cj0zUrNrw+MyD5UUjr/PvTv7X1mnhF/Q== diff --git a/setup.py b/setup.py index b11c148..b43214a 100644 --- a/setup.py +++ b/setup.py @@ -4,6 +4,7 @@ import json import os import shutil from configparser import ConfigParser +from datetime import date import requests @@ -11,6 +12,7 @@ import requests CONFIG_FILE = "ticketing.ini" CONFIG_TEMPLATE_FILE = "ticketing.ini.example" STATE_FILE = "tickets_state.json" +NO_CHANGE_STATE_FILE = "no_change_notification_state.json" DEFAULT_TIMEOUT = 30 DEFAULT_TICKET_FIELDS = ["id", "name", "stage_id", "partner_id", "user_id", "write_date"] DEFAULT_MAX_NOTIFIED_TICKETS = 50 @@ -29,6 +31,10 @@ def get_state_path(): return makepath(WAPT.private_dir, STATE_FILE) +def get_no_change_state_path(): + return makepath(WAPT.private_dir, NO_CHANGE_STATE_FILE) + + def install(): config_path = get_config_path() @@ -53,7 +59,13 @@ def audit(): conf_wapt.get("odoo", "model", fallback="helpdesk.ticket"), ) - send_to_rocket(message, conf_wapt=conf_wapt) + if should_send_notification(tickets_updated, get_no_change_state_path()): + send_to_rocket(message, conf_wapt=conf_wapt) + if not tickets_updated: + record_no_change_notification(get_no_change_state_path()) + else: + print("Notification aucun changement deja envoyee aujourd'hui.") + save_tickets_state(get_state_path(), build_tickets_state(tickets)) return "OK" @@ -284,6 +296,37 @@ def save_tickets_state(state_path, tickets_state): json.dump(tickets_state, state_file, indent=2, sort_keys=True) +def get_today(): + return date.today().isoformat() + + +def load_json_state(state_path): + if not os.path.exists(state_path): + return {} + + with open(state_path, "r") as state_file: + return json.load(state_file) + + +def save_json_state(state_path, state): + with open(state_path, "w") as state_file: + json.dump(state, state_file, indent=2, sort_keys=True) + + +def should_send_notification(tickets_updated, no_change_state_path): + if tickets_updated: + return True + + no_change_state = load_json_state(no_change_state_path) + return no_change_state.get("last_no_change_notification_date") != get_today() + + +def record_no_change_notification(no_change_state_path): + save_json_state(no_change_state_path, { + "last_no_change_notification_date": get_today(), + }) + + def build_tickets_state(tickets): return { str(ticket["id"]): build_ticket_state_value(ticket) diff --git a/tests/test_ticketing.py b/tests/test_ticketing.py index dea7dae..70c1ca7 100644 --- a/tests/test_ticketing.py +++ b/tests/test_ticketing.py @@ -203,6 +203,35 @@ class TicketingStateTest(unittest.TestCase): self.assertEqual(ticketing.load_tickets_state(state_path), expected_state) + def test_should_send_notification_always_sends_when_tickets_changed(self): + with tempfile.TemporaryDirectory() as tmpdir: + state_path = os.path.join(tmpdir, "no_change_notification_state.json") + ticket = {"id": 42, "write_date": "2026-07-24 12:00:00"} + + self.assertTrue(ticketing.should_send_notification([ticket], state_path)) + + @patch("ticketing_setup.get_today", return_value="2026-07-27") + def test_should_send_no_change_notification_once_per_day(self, get_today): + with tempfile.TemporaryDirectory() as tmpdir: + state_path = os.path.join(tmpdir, "no_change_notification_state.json") + + self.assertTrue(ticketing.should_send_notification([], state_path)) + + ticketing.record_no_change_notification(state_path) + + self.assertFalse(ticketing.should_send_notification([], state_path)) + get_today.assert_called() + + @patch("ticketing_setup.get_today", return_value="2026-07-28") + def test_should_send_no_change_notification_again_next_day(self, get_today): + with tempfile.TemporaryDirectory() as tmpdir: + state_path = os.path.join(tmpdir, "no_change_notification_state.json") + ticketing.save_json_state(state_path, { + "last_no_change_notification_date": "2026-07-27", + }) + + self.assertTrue(ticketing.should_send_notification([], state_path)) + @patch("ticketing_setup.requests.post") def test_fetch_odoo_tickets_authenticates_and_returns_result(self, post): auth_response = Mock()