Odoo calendar
pour modification / annulation sous forme d’objets standards (
Très bien. Avec Odoo Calendar, vous avez un socle propre pour gérer la prise / modification / annulation sous forme d’objets standards (calendar.event) et vous pouvez piloter tout cela depuis n8n via JSON-RPC Odoo.
Ci-dessous : (1) le mapping Odoo, (2) les appels JSON-RPC, (3) une stratégie “3 créneaux” réaliste, (4) la gestion des rappels, (5) le transfert agent avec “screen-pop”.
1) Mapping Odoo Calendar (objets et champs)
| Besoin IVR | Modèle Odoo | Champs clés | Notes |
|---|---|---|---|
| Patient / client | res.partner | name, phone, mobile, email, lang | Votre “identité” de base. |
| Rendez-vous | calendar.event | name, start, stop, duration, partner_ids, user_id, location, description, alarm_ids | partner_ids = participants (incluant le patient). |
| Ressource (agent/équipe) | res.users (ou hr.employee) | name, tz | En pratique : calendar.event.user_id = “responsable” de l’événement. |
| Rappels Odoo | calendar.alarm + calendar.event.alarm_ids | alarm_type, duration, interval | Permet reminders Odoo (email/notification). |
| Invitation/présence | calendar.attendee | state | Utile si vous suivez “accepted/declined”. |
Recommandation Studio (indispensable pour un IVR robuste)
Ajoutez sur calendar.event :
- x_synergia_status (Selection) : confirmed | pending | canceled | to_reschedule
- x_synergia_call_id (Char) : pour traçabilité IVR
- x_synergia_source (Selection) : ivr | agent | web
- x_synergia_patient_phone (Char) : si vous voulez figer le numéro “appelant”
2) Endpoints JSON-RPC Odoo (utilisables depuis n8n)
URL & méthode
- POST https://<votre-odoo>/web/session/authenticate
- POST https://<votre-odoo>/web/dataset/call_kw
A) Auth (une fois, puis cookie de session)
{ "jsonrpc": "2.0", "method": "call", "params": { "db": "YOUR_DB", "login": "YOUR_LOGIN", "password": "YOUR_PASSWORD" }, "id": 1 }
B) Rechercher un patient (res.partner) par téléphone
Important : normaliser le numéro (E.164 si possible) côté n8n.
{ "jsonrpc": "2.0", "method": "call", "params": { "model": "res.partner", "method": "search_read", "args": [ ["|", ["phone", "ilike", "+33"], ["mobile", "ilike", "+33"]] ], "kwargs": { "fields": ["id", "name", "phone", "mobile", "lang"], "limit": 5 } }, "id": 2 }
C) Trouver le prochain RDV du patient (calendar.event)
{ "jsonrpc": "2.0", "method": "call", "params": { "model": "calendar.event", "method": "search_read", "args": [ [ ["partner_ids", "in", [123]], ["start", ">=", "2026-01-02 00:00:00"], ["active", "=", true] ] ], "kwargs": { "fields": ["id", "name", "start", "stop", "duration", "user_id", "location", "x_synergia_status"], "order": "start asc", "limit": 1 } }, "id": 3 }
D) Créer un RDV (prise de RDV)
{ "jsonrpc": "2.0", "method": "call", "params": { "model": "calendar.event", "method": "create", "args": [{ "name": "RDV Synergia – Téléassistance", "start": "2026-01-05 09:00:00", "stop": "2026-01-05 09:30:00", "duration": 0.5, "user_id": 45, "partner_ids": [[6, 0, [123]]], "location": "Téléphone", "description": "Créé via IVR conversationnel", "x_synergia_status": "confirmed", "x_synergia_source": "ivr", "x_synergia_call_id": "CALL-UUID" }], "kwargs": {} }, "id": 4 }
E) Replanifier (update start/stop)
{ "jsonrpc": "2.0", "method": "call", "params": { "model": "calendar.event", "method": "write", "args": [ [987], { "start": "2026-01-06 11:00:00", "stop": "2026-01-06 11:30:00", "x_synergia_status": "confirmed" } ], "kwargs": {} }, "id": 5 }
F) “Annuler” un RDV (approche recommandée)
Évitez unlink (suppression). Préférez un statut.
{ "jsonrpc": "2.0", "method": "call", "params": { "model": "calendar.event", "method": "write", "args": [ [987], { "x_synergia_status": "canceled" } ], "kwargs": {} }, "id": 6 }
3) Calculer “3 créneaux” dans Odoo (stratégie réaliste)
Odoo n’expose pas nativement un “slotting engine” complet pour tous les cas. Pour un MVP fiable, je recommande un algorithme simple côté n8n :
Entrées
- duration_min (ex. 30)
- fenêtre de recherche (ex. J+1 à J+14)
- horaires autorisés (ex. 09:00–12:30, 14:00–18:00)
- ressource(s) : user_id(s) éligibles (1 équipe ou plusieurs agents)
Étapes (côté n8n)
- Lister les événements existants des agents sur la fenêtre :
-
calendar.event.search_read avec domaine :
["user_id","in",[...]] + ["start","<",window_end] + ["stop",">",window_start] + ["active","=",true] + éventuellement ["x_synergia_status","!=", "canceled"].
- Construire un planning occupé → en déduire les trous.
- Générer des slots candidats alignés sur un pas (ex. 15 min).
- Garder les 3 premiers qui matchent la contrainte vocale (“mardi matin”, etc.).
Domaine Odoo pour “busy events”
[ ["user_id", "in", [45, 46, 47]], ["start", "<", "2026-01-16 00:00:00"], ["stop", ">", "2026-01-03 00:00:00"], ["active", "=", true], ["x_synergia_status", "!=", "canceled"] ]
4) Rappels : 2 options (choisissez selon votre logique “100% voix”)
Option A — Rappels “métier” via n8n (recommandé pour IVR 100% voix)
- n8n enregistre 2 jobs : J-1 et H-1 (dans votre store ou via une queue).
- À l’échéance : n8n déclenche un appel sortant (via jambonz/FreeSWITCH) et rejoue un mini-flow (“vous avez RDV demain… dites confirmer / replanifier / agent”).
Avantage : cohérence totale avec votre IVR conversationnel.
Option B — Rappels Odoo (email / notif) via alarm_ids
- utile si vous acceptez un rappel “non vocal” (mail, notif interne), ou en complément.
5) Transfert agent : “screen-pop” Odoo en 3 secondes
Quand vous transférez vers un agent, l’agent doit voir immédiatement :
- patient (res.partner)
- RDV courant (calendar.event)
- intention (book/reschedule/cancel)
- 3 créneaux proposés / choisi
Pattern recommandé
- n8n crée un log de call (dans un modèle custom x_synergia_call) OU écrit dans calendar.event.description.
- n8n génère une URL Odoo “form view” pour l’événement ou le patient (selon votre usage).
- n8n envoie cette URL au poste agent (Ringover/CRM/Slack/email interne).
6) Structure de context (à stocker par call_id)
| Champ | Type | Exemple | Usage |
|---|---|---|---|
| call_id | string | uuid | clé de state machine |
| intent | enum | BOOK/RESCHEDULE/CANCEL/AGENT | routage |
| partner_id | int | 123 | patient |
| event_id | int | 987 | RDV existant |
| agent_user_ids | int[] | [45,46] | ressources |
| duration_min | int | 30 | slotting |
| slots | array | [{start,stop,user_id,label}] | lecture TTS |
| picked_slot | object | {...} | commit |
| retry_count | int | 1 | no-speech/low conf |
| language | string | fr-FR | voix |
7) Blueprint n8n (nœuds minimaux) pour Odoo Calendar
| Étape | Nœud n8n | Sortie |
|---|---|---|
| 1 | Webhook /ivr | event + stt |
| 2 | Function “Normalize” | payload standard |
| 3 | Data Store “Get State” | context |
| 4 | HTTP Request “Odoo call_kw” (selon state) | partner/event/busy |
| 5 | Function “Compute slots” | slots[3] |
| 6 | HTTP Request “Odoo create/write” | event_id |
| 7 | Data Store “Set State” | context |
| 8 | Respond to Webhook | actions[] |
Un squelette d’actions IVR (say/listen/transfer) par état pour jambonz, avec les timeouts “100% voix
1) Domaines Odoo complets (patient lookup, prochain RDV, busy events) + normalisation téléphone
1.1 Normalisation téléphone (côté n8n, recommandé)
Objectif : produire une version E.164 et 1–2 variantes “souples” pour ilike.
Règle simple France (MVP) :
- si commence par 0 et longueur 10 → remplacer par +33 et supprimer le 0
- supprimer espaces, points, tirets
- conserver aussi une variante “digits-only” pour matcher les champs mal saisis
Exemple (Function node n8n) :
const raw = ($json.phone || "").toString(); const digits = raw.replace(/[^\d+]/g, ""); let e164 = digits; if (/^0\d{9}$/.test(digits)) e164 = "+33" + digits.slice(1); if (/^\d{9}$/.test(digits)) e164 = "+33" + digits; // si on reçoit "612345678" par ex. const loose = digits.replace(/[^\d]/g, ""); // digits-only return [{ phone_raw: raw, phone_e164: e164, phone_loose: loose }];
1.2 Patient lookup (res.partner) par téléphone (robuste)
But : retrouver 0/1/N partenaires.
Domain (à injecter dans args de search_read) :
[ "|","|","|", ["mobile","ilike","{{phone_e164}}"], ["phone","ilike","{{phone_e164}}"], ["mobile","ilike","{{phone_loose}}"], ["phone","ilike","{{phone_loose}}"] ]
JSON-RPC search_read :
{ "jsonrpc": "2.0", "method": "call", "params": { "model": "res.partner", "method": "search_read", "args": [ [ "|","|","|", ["mobile","ilike","{{phone_e164}}"], ["phone","ilike","{{phone_e164}}"], ["mobile","ilike","{{phone_loose}}"], ["phone","ilike","{{phone_loose}}"] ] ], "kwargs": { "fields": ["id","name","phone","mobile","email","lang","birthdate_date"], "limit": 10 } }, "id": 101 }
Si vous n’avez pas birthdate_date, remplacez par votre champ Studio (ex. x_birthdate).
1.3 Prochain RDV (calendar.event) du patient
Hypothèses :
- vous utilisez x_synergia_status pour éviter “canceled”
- vous stockez les dates en UTC ou en timezone serveur ; gardez une seule convention
Domain :
[ ["partner_ids","in",[{{partner_id}}]], ["start",">=","{{now}}"], ["active","=",true], ["x_synergia_status","!=","canceled"] ]
JSON-RPC :
{ "jsonrpc": "2.0", "method": "call", "params": { "model": "calendar.event", "method": "search_read", "args": [[ ["partner_ids","in",[{{partner_id}}]], ["start",">=","{{now}}"], ["active","=",true], ["x_synergia_status","!=","canceled"] ]], "kwargs": { "fields": ["id","name","start","stop","duration","user_id","location","description","x_synergia_status"], "order": "start asc", "limit": 1 } }, "id": 102 }
1.4 Busy events (disponibilités) pour une équipe d’agents
Objectif : récupérer tout événement qui chevauche la fenêtre [window_start, window_end) pour une liste user_ids.
Domain (chevauchement standard) :
[ ["user_id","in",{{user_ids}}], ["start","<","{{window_end}}"], ["stop",">","{{window_start}}"], ["active","=",true], ["x_synergia_status","!=","canceled"] ]
JSON-RPC :
{ "jsonrpc": "2.0", "method": "call", "params": { "model": "calendar.event", "method": "search_read", "args": [[ ["user_id","in",{{user_ids}}], ["start","<","{{window_end}}"], ["stop",">","{{window_start}}"], ["active","=",true], ["x_synergia_status","!=","canceled"] ]], "kwargs": { "fields": ["id","start","stop","user_id","name","x_synergia_status"], "order": "start asc", "limit": 5000 } }, "id": 103 }
Paramètres conseillés MVP
- window_start = now + 1 day at 00:00
- window_end = now + 14 days at 23:59
- pas de slot : 15 min
- durée : 30 min (configurable)
2) Squelette “actions IVR” (say/listen/transfer) par état – 100% voix
2.1 Conventions (très important)
- barge_in = true partout (l’utilisateur peut couper le TTS)
-
listen.timeout_ms :
- intents courts : 6–7s
- contraintes / phrases plus longues : 9–10s
-
no_speech :
- 2 tentatives puis TRANSFER_AGENT
-
low_confidence :
- bascule sur CONFIRMATION (“J’ai compris X, confirmez ?”)
2.2 Actions types (catalogue)
Vous réutilisez ces blocs dans chaque état.
A) Prompt + écoute (standard)
{ "actions": [ { "type": "say", "text": "{{prompt}}" }, { "type": "listen", "mode": "stt", "timeout_ms": 7000, "barge_in": true } ] }
B) Prompt “long” + écoute
{ "actions": [ { "type": "say", "text": "{{prompt}}" }, { "type": "listen", "mode": "stt", "timeout_ms": 9500, "barge_in": true } ] }
C) Confirmation (yes/no)
{ "actions": [ { "type": "say", "text": "J'ai compris : {{hypothesis}}. Confirmez-vous ?" }, { "type": "listen", "mode": "stt", "timeout_ms": 6000, "barge_in": true } ], "expect": "YES_NO" }
D) Lecture des 3 créneaux + choix
{ "actions": [ { "type": "say", "text": "Je vous propose : 1) {{s1}}, 2) {{s2}}, 3) {{s3}}. Dites 1, 2, 3, ou agent." }, { "type": "listen", "mode": "stt", "timeout_ms": 9000, "barge_in": true } ], "expect": "SLOT_PICK_OR_AGENT" }
E) Transfert agent (warm)
{ "actions": [ { "type": "say", "text": "Je vous mets en relation avec un agent." }, { "type": "transfer", "mode": "warm", "target": "AGENT_QUEUE", "timeout_ms": 30000 } ] }
F) Fin
{ "actions": [ { "type": "say", "text": "C'est noté. Merci et bonne journée." }, { "type": "hangup" } ] }
2.3 Squelette par état (scénario RDV complet)
| État | But | Actions (référence) | Sorties attendues |
|---|---|---|---|
| WELCOME_INTENT | capter intention | A | intent: BOOK/RESCHEDULE/CANCEL/AGENT |
| IDENTIFY | capter identité (nom/prénom) | A (timeout 9–10s si besoin) | identity_text |
| PATIENT_LOOKUP | recherche Odoo partner | (pas d’action IVR : action backend) | FOUND_ONE / FOUND_MULTI / NOT_FOUND |
| CONFIRM_IDENTITY | confirmer partenaire | C | YES/NO |
| GET_EXISTING_APPT | récupérer prochain RDV | (backend) | FOUND / NONE |
| EXISTING_APPT_DECIDE | déplacer/annuler/garder | A | intent: RESCHEDULE/CANCEL/KEEP/AGENT |
| COLLECT_CONSTRAINTS | préférences (jour/heure) | B | constraints_text |
| COMPUTE_SLOTS | calc 3 créneaux (busy events) | (backend) | SLOTS / NO_SLOTS |
| READ_SLOTS | lire 3 créneaux | D | pick: 1/2/3 or AGENT |
| COMMIT_BOOK_OR_RESCHED | create/write event | (backend) | OK/FAIL |
| CANCEL_CONFIRM | confirmer annulation | C | YES/NO |
| COMMIT_CANCEL | write status canceled | (backend) | OK/FAIL |
| REMINDER_OPTIN | opt-in rappels | A | YES/NO |
| SCHEDULE_REMINDERS | jobs n8n (J-1, H-1) | (backend) | OK |
| TRANSFER_AGENT | warm transfer | E | transfer_result |
| END | clôture | F | — |
3) Paramètres “Odoo Calendar” – décisions à figer (je les fige par défaut)
Sans vous re-questionner, je fige pour le MVP :
- Durée RDV : 30 min
- Fenêtre : J+1 → J+14
- Horaires : 09:00–12:30 et 14:00–18:00
- Pas de slot : 15 min
- Annulation : x_synergia_status = canceled (pas de suppression)
4) “Screen-pop agent” Odoo (optionnel mais fortement recommandé)
Quand vous appelez TRANSFER_AGENT, faites en parallèle (côté n8n) :
- écrire un résumé dans calendar.event.description (ou un modèle x_synergia_call)
- pousser un lien Odoo form view à l’agent
Exemple de lien (générique) :
- Patient : /web#id={{partner_id}}&model=res.partner&view_type=form
- RDV : /web#id={{event_id}}&model=calendar.event&view_type=form