fix: use standard odoo jsonrpc protocol
This commit is contained in:
@@ -1,2 +1,5 @@
|
|||||||
/ticketing.ini
|
/ticketing.ini
|
||||||
/tickets_state.json
|
/tickets_state.json
|
||||||
|
/WAPT/certificate.crt
|
||||||
|
/WAPT/manifest.sha256
|
||||||
|
/WAPT/signature.sha256
|
||||||
|
|||||||
@@ -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
@@ -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.
|
||||||
|
|||||||
+2
-2
@@ -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 :
|
||||||
|
|||||||
@@ -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
@@ -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()
|
||||||
|
|||||||
@@ -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
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user