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 IVRModèle OdooChamps clésNotes
Patient / clientres.partnername, phone, mobile, email, langVotre “identité” de base.
Rendez-vouscalendar.eventname, start, stop, duration, partner_ids, user_id, location, description, alarm_idspartner_ids = participants (incluant le patient).
Ressource (agent/équipe)res.users (ou hr.employee)name, tzEn pratique : calendar.event.user_id = “responsable” de l’événement.
Rappels Odoocalendar.alarm + calendar.event.alarm_idsalarm_type, duration, intervalPermet reminders Odoo (email/notification).
Invitation/présencecalendar.attendeestateUtile 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)

  1. 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"].
  1. Construire un planning occupé → en déduire les trous.
  2. Générer des slots candidats alignés sur un pas (ex. 15 min).
  3. 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é

  1. n8n crée un log de call (dans un modèle custom x_synergia_call) OU écrit dans calendar.event.description.
  2. n8n génère une URL Odoo “form view” pour l’événement ou le patient (selon votre usage).
  3. n8n envoie cette URL au poste agent (Ringover/CRM/Slack/email interne).

6) Structure de context (à stocker par call_id)

ChampTypeExempleUsage
call_idstringuuidclé de state machine
intentenumBOOK/RESCHEDULE/CANCEL/AGENTroutage
partner_idint123patient
event_idint987RDV existant
agent_user_idsint[][45,46]ressources
duration_minint30slotting
slotsarray[{start,stop,user_id,label}]lecture TTS
picked_slotobject{...}commit
retry_countint1no-speech/low conf
languagestringfr-FRvoix

7) Blueprint n8n (nœuds minimaux) pour Odoo Calendar

ÉtapeNœud n8nSortie
1Webhook /ivrevent + stt
2Function “Normalize”payload standard
3Data Store “Get State”context
4HTTP Request “Odoo call_kw” (selon state)partner/event/busy
5Function “Compute slots”slots[3]
6HTTP Request “Odoo create/write”event_id
7Data Store “Set State”context
8Respond to Webhookactions[]


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)

ÉtatButActions (référence)Sorties attendues
WELCOME_INTENTcapter intentionAintent: BOOK/RESCHEDULE/CANCEL/AGENT
IDENTIFYcapter identité (nom/prénom)A (timeout 9–10s si besoin)identity_text
PATIENT_LOOKUPrecherche Odoo partner(pas d’action IVR : action backend)FOUND_ONE / FOUND_MULTI / NOT_FOUND
CONFIRM_IDENTITYconfirmer partenaireCYES/NO
GET_EXISTING_APPTrécupérer prochain RDV(backend)FOUND / NONE
EXISTING_APPT_DECIDEdéplacer/annuler/garderAintent: RESCHEDULE/CANCEL/KEEP/AGENT
COLLECT_CONSTRAINTSpréférences (jour/heure)Bconstraints_text
COMPUTE_SLOTScalc 3 créneaux (busy events)(backend)SLOTS / NO_SLOTS
READ_SLOTSlire 3 créneauxDpick: 1/2/3 or AGENT
COMMIT_BOOK_OR_RESCHEDcreate/write event(backend)OK/FAIL
CANCEL_CONFIRMconfirmer annulationCYES/NO
COMMIT_CANCELwrite status canceled(backend)OK/FAIL
REMINDER_OPTINopt-in rappelsAYES/NO
SCHEDULE_REMINDERSjobs n8n (J-1, H-1)(backend)OK
TRANSFER_AGENTwarm transferEtransfer_result
ENDclôtureF

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