fix: use standard odoo jsonrpc protocol

This commit is contained in:
2026-07-24 09:46:34 +02:00
parent 7b04a1e803
commit adedf56b7b
7 changed files with 129 additions and 26 deletions
+3
View File
@@ -1,2 +1,5 @@
/ticketing.ini /ticketing.ini
/tickets_state.json /tickets_state.json
/WAPT/certificate.crt
/WAPT/manifest.sha256
/WAPT/signature.sha256
+25 -6
View File
@@ -46,6 +46,8 @@ webhook_url = CHANGE_ME_ROCKET_CHAT_WEBHOOK_URL
[odoo] [odoo]
url = https://CHANGE_ME_ODOO_HOST url = https://CHANGE_ME_ODOO_HOST
database = CHANGE_ME_ODOO_DATABASE
username = CHANGE_ME_ODOO_USERNAME
api_key = CHANGE_ME_ODOO_API_KEY api_key = CHANGE_ME_ODOO_API_KEY
model = helpdesk.ticket 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. 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 ## Protocole Odoo
Le script utilise l'endpoint configure sous la forme `<odoo.url>/jsonrpc` avec un payload JSON-RPC direct : Le script utilise l'endpoint configure sous la forme `<odoo.url>/jsonrpc` avec le protocole JSON-RPC Odoo standard.
Authentification :
```json ```json
{ {
"jsonrpc": "2.0", "jsonrpc": "2.0",
"method": "call", "method": "call",
"params": { "params": {
"model": "helpdesk.ticket", "service": "common",
"method": "search_read", "method": "authenticate",
"args": [[], ["id", "write_date"]] "args": ["database", "username", "api_key", {}]
}, },
"id": 1 "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 ## Tests
@@ -116,6 +135,6 @@ python3 -m unittest discover -s tests
## Prochaines ameliorations recommandees ## 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. 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. 3. Ajouter un test d'integration manuel ou automatise sur une instance Odoo de recette.
+18
View File
@@ -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 de tests avec mocks `requests` sur `fetch_odoo_tickets()` et `send_to_rocket()`.
- Ajout d'un fichier `WAPT/control` minimal. - Ajout d'un fichier `WAPT/control` minimal.
- Documentation du payload JSON-RPC Odoo attendu dans `README.md`. - 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.
+3 -3
View File
@@ -1,5 +1,5 @@
package : comi-odoo-ticketing package : comi-odoo-ticketing
version : 0.1.0-2 version : 0.1.0-3
architecture : all architecture : all
section : base section : base
priority : optional priority : optional
@@ -11,7 +11,7 @@ depends :
conflicts : conflicts :
maturity : PROD maturity : PROD
locale : all locale : all
target_os : windows target_os : all
min_wapt_version : min_wapt_version :
sources : sources :
installed_size : installed_size :
@@ -41,4 +41,4 @@ signer : pcosson_key
signer_fingerprint: a25582410cf03bad179a60c189f459a0b03821c92c0cedf209e82448a66a9b4e signer_fingerprint: a25582410cf03bad179a60c189f459a0b03821c92c0cedf209e82448a66a9b4e
signature_date : 2026-07-24T07:19:02.000000 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 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 : dBwE3rudjuJOvYhduNbzizfvql8Fqo13fs6anaHSoRoBCnRteqzJy4JRmaA6OV+YJOtfC89E7OnNBKAOvO1YErsaanKzFG+wkZFBZEgGkBcJZsb0xec5iLHoslEt6ZJw9ldsSs0dzRH1tzIb7nthMi5AOkPPqICKf5mXkYiOtAcBG9HjFa8YeYtBwyFM5ME2XeKkqpsESh03cOseNLpQ9DB2vMe0qZHd14nPZe5TpOFen+F4xklHSfemiMU8AFPclewTuhkYC8mqps2+WkHmTWl35ilJGg8BC2nLxiydotmOwc+QqpAYw90N6r9lVnPUFa1Bh2QqnmJnk5bfePYKTQ==
+53 -11
View File
@@ -52,9 +52,16 @@ def load_config():
required_options = [ required_options = [
("odoo", "url"), ("odoo", "url"),
("odoo", "database"),
("odoo", "api_key"), ("odoo", "api_key"),
("rocket", "webhook_url"), ("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 = [ missing_options = [
"%s.%s" % (section, option) "%s.%s" % (section, option)
for section, option in required_options 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) return conf_wapt.getint("http", "timeout", fallback=DEFAULT_TIMEOUT)
def build_odoo_payload(conf_wapt): def build_odoo_payload(service, method, args):
return { return {
"jsonrpc": "2.0", "jsonrpc": "2.0",
"method": "call", "method": "call",
"params": { "params": {
"model": conf_wapt.get("odoo", "model", fallback="helpdesk.ticket"), "service": service,
"method": "search_read", "method": method,
"args": [[], ["id", "write_date"]], "args": args,
}, },
"id": 1, "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("/") odoo_url = conf_wapt.get("odoo", "url").rstrip("/")
endpoint = "%s/jsonrpc" % odoo_url endpoint = "%s/jsonrpc" % odoo_url
headers = {
"Content-Type": "application/json",
"Authorization": "Bearer %s" % conf_wapt.get("odoo", "api_key"),
}
response = requests.post( response = requests.post(
endpoint, endpoint,
json=build_odoo_payload(conf_wapt), json=build_odoo_payload(service, method, args),
headers=headers, headers={"Content-Type": "application/json"},
timeout=get_http_timeout(conf_wapt), timeout=get_http_timeout(conf_wapt),
) )
response.raise_for_status() response.raise_for_status()
@@ -123,6 +126,45 @@ def fetch_odoo_tickets(conf_wapt):
return response_data["result"] 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): def load_tickets_state(state_path):
if not os.path.exists(state_path): if not os.path.exists(state_path):
return {} return {}
+25 -6
View File
@@ -33,6 +33,8 @@ def make_config():
config = ConfigParser() config = ConfigParser()
config.add_section("odoo") config.add_section("odoo")
config.set("odoo", "url", "https://odoo.example.test") 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", "api_key", "test-api-key")
config.set("odoo", "model", "helpdesk.ticket") config.set("odoo", "model", "helpdesk.ticket")
config.add_section("rocket") config.add_section("rocket")
@@ -82,19 +84,24 @@ class TicketingStateTest(unittest.TestCase):
self.assertEqual(ticketing.load_tickets_state(state_path), expected_state) self.assertEqual(ticketing.load_tickets_state(state_path), expected_state)
@patch("ticketing_setup.requests.post") @patch("ticketing_setup.requests.post")
def test_fetch_odoo_tickets_uses_timeout_and_returns_result(self, post): def test_fetch_odoo_tickets_authenticates_and_returns_result(self, post):
response = Mock() auth_response = Mock()
response.json.return_value = { 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"}], "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()) tickets = ticketing.fetch_odoo_tickets(make_config())
self.assertEqual(tickets, [{"id": 42, "write_date": "2026-07-24 12:00:00"}]) self.assertEqual(tickets, [{"id": 42, "write_date": "2026-07-24 12:00:00"}])
response.raise_for_status.assert_called_once_with() auth_response.raise_for_status.assert_called_once_with()
post.assert_called_once() 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["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") @patch("ticketing_setup.requests.post")
def test_fetch_odoo_tickets_rejects_jsonrpc_error(self, 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"): with self.assertRaisesRegex(Exception, "Erreur Odoo JSON-RPC"):
ticketing.fetch_odoo_tickets(make_config()) 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") @patch("ticketing_setup.requests.post")
def test_send_to_rocket_raises_for_http_errors(self, post): def test_send_to_rocket_raises_for_http_errors(self, post):
response = Mock() response = Mock()
+2
View File
@@ -3,6 +3,8 @@ webhook_url = CHANGE_ME_ROCKET_CHAT_WEBHOOK_URL
[odoo] [odoo]
url = https://CHANGE_ME_ODOO_HOST url = https://CHANGE_ME_ODOO_HOST
database = CHANGE_ME_ODOO_DATABASE
username = CHANGE_ME_ODOO_USERNAME
api_key = CHANGE_ME_ODOO_API_KEY api_key = CHANGE_ME_ODOO_API_KEY
model = helpdesk.ticket model = helpdesk.ticket