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
/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]
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 `<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
{
"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.
+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 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.
+3 -3
View File
@@ -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==
signature : dBwE3rudjuJOvYhduNbzizfvql8Fqo13fs6anaHSoRoBCnRteqzJy4JRmaA6OV+YJOtfC89E7OnNBKAOvO1YErsaanKzFG+wkZFBZEgGkBcJZsb0xec5iLHoslEt6ZJw9ldsSs0dzRH1tzIb7nthMi5AOkPPqICKf5mXkYiOtAcBG9HjFa8YeYtBwyFM5ME2XeKkqpsESh03cOseNLpQ9DB2vMe0qZHd14nPZe5TpOFen+F4xklHSfemiMU8AFPclewTuhkYC8mqps2+WkHmTWl35ilJGg8BC2nLxiydotmOwc+QqpAYw90N6r9lVnPUFa1Bh2QqnmJnk5bfePYKTQ==
+53 -11
View File
@@ -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 {}
+25 -6
View File
@@ -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()
+2
View File
@@ -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