From adedf56b7b04f8223861243b8ade0bc4967f5220 Mon Sep 17 00:00:00 2001 From: pcosson Date: Fri, 24 Jul 2026 09:46:34 +0200 Subject: [PATCH] fix: use standard odoo jsonrpc protocol --- .gitignore | 3 ++ README.md | 31 ++++++++++++++++---- TRAVAIL.md | 18 ++++++++++++ WAPT/control | 6 ++-- setup.py | 64 ++++++++++++++++++++++++++++++++++------- tests/test_ticketing.py | 31 ++++++++++++++++---- ticketing.ini.example | 2 ++ 7 files changed, 129 insertions(+), 26 deletions(-) diff --git a/.gitignore b/.gitignore index f955128..6bb1b3f 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,5 @@ /ticketing.ini /tickets_state.json +/WAPT/certificate.crt +/WAPT/manifest.sha256 +/WAPT/signature.sha256 diff --git a/README.md b/README.md index 9d21e0f..dfeaa77 100644 --- a/README.md +++ b/README.md @@ -46,6 +46,8 @@ webhook_url = CHANGE_ME_ROCKET_CHAT_WEBHOOK_URL [odoo] url = https://CHANGE_ME_ODOO_HOST +database = CHANGE_ME_ODOO_DATABASE +username = CHANGE_ME_ODOO_USERNAME api_key = CHANGE_ME_ODOO_API_KEY model = helpdesk.ticket @@ -82,24 +84,41 @@ 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. +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. + ## Protocole Odoo -Le script utilise l'endpoint configure sous la forme `/jsonrpc` avec un payload JSON-RPC direct : +Le script utilise l'endpoint configure sous la forme `/jsonrpc` avec le protocole JSON-RPC Odoo standard. + +Authentification : ```json { "jsonrpc": "2.0", "method": "call", "params": { - "model": "helpdesk.ticket", - "method": "search_read", - "args": [[], ["id", "write_date"]] + "service": "common", + "method": "authenticate", + "args": ["database", "username", "api_key", {}] }, "id": 1 } ``` -Il faut donc que l'instance Odoo expose bien un endpoint compatible avec ce format. Si l'instance attend le protocole externe Odoo standard `execute_kw`, il faudra adapter `fetch_odoo_tickets()`. +Lecture des tickets : + +```json +{ + "jsonrpc": "2.0", + "method": "call", + "params": { + "service": "object", + "method": "execute_kw", + "args": ["database", "uid", "api_key", "helpdesk.ticket", "search_read", [[]], {"fields": ["id", "write_date"]}] + }, + "id": 1 +} +``` ## Tests @@ -116,6 +135,6 @@ python3 -m unittest discover -s tests ## Prochaines ameliorations recommandees -1. Confirmer en environnement cible que l'endpoint Odoo accepte bien le payload JSON-RPC direct documente ci-dessus. +1. Confirmer en environnement cible que l'utilisateur Odoo configure a bien acces au modele `helpdesk.ticket`. 2. Completer les metadonnees `WAPT/control` selon les conventions internes Comitari. 3. Ajouter un test d'integration manuel ou automatise sur une instance Odoo de recette. diff --git a/TRAVAIL.md b/TRAVAIL.md index 41b5ab1..ef802e4 100644 --- a/TRAVAIL.md +++ b/TRAVAIL.md @@ -93,3 +93,21 @@ Ce fichier consigne les interventions Codex sur le projet afin de garder une tra - Ajout de tests avec mocks `requests` sur `fetch_odoo_tickets()` et `send_to_rocket()`. - Ajout d'un fichier `WAPT/control` minimal. - Documentation du payload JSON-RPC Odoo attendu dans `README.md`. +- Ajout des artefacts WAPT generes `certificate.crt`, `manifest.sha256` et `signature.sha256` dans `.gitignore`. + +### Correction protocole Odoo JSON-RPC + +- Analyse d'une erreur `RPC.jsonrpc() missing 1 required positional argument: 'service'` lors de `wapt-get audit`. +- Remplacement du payload direct `model/method/args` par le protocole JSON-RPC Odoo standard : + - `service=common`, `method=authenticate` pour obtenir le `uid` ; + - `service=object`, `method=execute_kw` pour appeler `helpdesk.ticket.search_read`. +- Ajout des champs `odoo.database` et `odoo.username` dans `ticketing.ini.example`. +- Support optionnel de `odoo.uid` pour eviter l'authentification si l'identifiant utilisateur est deja connu. +- Mise a jour des tests unitaires pour verifier les appels `authenticate` et `execute_kw`. + +### Ajustement WAPT/control + +- Passage de `target_os` de `windows` a `all` dans `WAPT/control`. +- Incrementation de la version WAPT de `0.1.0-2` a `0.1.0-3`. +- 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. diff --git a/WAPT/control b/WAPT/control index 9009038..0294308 100644 --- a/WAPT/control +++ b/WAPT/control @@ -1,5 +1,5 @@ package : comi-odoo-ticketing -version : 0.1.0-2 +version : 0.1.0-3 architecture : all section : base priority : optional @@ -11,7 +11,7 @@ depends : conflicts : maturity : PROD locale : all -target_os : windows +target_os : all min_wapt_version : sources : installed_size : @@ -41,4 +41,4 @@ signer : pcosson_key signer_fingerprint: a25582410cf03bad179a60c189f459a0b03821c92c0cedf209e82448a66a9b4e signature_date : 2026-07-24T07:19:02.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== \ No newline at end of file +signature : dBwE3rudjuJOvYhduNbzizfvql8Fqo13fs6anaHSoRoBCnRteqzJy4JRmaA6OV+YJOtfC89E7OnNBKAOvO1YErsaanKzFG+wkZFBZEgGkBcJZsb0xec5iLHoslEt6ZJw9ldsSs0dzRH1tzIb7nthMi5AOkPPqICKf5mXkYiOtAcBG9HjFa8YeYtBwyFM5ME2XeKkqpsESh03cOseNLpQ9DB2vMe0qZHd14nPZe5TpOFen+F4xklHSfemiMU8AFPclewTuhkYC8mqps2+WkHmTWl35ilJGg8BC2nLxiydotmOwc+QqpAYw90N6r9lVnPUFa1Bh2QqnmJnk5bfePYKTQ== diff --git a/setup.py b/setup.py index 0b87e30..7eca65f 100644 --- a/setup.py +++ b/setup.py @@ -52,9 +52,16 @@ def load_config(): required_options = [ ("odoo", "url"), + ("odoo", "database"), ("odoo", "api_key"), ("rocket", "webhook_url"), ] + if not ( + conf_wapt.has_option("odoo", "uid") + or conf_wapt.has_option("odoo", "username") + ): + required_options.append(("odoo", "username")) + missing_options = [ "%s.%s" % (section, option) for section, option in required_options @@ -85,31 +92,27 @@ def get_http_timeout(conf_wapt): return conf_wapt.getint("http", "timeout", fallback=DEFAULT_TIMEOUT) -def build_odoo_payload(conf_wapt): +def build_odoo_payload(service, method, args): return { "jsonrpc": "2.0", "method": "call", "params": { - "model": conf_wapt.get("odoo", "model", fallback="helpdesk.ticket"), - "method": "search_read", - "args": [[], ["id", "write_date"]], + "service": service, + "method": method, + "args": args, }, "id": 1, } -def fetch_odoo_tickets(conf_wapt): +def call_odoo_jsonrpc(conf_wapt, service, method, args): odoo_url = conf_wapt.get("odoo", "url").rstrip("/") endpoint = "%s/jsonrpc" % odoo_url - headers = { - "Content-Type": "application/json", - "Authorization": "Bearer %s" % conf_wapt.get("odoo", "api_key"), - } response = requests.post( endpoint, - json=build_odoo_payload(conf_wapt), - headers=headers, + json=build_odoo_payload(service, method, args), + headers={"Content-Type": "application/json"}, timeout=get_http_timeout(conf_wapt), ) response.raise_for_status() @@ -123,6 +126,45 @@ def fetch_odoo_tickets(conf_wapt): return response_data["result"] +def get_odoo_uid(conf_wapt): + if conf_wapt.has_option("odoo", "uid"): + return conf_wapt.getint("odoo", "uid") + + uid = call_odoo_jsonrpc( + conf_wapt, + "common", + "authenticate", + [ + conf_wapt.get("odoo", "database"), + conf_wapt.get("odoo", "username"), + conf_wapt.get("odoo", "api_key"), + {}, + ], + ) + if not uid: + raise Exception("Authentification Odoo impossible") + return uid + + +def fetch_odoo_tickets(conf_wapt): + uid = get_odoo_uid(conf_wapt) + + return call_odoo_jsonrpc( + conf_wapt, + "object", + "execute_kw", + [ + conf_wapt.get("odoo", "database"), + uid, + conf_wapt.get("odoo", "api_key"), + conf_wapt.get("odoo", "model", fallback="helpdesk.ticket"), + "search_read", + [[]], + {"fields": ["id", "write_date"]}, + ], + ) + + def load_tickets_state(state_path): if not os.path.exists(state_path): return {} diff --git a/tests/test_ticketing.py b/tests/test_ticketing.py index 350536d..40ee802 100644 --- a/tests/test_ticketing.py +++ b/tests/test_ticketing.py @@ -33,6 +33,8 @@ def make_config(): config = ConfigParser() config.add_section("odoo") config.set("odoo", "url", "https://odoo.example.test") + config.set("odoo", "database", "test-db") + config.set("odoo", "username", "test@example.test") config.set("odoo", "api_key", "test-api-key") config.set("odoo", "model", "helpdesk.ticket") config.add_section("rocket") @@ -82,19 +84,24 @@ class TicketingStateTest(unittest.TestCase): self.assertEqual(ticketing.load_tickets_state(state_path), expected_state) @patch("ticketing_setup.requests.post") - def test_fetch_odoo_tickets_uses_timeout_and_returns_result(self, post): - response = Mock() - response.json.return_value = { + def test_fetch_odoo_tickets_authenticates_and_returns_result(self, post): + auth_response = Mock() + auth_response.json.return_value = {"result": 7} + tickets_response = Mock() + tickets_response.json.return_value = { "result": [{"id": 42, "write_date": "2026-07-24 12:00:00"}], } - post.return_value = response + post.side_effect = [auth_response, tickets_response] tickets = ticketing.fetch_odoo_tickets(make_config()) self.assertEqual(tickets, [{"id": 42, "write_date": "2026-07-24 12:00:00"}]) - response.raise_for_status.assert_called_once_with() - post.assert_called_once() + auth_response.raise_for_status.assert_called_once_with() + tickets_response.raise_for_status.assert_called_once_with() + self.assertEqual(post.call_count, 2) 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") @patch("ticketing_setup.requests.post") def test_fetch_odoo_tickets_rejects_jsonrpc_error(self, post): @@ -105,6 +112,18 @@ class TicketingStateTest(unittest.TestCase): with self.assertRaisesRegex(Exception, "Erreur Odoo JSON-RPC"): ticketing.fetch_odoo_tickets(make_config()) + @patch("ticketing_setup.requests.post") + def test_fetch_odoo_tickets_can_use_configured_uid(self, post): + config = make_config() + config.set("odoo", "uid", "12") + response = Mock() + response.json.return_value = {"result": []} + post.return_value = response + + self.assertEqual(ticketing.fetch_odoo_tickets(config), []) + self.assertEqual(post.call_count, 1) + self.assertEqual(post.call_args.kwargs["json"]["params"]["args"][1], 12) + @patch("ticketing_setup.requests.post") def test_send_to_rocket_raises_for_http_errors(self, post): response = Mock() diff --git a/ticketing.ini.example b/ticketing.ini.example index e057865..4295748 100644 --- a/ticketing.ini.example +++ b/ticketing.ini.example @@ -3,6 +3,8 @@ webhook_url = CHANGE_ME_ROCKET_CHAT_WEBHOOK_URL [odoo] url = https://CHANGE_ME_ODOO_HOST +database = CHANGE_ME_ODOO_DATABASE +username = CHANGE_ME_ODOO_USERNAME api_key = CHANGE_ME_ODOO_API_KEY model = helpdesk.ticket