chore: secure ticketing configuration
This commit is contained in:
@@ -0,0 +1,2 @@
|
||||
/ticketing.ini
|
||||
/tickets_state.json
|
||||
@@ -0,0 +1,121 @@
|
||||
# comi-odoo-ticketing
|
||||
|
||||
Script WAPT de surveillance des tickets Odoo Helpdesk Comitari.
|
||||
|
||||
Le projet interroge Odoo via JSON-RPC, compare la date de modification des tickets avec un etat local JSON, puis envoie un message Rocket.Chat via webhook quand des tickets ont change depuis la derniere verification.
|
||||
|
||||
## Contenu du depot
|
||||
|
||||
| Fichier | Role |
|
||||
| --- | --- |
|
||||
| `setup.py` | Script WAPT principal. Contient les fonctions `install()`, `audit()` et `send_to_rocket()`. |
|
||||
| `WAPT/control` | Metadata minimale du paquet WAPT. |
|
||||
| `ticketing.ini.example` | Modele de configuration avec placeholders, copie dans le repertoire prive WAPT sous le nom `ticketing.ini`. |
|
||||
| `tests/test_ticketing.py` | Tests unitaires de la logique de comparaison et de persistance d'etat. |
|
||||
| `.env` | Variables d'environnement locales pour pointer vers l'installation Python/WAPT Windows. |
|
||||
| `TRAVAIL.md` | Journal de travail maintenu par Codex. |
|
||||
|
||||
## Fonctionnement
|
||||
|
||||
1. `install()` copie `ticketing.ini.example` vers `WAPT.private_dir/ticketing.ini` si le fichier cible n'existe pas encore.
|
||||
2. Le fichier copie doit etre renseigne sur la machine cible avec l'URL Odoo, la cle API et le webhook Rocket.Chat.
|
||||
3. `audit()` appelle l'endpoint JSON-RPC Odoo configure dans `ticketing.ini`.
|
||||
4. La methode Odoo appelee est `helpdesk.ticket.search_read`, avec les champs `id` et `write_date`.
|
||||
5. Le script charge l'ancien etat depuis `tickets_state.json`.
|
||||
6. Chaque ticket nouveau ou dont `write_date` a change est ajoute a la liste des tickets mis a jour.
|
||||
7. Un message est envoye dans Rocket.Chat via le webhook configure dans le fichier INI prive.
|
||||
8. Le nouvel etat est sauvegarde dans `WAPT.private_dir/tickets_state.json`.
|
||||
|
||||
## Prerequis
|
||||
|
||||
- Environnement WAPT avec `setuphelpers`.
|
||||
- Python compatible avec l'environnement WAPT cible.
|
||||
- Acces reseau vers Odoo Comitari.
|
||||
- Acces reseau vers Rocket.Chat Comitari.
|
||||
- Dependances Python :
|
||||
- `requests`
|
||||
- modules standard : `json`, `os`, `shutil`, `configparser`
|
||||
|
||||
## Configuration
|
||||
|
||||
La configuration sensible est centralisee dans la copie privee `WAPT.private_dir/ticketing.ini`. Le depot ne versionne que `ticketing.ini.example`, avec des placeholders :
|
||||
|
||||
```ini
|
||||
[rocket]
|
||||
webhook_url = CHANGE_ME_ROCKET_CHAT_WEBHOOK_URL
|
||||
|
||||
[odoo]
|
||||
url = https://CHANGE_ME_ODOO_HOST
|
||||
api_key = CHANGE_ME_ODOO_API_KEY
|
||||
model = helpdesk.ticket
|
||||
|
||||
[http]
|
||||
timeout = 30
|
||||
```
|
||||
|
||||
Au deploiement, `install()` copie ce modele vers `WAPT.private_dir/ticketing.ini` uniquement si le fichier n'existe pas deja. Il faut ensuite renseigner le fichier prive sur la machine cible. Les mises a jour du paquet n'ecrasent donc pas une configuration deja remplie.
|
||||
|
||||
Si une valeur obligatoire est absente ou conserve un placeholder `CHANGE_ME...`, l'audit echoue explicitement avec un message indiquant la cle a renseigner.
|
||||
|
||||
Important : ne pas renseigner de vrais secrets dans `ticketing.ini.example`. Le fichier local `ticketing.ini` est ignore par Git.
|
||||
|
||||
## Execution
|
||||
|
||||
Dans un paquet WAPT, l'execution attendue passe par les hooks WAPT :
|
||||
|
||||
```python
|
||||
install()
|
||||
audit()
|
||||
```
|
||||
|
||||
Hors WAPT, le script ne peut pas etre lance tel quel sans fournir les objets et chemins attendus par `setuphelpers`, notamment `WAPT.private_dir`.
|
||||
|
||||
## Etat local
|
||||
|
||||
`tickets_state.json` est stocke dans `WAPT.private_dir` et contient un dictionnaire JSON au format :
|
||||
|
||||
```json
|
||||
{
|
||||
"ticket_id": "write_date"
|
||||
}
|
||||
```
|
||||
|
||||
Le runtime lit et ecrit ce fichier dans `WAPT.private_dir`. Il n'est pas versionne dans le depot.
|
||||
|
||||
## Protocole Odoo
|
||||
|
||||
Le script utilise l'endpoint configure sous la forme `<odoo.url>/jsonrpc` avec un payload JSON-RPC direct :
|
||||
|
||||
```json
|
||||
{
|
||||
"jsonrpc": "2.0",
|
||||
"method": "call",
|
||||
"params": {
|
||||
"model": "helpdesk.ticket",
|
||||
"method": "search_read",
|
||||
"args": [[], ["id", "write_date"]]
|
||||
},
|
||||
"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()`.
|
||||
|
||||
## Tests
|
||||
|
||||
Les tests unitaires peuvent etre executes hors WAPT :
|
||||
|
||||
```bash
|
||||
python3 -m unittest discover -s tests
|
||||
```
|
||||
|
||||
## Points d'attention connus
|
||||
|
||||
- Les secrets doivent etre renseignes uniquement dans la copie privee `WAPT.private_dir/ticketing.ini`.
|
||||
- Les secrets deja presents dans l'historique Git doivent etre regeneres cote Odoo et Rocket.Chat.
|
||||
|
||||
## Prochaines ameliorations recommandees
|
||||
|
||||
1. Confirmer en environnement cible que l'endpoint Odoo accepte bien le payload JSON-RPC direct documente ci-dessus.
|
||||
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.
|
||||
+95
@@ -0,0 +1,95 @@
|
||||
# Journal de travail
|
||||
|
||||
Ce fichier consigne les interventions Codex sur le projet afin de garder une trace exploitable des analyses, decisions et suites a traiter.
|
||||
|
||||
## 2026-06-24
|
||||
|
||||
### Demande
|
||||
|
||||
- Analyser le projet.
|
||||
- Creer un README et le maintenir a jour.
|
||||
- Alimenter un fichier Markdown dedie au travail effectue.
|
||||
|
||||
### Analyse realisee
|
||||
|
||||
- Le depot contient un script WAPT principal `setup.py`.
|
||||
- Le script surveille les tickets Odoo Helpdesk via JSON-RPC.
|
||||
- Il compare les champs `id` et `write_date` avec un etat local `tickets_state.pkl`.
|
||||
- Il notifie Rocket.Chat via un webhook configure dans `rocket.ini`.
|
||||
- Le fichier `tickets_state.pkl` initial a ete inspecte avec `pickletools` sans charger le pickle comme code executable ; il contient actuellement un dictionnaire vide.
|
||||
- `.env` contient des chemins Windows vers l'environnement WAPT.
|
||||
|
||||
### Fichiers ajoutes
|
||||
|
||||
- `README.md` : documentation du projet, fonctionnement, prerequis, configuration, risques et ameliorations recommandees.
|
||||
- `TRAVAIL.md` : journal de travail maintenu par Codex.
|
||||
|
||||
### Points techniques identifies
|
||||
|
||||
- Bug bloquant probable dans `setup.py` : `tickets_updated.auppend(ticket_id)` doit etre corrige en `tickets_updated.append(ticket_id)`.
|
||||
- Secrets presents en clair :
|
||||
- cle API Odoo dans `setup.py` ;
|
||||
- webhook Rocket.Chat dans `rocket.ini`.
|
||||
- `pickle.load()` est sensible si le fichier d'etat peut etre modifie par une source non fiable.
|
||||
- `db` et `xmlrpc.client` semblent inutilises.
|
||||
- Le chemin de `tickets_state.pkl` est relatif au repertoire courant.
|
||||
|
||||
### Suites recommandees
|
||||
|
||||
1. Corriger la faute `auppend`.
|
||||
2. Sortir les secrets du depot.
|
||||
3. Remplacer l'etat pickle par JSON.
|
||||
4. Ajouter un test minimal sur la detection des tickets modifies.
|
||||
5. Verifier la conformite du payload JSON-RPC avec l'API Odoo reellement exposee.
|
||||
|
||||
### Modification configuration secrete
|
||||
|
||||
- Retrait de la cle API Odoo codee en dur dans `setup.py`.
|
||||
- Remplacement du webhook reel par des placeholders.
|
||||
- Centralisation de la configuration sensible dans `ticketing.ini.example`, copiee en prive sous le nom `ticketing.ini` :
|
||||
- `rocket.webhook_url`
|
||||
- `odoo.url`
|
||||
- `odoo.api_key`
|
||||
- `odoo.model`
|
||||
- `install()` copie le fichier INI modele vers `WAPT.private_dir/ticketing.ini` uniquement s'il n'existe pas deja, afin de ne pas ecraser une configuration renseignee sur la machine cible.
|
||||
- `audit()` charge maintenant l'URL Odoo et la cle API depuis le fichier INI prive.
|
||||
- `send_to_rocket()` charge maintenant le webhook depuis le fichier INI prive.
|
||||
- Correction du bug `tickets_updated.auppend(ticket_id)` en `tickets_updated.append(ticket_id)`.
|
||||
- Deplacement du fichier d'etat vers `WAPT.private_dir/tickets_state.pkl` pour eviter de dependre du repertoire courant.
|
||||
- Ajout d'une validation explicite des placeholders `CHANGE_ME...` pour eviter une execution avec une configuration non renseignee.
|
||||
- Ajout de `.gitignore` pour eviter de versionner un `ticketing.ini` rempli localement et un futur fichier d'etat runtime.
|
||||
|
||||
## 2026-07-24
|
||||
|
||||
### Analyse des ameliorations possibles
|
||||
|
||||
- Remplacer `pickle` par JSON pour le fichier d'etat runtime.
|
||||
- Extraire la recuperation Odoo, la comparaison d'etat et l'envoi Rocket.Chat dans des fonctions testables.
|
||||
- Ajouter des timeouts explicites aux appels HTTP Odoo et Rocket.Chat.
|
||||
- Faire echouer l'audit si l'appel Odoo echoue, afin d'eviter d'ecraser l'etat avec une liste vide en cas d'indisponibilite.
|
||||
- Utiliser `response.raise_for_status()` aussi pour l'appel Rocket.Chat.
|
||||
- Ajouter des tests unitaires sur la detection des tickets nouveaux ou modifies.
|
||||
- Retirer du depot le reliquat `tickets_state.pkl`, aujourd'hui remplace par l'etat stocke dans `WAPT.private_dir`.
|
||||
- Ajouter un fichier de controle WAPT si le depot doit etre un paquet complet.
|
||||
- Mettre en place une rotation des secrets deja presents dans l'historique Git.
|
||||
|
||||
### Mise en oeuvre des ameliorations
|
||||
|
||||
- Remplacement de l'etat runtime `tickets_state.pkl` par `tickets_state.json`.
|
||||
- Suppression de l'utilisation de `pickle` dans le code.
|
||||
- Ajout des fonctions testables :
|
||||
- `build_odoo_payload()`
|
||||
- `fetch_odoo_tickets()`
|
||||
- `load_tickets_state()`
|
||||
- `save_tickets_state()`
|
||||
- `build_tickets_state()`
|
||||
- `get_updated_ticket_ids()`
|
||||
- Ajout d'un timeout HTTP configurable via `http.timeout`, avec valeur par defaut a 30 secondes.
|
||||
- Durcissement de l'appel Odoo : les erreurs HTTP, les erreurs JSON-RPC et les reponses sans `result` font echouer l'audit au lieu de sauvegarder un etat vide.
|
||||
- Durcissement de l'appel Rocket.Chat avec `raise_for_status()`.
|
||||
- Ajout de `tests/test_ticketing.py` pour valider la comparaison des tickets et la lecture/ecriture JSON.
|
||||
- Suppression du reliquat `tickets_state.pkl` du repertoire projet.
|
||||
- Mise a jour de `README.md`, `ticketing.ini.example` et `.gitignore`.
|
||||
- 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`.
|
||||
@@ -0,0 +1,44 @@
|
||||
package : comi-odoo-ticketing
|
||||
version : 0.1.0-2
|
||||
architecture : all
|
||||
section : base
|
||||
priority : optional
|
||||
name : Comitari Odoo ticketing audit
|
||||
categories : Utilities
|
||||
maintainer : Comitari
|
||||
description : Surveille les tickets Odoo Helpdesk et notifie Rocket.Chat via webhook.
|
||||
depends :
|
||||
conflicts :
|
||||
maturity : PROD
|
||||
locale : all
|
||||
target_os : windows
|
||||
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 : 0afd3de2-c939-918b-67b0-d79a8ccba84b
|
||||
valid_from :
|
||||
valid_until :
|
||||
forced_install_on :
|
||||
changelog :
|
||||
min_os_version :
|
||||
max_os_version :
|
||||
icon_sha256sum :
|
||||
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==
|
||||
@@ -1,2 +0,0 @@
|
||||
[rocket]
|
||||
url= https://chat.comitari.fr/hooks/64d4d02760b38508f62a5bcb/ncKSYRiLM9oNXagK5c7G3KWX2qEzET3kbFFXKnNAhtfZQEQ9
|
||||
@@ -1,125 +1,183 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
from setuphelpers import *
|
||||
import xmlrpc.client
|
||||
import pickle
|
||||
import os
|
||||
from configparser import ConfigParser
|
||||
import requests
|
||||
import json
|
||||
import os
|
||||
import shutil
|
||||
from configparser import ConfigParser
|
||||
|
||||
# Configuration
|
||||
url = 'https://odoo.comitari.fr'
|
||||
db = 'comitari'
|
||||
state_file = 'tickets_state.pkl'
|
||||
api_key = "182534f9e754ce1016e86b86b4b5b47199659a6f"
|
||||
headers = {
|
||||
'Content-Type': 'application/json',
|
||||
'Authorization': f'Bearer {api_key}',
|
||||
}
|
||||
import requests
|
||||
|
||||
# Endpoint URL
|
||||
endpoint = f'{url}/jsonrpc'
|
||||
|
||||
# Payload pour récupérer tous les tickets
|
||||
payload = {
|
||||
'jsonrpc': '2.0',
|
||||
'method': 'call',
|
||||
'params': {
|
||||
'model': 'helpdesk.ticket',
|
||||
'method': 'search_read',
|
||||
'args': [[], ['id', 'write_date']],
|
||||
},
|
||||
'id': 1,
|
||||
}
|
||||
CONFIG_FILE = "ticketing.ini"
|
||||
CONFIG_TEMPLATE_FILE = "ticketing.ini.example"
|
||||
STATE_FILE = "tickets_state.json"
|
||||
DEFAULT_TIMEOUT = 30
|
||||
|
||||
|
||||
def get_config_path():
|
||||
return makepath(WAPT.private_dir, CONFIG_FILE)
|
||||
|
||||
|
||||
def get_state_path():
|
||||
return makepath(WAPT.private_dir, STATE_FILE)
|
||||
|
||||
|
||||
def install():
|
||||
config_path = get_config_path()
|
||||
|
||||
plugin_inifile = makepath(WAPT.private_dir, "rocket.ini")
|
||||
if not isfile(config_path):
|
||||
shutil.copyfile(CONFIG_TEMPLATE_FILE, config_path)
|
||||
|
||||
if not isfile(plugin_inifile):
|
||||
filecopyto("rocket.ini", WAPT.private_dir)
|
||||
|
||||
def audit():
|
||||
|
||||
try:
|
||||
# Récupération de l'état actuel de tous les tickets
|
||||
response = requests.post(endpoint, json=payload, headers=headers)
|
||||
response.raise_for_status() # Vérifie si la requête a réussi
|
||||
response_data = response.json()
|
||||
|
||||
# Vérifie si 'result' est présent dans la réponse
|
||||
if 'result' in response_data:
|
||||
tickets = response_data['result']
|
||||
else:
|
||||
print("Erreur: La clé 'result' n'est pas présente dans la réponse.")
|
||||
print("Réponse complète:", response_data)
|
||||
tickets = []
|
||||
except requests.exceptions.RequestException as e:
|
||||
print(f"Erreur lors de la requête: {e}")
|
||||
tickets = []
|
||||
|
||||
# Vérification si un état précédent existe
|
||||
if os.path.exists(state_file):
|
||||
with open(state_file, 'rb') as f:
|
||||
previous_tickets_state = pickle.load(f)
|
||||
else:
|
||||
previous_tickets_state = {}
|
||||
|
||||
|
||||
# Comparaison et affichage des résultats
|
||||
tickets_updated = []
|
||||
for ticket in tickets:
|
||||
ticket_id = ticket['id']
|
||||
ticket_write_date = ticket['write_date']
|
||||
|
||||
if ticket_id in previous_tickets_state:
|
||||
if ticket_write_date != previous_tickets_state[ticket_id]:
|
||||
tickets_updated.auppend(ticket_id)
|
||||
else:
|
||||
tickets_updated.append(ticket_id)
|
||||
conf_wapt = load_config()
|
||||
tickets = fetch_odoo_tickets(conf_wapt)
|
||||
previous_tickets_state = load_tickets_state(get_state_path())
|
||||
tickets_updated = get_updated_ticket_ids(tickets, previous_tickets_state)
|
||||
|
||||
if tickets_updated:
|
||||
message = f"Les tickets suivants ont été mis à jour depuis la dernière vérification : {tickets_updated}"
|
||||
send_to_rocket(message)
|
||||
|
||||
message = "Les tickets suivants ont ete mis a jour depuis la derniere verification : %s" % tickets_updated
|
||||
else:
|
||||
message = "Aucun ticket n'a été mis à jour depuis la dernière vérification."
|
||||
send_to_rocket(message)
|
||||
# Sauvegarde de l'état actuel pour la prochaine exécution
|
||||
current_tickets_state = {ticket['id']: ticket['write_date'] for ticket in tickets}
|
||||
with open(state_file, 'wb') as f:
|
||||
pickle.dump(current_tickets_state, f)
|
||||
message = "Aucun ticket n'a ete mis a jour depuis la derniere verification."
|
||||
|
||||
send_to_rocket(message, conf_wapt=conf_wapt)
|
||||
save_tickets_state(get_state_path(), build_tickets_state(tickets))
|
||||
return "OK"
|
||||
|
||||
|
||||
def send_to_rocket(message_text, attachments=None):
|
||||
"""
|
||||
Envoie un message à Rocket.Chat via un webhook.
|
||||
|
||||
:param message_text: Texte du message à envoyer
|
||||
:param attachments: Liste de pièces jointes (facultatif)
|
||||
"""
|
||||
smtp_inifile = makepath(WAPT.private_dir, "rocket.ini")
|
||||
def load_config():
|
||||
config_path = get_config_path()
|
||||
conf_wapt = ConfigParser()
|
||||
conf_wapt.read(smtp_inifile)
|
||||
conf_wapt.read(config_path)
|
||||
|
||||
webhook_url = conf_wapt.get("rocket", "url")
|
||||
required_options = [
|
||||
("odoo", "url"),
|
||||
("odoo", "api_key"),
|
||||
("rocket", "webhook_url"),
|
||||
]
|
||||
missing_options = [
|
||||
"%s.%s" % (section, option)
|
||||
for section, option in required_options
|
||||
if not conf_wapt.has_option(section, option)
|
||||
]
|
||||
|
||||
# Construire le message
|
||||
if missing_options:
|
||||
raise Exception(
|
||||
"Configuration incomplete dans %s : %s"
|
||||
% (config_path, ", ".join(missing_options))
|
||||
)
|
||||
|
||||
placeholder_options = [
|
||||
"%s.%s" % (section, option)
|
||||
for section, option in required_options
|
||||
if "CHANGE_ME" in conf_wapt.get(section, option).strip()
|
||||
]
|
||||
if placeholder_options:
|
||||
raise Exception(
|
||||
"Configuration a renseigner dans %s : %s"
|
||||
% (config_path, ", ".join(placeholder_options))
|
||||
)
|
||||
|
||||
return conf_wapt
|
||||
|
||||
|
||||
def get_http_timeout(conf_wapt):
|
||||
return conf_wapt.getint("http", "timeout", fallback=DEFAULT_TIMEOUT)
|
||||
|
||||
|
||||
def build_odoo_payload(conf_wapt):
|
||||
return {
|
||||
"jsonrpc": "2.0",
|
||||
"method": "call",
|
||||
"params": {
|
||||
"model": conf_wapt.get("odoo", "model", fallback="helpdesk.ticket"),
|
||||
"method": "search_read",
|
||||
"args": [[], ["id", "write_date"]],
|
||||
},
|
||||
"id": 1,
|
||||
}
|
||||
|
||||
|
||||
def fetch_odoo_tickets(conf_wapt):
|
||||
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,
|
||||
timeout=get_http_timeout(conf_wapt),
|
||||
)
|
||||
response.raise_for_status()
|
||||
response_data = response.json()
|
||||
|
||||
if "error" in response_data:
|
||||
raise Exception("Erreur Odoo JSON-RPC : %s" % response_data["error"])
|
||||
if "result" not in response_data:
|
||||
raise Exception("Reponse Odoo invalide : cle 'result' absente")
|
||||
|
||||
return response_data["result"]
|
||||
|
||||
|
||||
def load_tickets_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_tickets_state(state_path, tickets_state):
|
||||
with open(state_path, "w") as state_file:
|
||||
json.dump(tickets_state, state_file, indent=2, sort_keys=True)
|
||||
|
||||
|
||||
def build_tickets_state(tickets):
|
||||
return {
|
||||
str(ticket["id"]): ticket["write_date"]
|
||||
for ticket in tickets
|
||||
}
|
||||
|
||||
|
||||
def get_updated_ticket_ids(tickets, previous_tickets_state):
|
||||
tickets_updated = []
|
||||
|
||||
for ticket in tickets:
|
||||
ticket_id = str(ticket["id"])
|
||||
ticket_write_date = ticket["write_date"]
|
||||
|
||||
if previous_tickets_state.get(ticket_id) != ticket_write_date:
|
||||
tickets_updated.append(ticket["id"])
|
||||
|
||||
return tickets_updated
|
||||
|
||||
|
||||
def send_to_rocket(message_text, attachments=None, conf_wapt=None):
|
||||
"""
|
||||
Envoie un message a Rocket.Chat via un webhook.
|
||||
|
||||
:param message_text: Texte du message a envoyer
|
||||
:param attachments: Liste de pieces jointes (facultatif)
|
||||
:param conf_wapt: Configuration deja chargee (facultatif)
|
||||
"""
|
||||
if conf_wapt is None:
|
||||
conf_wapt = load_config()
|
||||
|
||||
message = {
|
||||
'text': message_text
|
||||
"text": message_text,
|
||||
}
|
||||
if attachments:
|
||||
message['attachments'] = attachments
|
||||
|
||||
# Envoyer la requête POST
|
||||
response = requests.post(webhook_url, data=json.dumps(message), headers={'Content-Type': 'application/json'})
|
||||
|
||||
# Vérifier la réponse
|
||||
if response.status_code == 200:
|
||||
print('Message envoyé avec succès.')
|
||||
else:
|
||||
print(f'Échec de l\'envoi du message. Statut de la réponse : {response.status_code}')
|
||||
print(f'Erreur : {response.text}')
|
||||
message["attachments"] = attachments
|
||||
|
||||
response = requests.post(
|
||||
conf_wapt.get("rocket", "webhook_url"),
|
||||
json=message,
|
||||
headers={"Content-Type": "application/json"},
|
||||
timeout=get_http_timeout(conf_wapt),
|
||||
)
|
||||
response.raise_for_status()
|
||||
print("Message envoye avec succes.")
|
||||
|
||||
@@ -0,0 +1,121 @@
|
||||
import importlib.util
|
||||
import os
|
||||
import sys
|
||||
import tempfile
|
||||
import types
|
||||
import unittest
|
||||
from configparser import ConfigParser
|
||||
from unittest.mock import Mock, patch
|
||||
|
||||
|
||||
PROJECT_ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||||
SETUP_PATH = os.path.join(PROJECT_ROOT, "setup.py")
|
||||
|
||||
|
||||
def load_setup_module():
|
||||
setuphelpers = types.ModuleType("setuphelpers")
|
||||
setuphelpers.makepath = lambda *parts: os.path.join(*parts)
|
||||
setuphelpers.isfile = os.path.isfile
|
||||
setuphelpers.WAPT = types.SimpleNamespace(private_dir=tempfile.gettempdir())
|
||||
sys.modules["setuphelpers"] = setuphelpers
|
||||
|
||||
spec = importlib.util.spec_from_file_location("ticketing_setup", SETUP_PATH)
|
||||
module = importlib.util.module_from_spec(spec)
|
||||
sys.modules["ticketing_setup"] = module
|
||||
spec.loader.exec_module(module)
|
||||
return module
|
||||
|
||||
|
||||
ticketing = load_setup_module()
|
||||
|
||||
|
||||
def make_config():
|
||||
config = ConfigParser()
|
||||
config.add_section("odoo")
|
||||
config.set("odoo", "url", "https://odoo.example.test")
|
||||
config.set("odoo", "api_key", "test-api-key")
|
||||
config.set("odoo", "model", "helpdesk.ticket")
|
||||
config.add_section("rocket")
|
||||
config.set("rocket", "webhook_url", "https://rocket.example.test/webhook-test")
|
||||
config.add_section("http")
|
||||
config.set("http", "timeout", "5")
|
||||
return config
|
||||
|
||||
|
||||
class TicketingStateTest(unittest.TestCase):
|
||||
def test_build_tickets_state_uses_string_ids_for_json_keys(self):
|
||||
state = ticketing.build_tickets_state([
|
||||
{"id": 12, "write_date": "2026-07-24 10:00:00"},
|
||||
{"id": 34, "write_date": "2026-07-24 11:00:00"},
|
||||
])
|
||||
|
||||
self.assertEqual(state, {
|
||||
"12": "2026-07-24 10:00:00",
|
||||
"34": "2026-07-24 11:00:00",
|
||||
})
|
||||
|
||||
def test_get_updated_ticket_ids_detects_new_and_changed_tickets(self):
|
||||
previous_state = {
|
||||
"1": "2026-07-24 09:00:00",
|
||||
"2": "2026-07-24 09:30:00",
|
||||
}
|
||||
tickets = [
|
||||
{"id": 1, "write_date": "2026-07-24 09:00:00"},
|
||||
{"id": 2, "write_date": "2026-07-24 10:00:00"},
|
||||
{"id": 3, "write_date": "2026-07-24 10:30:00"},
|
||||
]
|
||||
|
||||
self.assertEqual(ticketing.get_updated_ticket_ids(tickets, previous_state), [2, 3])
|
||||
|
||||
def test_load_tickets_state_returns_empty_dict_when_file_is_missing(self):
|
||||
missing_path = os.path.join(tempfile.gettempdir(), "missing-ticket-state.json")
|
||||
|
||||
self.assertEqual(ticketing.load_tickets_state(missing_path), {})
|
||||
|
||||
def test_save_and_load_tickets_state_roundtrip(self):
|
||||
with tempfile.TemporaryDirectory() as tmpdir:
|
||||
state_path = os.path.join(tmpdir, "tickets_state.json")
|
||||
expected_state = {"42": "2026-07-24 12:00:00"}
|
||||
|
||||
ticketing.save_tickets_state(state_path, expected_state)
|
||||
|
||||
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 = {
|
||||
"result": [{"id": 42, "write_date": "2026-07-24 12:00:00"}],
|
||||
}
|
||||
post.return_value = 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()
|
||||
self.assertEqual(post.call_args.kwargs["timeout"], 5)
|
||||
|
||||
@patch("ticketing_setup.requests.post")
|
||||
def test_fetch_odoo_tickets_rejects_jsonrpc_error(self, post):
|
||||
response = Mock()
|
||||
response.json.return_value = {"error": {"message": "Access denied"}}
|
||||
post.return_value = response
|
||||
|
||||
with self.assertRaisesRegex(Exception, "Erreur Odoo JSON-RPC"):
|
||||
ticketing.fetch_odoo_tickets(make_config())
|
||||
|
||||
@patch("ticketing_setup.requests.post")
|
||||
def test_send_to_rocket_raises_for_http_errors(self, post):
|
||||
response = Mock()
|
||||
post.return_value = response
|
||||
|
||||
ticketing.send_to_rocket("message de test", conf_wapt=make_config())
|
||||
|
||||
response.raise_for_status.assert_called_once_with()
|
||||
self.assertEqual(post.call_args.kwargs["timeout"], 5)
|
||||
self.assertEqual(post.call_args.kwargs["json"], {"text": "message de test"})
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@@ -0,0 +1,10 @@
|
||||
[rocket]
|
||||
webhook_url = CHANGE_ME_ROCKET_CHAT_WEBHOOK_URL
|
||||
|
||||
[odoo]
|
||||
url = https://CHANGE_ME_ODOO_HOST
|
||||
api_key = CHANGE_ME_ODOO_API_KEY
|
||||
model = helpdesk.ticket
|
||||
|
||||
[http]
|
||||
timeout = 30
|
||||
@@ -1 +0,0 @@
|
||||
�}�.
|
||||
Reference in New Issue
Block a user