l’adaptation si le backend RDV est Google Calendar (au lieu d’Odoo Calendar).
Oui. Ci-dessous, en format copiable, les 3 éléments + l’adaptation si le backend RDV est Google Calendar (au lieu d’Odoo Calendar). Je conserve un design où n8n porte la state machine et où le “backend agenda” est interchangeable via 3 fonctions : lookup_patient, compute_slots, commit_event.
1) Schéma de données context (canonique)
1.1 Objet context (stocké par call_id)
{ "call_id": "b6a1c9b0-5c47-4db0-9d50-2b0b7b6d6c2a", "tenant_id": "synergia-prod", "flow_version": "rdv_v1", "language": "fr-FR", "timezone": "Europe/Paris", "intent": "BOOK", "intent_confidence": 0.86, "caller": { "from_e164": "+33612345678", "from_raw": "06 12 34 56 78", "channel": "pstn" }, "patient": { "partner_id": 123, "display_name": "Jean Dupont", "phone_e164": "+33612345678", "match_mode": "PHONE", "match_confidence": 0.92, "needs_disambiguation": false }, "appointment": { "event_id": 987, "status": "confirmed", "start": "2026-01-05 09:00:00", "stop": "2026-01-05 09:30:00", "duration_min": 30, "owner_user_id": 45, "location": "Téléphone" }, "constraints": { "raw_text": "mardi matin ou jeudi après-midi", "window_start": "2026-01-03 00:00:00", "window_end": "2026-01-17 00:00:00", "work_hours": [ { "start": "09:00", "end": "12:30" }, { "start": "14:00", "end": "18:00" } ], "slot_step_min": 15, "preferred_days": ["TUE", "THU"], "preferred_periods": ["AM", "PM"] }, "routing": { "agent_queue": "AGENT_QUEUE", "eligible_user_ids": [45, 46, 47], "warm_transfer": true }, "slots": [ { "rank": 1, "start": "2026-01-06 09:00:00", "stop": "2026-01-06 09:30:00", "owner_user_id": 45, "label_tts": "mardi 6 janvier à 9 heures" }, { "rank": 2, "start": "2026-01-08 15:00:00", "stop": "2026-01-08 15:30:00", "owner_user_id": 46, "label_tts": "jeudi 8 janvier à 15 heures" }, { "rank": 3, "start": "2026-01-08 16:15:00", "stop": "2026-01-08 16:45:00", "owner_user_id": 47, "label_tts": "jeudi 8 janvier à 16 heures 15" } ], "picked_slot": { "rank": 2 }, "reminders": { "opt_in": true, "jobs": [ { "type": "CALL", "offset": "P1D", "when": "J-1", "status": "scheduled" }, { "type": "CALL", "offset": "PT1H", "when": "H-1", "status": "scheduled" } ] }, "counters": { "no_speech": 0, "low_confidence": 0, "retries_total": 0 }, "telemetry": { "state": "WELCOME_INTENT", "last_event": "stt_final", "last_stt": "je veux déplacer mon rendez-vous", "last_stt_conf": 0.86 } }
1.2 Seuils recommandés (constants)
{ "stt_confidence_threshold": 0.75, "max_no_speech": 2, "max_low_confidence": 2, "listen_timeout_ms_short": 7000, "listen_timeout_ms_long": 9500, "transfer_timeout_ms": 30000 }
2) Exemples de réponses actions[] par grands états (copiables)
Convention : n8n renvoie toujours { actions: [...], state: {...} }.
Les placeholders {{...}} sont substitués par n8n (Function/Set).
2.1 WELCOME_INTENT
{ "actions": [ { "type": "say", "text": "Bonjour. Dites simplement : prendre rendez-vous, déplacer, annuler, ou parler à un agent." }, { "type": "listen", "mode": "stt", "timeout_ms": 7000, "barge_in": true } ], "state": { "name": "WELCOME_INTENT" } }
2.2 CONFIRM_IDENTITY
{ "actions": [ { "type": "say", "text": "Je vous ai identifié comme {{patient.display_name}}. Confirmez-vous ?" }, { "type": "listen", "mode": "stt", "timeout_ms": 6000, "barge_in": true } ], "state": { "name": "CONFIRM_IDENTITY" } }
2.3 SLOTS_READOUT
{ "actions": [ { "type": "say", "text": "Je peux vous proposer : 1) {{slots[0].label_tts}}, 2) {{slots[1].label_tts}}, 3) {{slots[2].label_tts}}. Dites 1, 2, 3, ou agent." }, { "type": "listen", "mode": "stt", "timeout_ms": 9000, "barge_in": true } ], "state": { "name": "SLOTS_READOUT" } }
2.4 CANCEL_CONFIRM
{ "actions": [ { "type": "say", "text": "Confirmez-vous l'annulation de votre rendez-vous ?" }, { "type": "listen", "mode": "stt", "timeout_ms": 6000, "barge_in": true } ], "state": { "name": "CANCEL_CONFIRM" } }
2.5 REMINDER_OPTIN
{ "actions": [ { "type": "say", "text": "Souhaitez-vous recevoir un rappel la veille et une heure avant ?" }, { "type": "listen", "mode": "stt", "timeout_ms": 7000, "barge_in": true } ], "state": { "name": "REMINDER_OPTIN" } }
2.6 TRANSFER_AGENT (warm)
{ "actions": [ { "type": "say", "text": "Je vous mets en relation avec un agent." }, { "type": "transfer", "mode": "warm", "target": "AGENT_QUEUE", "timeout_ms": 30000 } ], "state": { "name": "TRANSFER_AGENT" } }
2.7 END_CONFIRMATION
{ "actions": [ { "type": "say", "text": "C'est noté. Merci et bonne journée." }, { "type": "hangup" } ], "state": { "name": "END_CONFIRMATION" } }
3) Template n8n “pseudo-export” (liste exacte des nœuds + variables)
3.1 Variables standard (dans n8n)
- {{$json.call_id}}
- {{$json.event}} (call_start, stt_final, no_speech, transfer_result…)
- {{$json.stt.text}}
- {{$json.stt.confidence}}
- {{$json.state.name}} (state courant récupéré du store)
- {{$json.context}} (context complet)
3.2 Nœuds (ordre recommandé)
A — Webhook_IVR (Webhook Trigger)
- Path : /ivr
- Method : POST
- Response mode : “Respond to Webhook” (via node final)
B — Fn_NormalizeEvent (Function)
Sortie :
return [{ call_id: $json.call_id, event: $json.event, stt_text: $json.stt?.text || "", stt_conf: $json.stt?.confidence ?? null, payload: $json }];
C — Store_GetContext (Data Store / Redis / Postgres)
- Key : call_id
- Default si absent : { telemetry:{state:"START"}, counters:{...}, ... }
D — Fn_MergeContext (Function)
- merge payload + context
- met telemetry.last_*
E — Switch_ByEvent (Switch)
Branches :
- call_start → Flow_OnCallStart
- stt_final → Flow_OnSTTFinal
- no_speech → Flow_OnNoSpeech
- transfer_result → Flow_OnTransferResult
- default → Flow_FailoverAgent
3.3 Subflows (nœuds internes)
Flow_OnCallStart
-
Fn_InitContextDefaults
- init window J+1→J+14, step 15, duration 30, work_hours…
- Fn_SetState_WELCOME_INTENT
- Respond_Webhook (actions WELCOME_INTENT)
Flow_OnSTTFinal
-
Fn_CheckConfidence
- si stt_conf < threshold → Fn_SetState_LOWCONFIRM → Respond (confirmation)
-
Fn_RouteByState (Switch sur context.telemetry.state)
- WELCOME_INTENT → NLP_IntentExtract
- CONFIRM_IDENTITY → NLP_YesNo
- SLOTS_READOUT → NLP_PickSlotOrAgent
- CANCEL_CONFIRM → NLP_YesNo
- REMINDER_OPTIN → NLP_YesNo
- autres → NLP_Generic
NLP_IntentExtract (HTTP Request ou Function)
- Sort : intent=BOOK|RESCHEDULE|CANCEL|AGENT|UNKNOWN
-
Puis :
- si AGENT → Build_TRANSFER_AGENT
- sinon → Odoo_or_GCal_LookupPatient → Build_CONFIRM_IDENTITY ou Build_IDENTIFY (si not found)
Odoo_or_GCal_LookupPatient
- Si backend = Odoo : JSON-RPC res.partner.search_read
- Si backend = GCal : lookup patient = CRM externe (Odoo CRM / DB). (Google Calendar ne gère pas un “patient” nativement.)
Build_CONFIRM_IDENTITY
- set context.patient.partner_id + display_name
- set state CONFIRM_IDENTITY
- respond actions CONFIRM_IDENTITY
After_CONFIRM_IDENTITY (YES)
-
route par context.intent :
- BOOK → Collect_CONSTRAINTS (prompt)
- RESCHEDULE/CANCEL → Fetch_Existing_Appt (backend)
- etc.
Flow_OnNoSpeech
- Fn_IncNoSpeech
- IF no_speech >= 2 → Build_TRANSFER_AGENT
- else → Build_REPROMPT (même état, prompt plus direct)
- Respond_Webhook
Flow_OnTransferResult
- Fn_SetState_END
- Respond_Webhook (END_CONFIRMATION ou hangup direct selon votre politique)
Node final — Store_SetContext puis Respond_Webhook
Dans chaque branche avant réponse :
- Store_SetContext (key call_id, value context)
- Respond_Webhook renvoie {actions, state, context_delta}
4) Si le backend RDV est Google Calendar (au lieu d’Odoo Calendar)
4.1 Ce qui change (très concret)
Google Calendar remplace uniquement :
- compute_slots (lecture busy)
- commit_event (create/update/delete)
Mais il ne remplace pas :
- patient lookup (il n’existe pas “res.partner” dans Google Calendar)
- la gouvernance métier (statuts, liens dossier, RGPD) → à tenir dans Odoo/CRM ou votre base.
4.2 API Google Calendar à utiliser (concepts)
- FreeBusy : obtenir les périodes occupées sur une fenêtre
- Events.insert / events.patch / events.delete : créer / modifier / supprimer
- extendedProperties.private : stocker call_id, partner_id, source=ivr (recommandé)
A) FreeBusy (équivalent “busy events”)
Request (conceptuelle) :
{ "timeMin": "2026-01-03T00:00:00+01:00", "timeMax": "2026-01-17T00:00:00+01:00", "items": [ { "id": "agent1@yourdomain.com" }, { "id": "agent2@yourdomain.com" } ] }
Response :
{ "calendars": { "agent1@yourdomain.com": { "busy": [{ "start": "...", "end": "..." }] } } }
B) Create event (prise RDV)
{ "summary": "RDV Synergia – Téléassistance", "start": { "dateTime": "2026-01-06T15:00:00+01:00" }, "end": { "dateTime": "2026-01-06T15:30:00+01:00" }, "attendees": [{ "email": "patient@example.com" }], "location": "Téléphone", "description": "Créé via IVR", "extendedProperties": { "private": { "call_id": "CALL-UUID", "partner_id": "123", "source": "ivr" } } }
C) Update event (replanification)
- events.patch avec start/end + maintien extendedProperties.private
4.3 Rappels (Google Calendar)
- Google Calendar a des “reminders”/notifications, mais pour un IVR 100% voix, je recommande toujours :
- Rappels orchestrés par n8n (J-1 / H-1) déclenchant un appel sortant (mini-flow voix), car c’est vous qui contrôlez l’expérience.
5) Tableau comparatif Odoo vs Google Calendar (décision d’architecture)
| Sujet | Odoo Calendar | Google Calendar |
|---|---|---|
| Patient/CRM | Natif (res.partner) | Non (à gérer ailleurs) |
| Statuts métier (canceled/to_reschedule) | Très simple via champs custom | À reconstituer via extendedProperties / DB |
| Slotting (busy) | Vous calculez via events | FreeBusy natif (pratique) |
| Gouvernance / audit / RGPD | Plus maîtrisable en interne | Plus complexe (données chez Google) |
| “Screen-pop agent” | Très direct (URL Odoo) | Dépend de votre outil agent |
| Multi-tenant / verticalisation | Très bon | Moins naturel |
Décision recommandée (alignée avec votre contexte)
- Si votre “vérité métier” est déjà dans Odoo (ce qui est votre cas) : Odoo Calendar reste le backend RDV principal.
-
Si vous avez des équipes qui travaillent déjà dans Google Workspace : vous pouvez faire un mode hybride :
- Odoo = patient + traçabilité + statut
- Google Calendar = disponibilité agents (FreeBusy) + création event
- et vous synchronisez event_id Google ↔ calendar.event Odoo via champs x_gcal_event_id.
Si vous confirmez “Odoo seul” vs “hybride Odoo+Google”, je vous fournis le bloc copiable suivant :
- modèle de synchronisation (champs Studio Odoo + règles d’écriture) et
- les 6 appels JSON-RPC exacts (Odoo) / les 3 appels Google (FreeBusy/Insert/Patch) avec le mapping des champs.
hybride Odoo+Google”, 6 appels JSON-RPC exacts (Odoo) / les 3 appels Google (FreeBusy/Insert/Patch)
Très bien. En hybride Odoo + Google Calendar, la règle qui rend le système stable est :
- Odoo = source de vérité métier (patient, statut, traçabilité, RGPD, logs IVR, décision).
- Google Calendar = source de vérité “planning agents” (free/busy, calendriers personnels/pro, UX agent).
- Synchronisation pilotée par n8n (event-driven) avec IDs croisés + versioning.
Je vous donne ci-dessous, en format copiable :
- Modèle de synchronisation (champs Studio Odoo + règles)
- Appels Odoo JSON-RPC exacts (6 blocs)
- Appels Google Calendar exacts (FreeBusy / Insert / Patch / Delete) + mapping
1) Modèle de synchronisation hybride
1.1 Champs Studio à ajouter sur calendar.event (Odoo)
Table copiable :
| Champ Studio | Type | Exemple | Rôle |
|---|---|---|---|
| x_source | Selection (ivr,agent,web) | ivr | Origine métier |
| x_status | Selection (pending,confirmed,canceled,to_reschedule) | confirmed | Statut métier (évite delete) |
| x_patient_partner_id | Many2one → res.partner | 123 | Redondant si partner_ids contient le patient, mais utile |
| x_call_id | Char | uuid | Traçabilité IVR |
| x_gcal_calendar_id | Char | agent1@domain.com | Calendrier Google cible (agent) |
| x_gcal_event_id | Char | 7k3...abc | ID Google event |
| x_gcal_etag | Char | "33q9..." | Concurrency Google |
| x_sync_state | Selection (pending,synced,error) | synced | Statut sync n8n |
| x_sync_error | Text | quota exceeded... | Diagnostic |
| x_sync_updated_at | Datetime | ... | Dernier sync n8n |
| x_external_uid | Char | synergia:987 | UID stable exposé à Google (extendedProperties) |
Recommandation : mettez aussi un champ sur res.users (ou hr.employee) :
| Champ | Type | Exemple | Rôle |
|---|---|---|---|
| x_gcal_calendar_id | Char | agent1@domain.com | Quel calendrier Google correspond à cet agent |
1.2 Règles d’écriture (qui écrit où)
Copiable :
Règle A — Création / modification via IVR (n8n)
- Créer/mettre à jour l’événement dans Odoo (métier).
- Déterminer l’agent (user_id) et donc x_gcal_calendar_id.
- Créer/patcher l’événement dans Google Calendar.
- Écrire dans Odoo : x_gcal_event_id, x_gcal_etag, x_sync_state='synced'.
Règle B — Annulation
- Ne supprimez pas côté Odoo : x_status='canceled'.
-
Côté Google : soit events.patch (mettre status=cancelled), soit events.delete.
(Je recommande patch status=cancelled pour garder l’historique côté Google.)
Règle C — “Source de vérité” en conflit
-
Si un agent déplace un RDV directement dans Google : vous pouvez choisir :
- Mode strict (recommandé MVP) : Google ne fait foi que pour free/busy ; toute modif doit passer par Odoo (sinon ignorée).
- Mode bidirectionnel : un watcher Google (push notifications) remonte l’update et n8n patch Odoo. (Plus complexe, à faire en phase 2.)
1.3 Mapping de champs Odoo → Google
Copiable :
| Odoo calendar.event | Google event | Commentaire |
|---|---|---|
| name | summary | Titre |
| start / stop | start.dateTime / end.dateTime | Convertir timezone (Europe/Paris) |
| location | location | Optionnel |
| description | description | Ajoutez un résumé IVR et un lien “screen-pop” Odoo |
| user_id | calendarId (cible) | Via res.users.x_gcal_calendar_id |
| partner_ids (patient) | attendees[] | Si email dispo ; sinon omettre |
| x_external_uid | extendedProperties.private.external_uid | Clé stable |
| x_call_id / partner_id | extendedProperties.private.* | Traçabilité |
2) Appels Odoo JSON-RPC (exact, copiable)
2.1 Auth
{ "jsonrpc": "2.0", "method": "call", "params": { "db": "YOUR_DB", "login": "YOUR_LOGIN", "password": "YOUR_PASSWORD" }, "id": 1 }
2.2 Lire le mapping agent → Google calendarId
(Champ Studio x_gcal_calendar_id sur res.users)
{ "jsonrpc": "2.0", "method": "call", "params": { "model": "res.users", "method": "read", "args": [[45], ["id","name","tz","x_gcal_calendar_id"]], "kwargs": {} }, "id": 2 }
2.3 Créer l’événement Odoo (BOOK)
{ "jsonrpc": "2.0", "method": "call", "params": { "model": "calendar.event", "method": "create", "args": [{ "name": "RDV Synergia – Téléassistance", "start": "2026-01-06 15:00:00", "stop": "2026-01-06 15:30:00", "duration": 0.5, "user_id": 45, "partner_ids": [[6, 0, [123]]], "location": "Téléphone", "description": "Créé via IVR", "x_source": "ivr", "x_status": "confirmed", "x_call_id": "CALL-UUID", "x_patient_partner_id": 123, "x_external_uid": "synergia:odoo_event:{{placeholder}}", "x_sync_state": "pending" }], "kwargs": {} }, "id": 3 }
Note : x_external_uid doit être stable. Après création, vous le ré-écrivez avec l’ID réel.
2.4 Mettre à jour x_external_uid après création
{ "jsonrpc": "2.0", "method": "call", "params": { "model": "calendar.event", "method": "write", "args": [ [987], { "x_external_uid": "synergia:odoo_event:987" } ], "kwargs": {} }, "id": 4 }
2.5 Replanifier (RESCHEDULE)
{ "jsonrpc": "2.0", "method": "call", "params": { "model": "calendar.event", "method": "write", "args": [ [987], { "start": "2026-01-08 16:15:00", "stop": "2026-01-08 16:45:00", "x_status": "confirmed", "x_sync_state": "pending" } ], "kwargs": {} }, "id": 5 }
2.6 Enregistrer le retour Google (event_id + etag)
{ "jsonrpc": "2.0", "method": "call", "params": { "model": "calendar.event", "method": "write", "args": [ [987], { "x_gcal_calendar_id": "agent1@domain.com", "x_gcal_event_id": "7k3abc123def", "x_gcal_etag": "\"33q9...\"", "x_sync_state": "synced", "x_sync_error": false, "x_sync_updated_at": "2026-01-02 19:10:00" } ], "kwargs": {} }, "id": 6 }
2.7 Annuler côté Odoo (CANCEL)
{ "jsonrpc": "2.0", "method": "call", "params": { "model": "calendar.event", "method": "write", "args": [ [987], { "x_status": "canceled", "x_sync_state": "pending" } ], "kwargs": {} }, "id": 7 }
3) Appels Google Calendar (exact, copiable)
3.1 Pré-requis d’auth (n8n)
Dans n8n, utilisez Google Calendar credentials OAuth2 (scopes typiques) :
- https://www.googleapis.com/auth/calendar
- ou plus restreint : .../calendar.events + .../calendar.freebusy
Je vous donne les appels REST bruts (n8n HTTP Request), avec header :
- Authorization: Bearer {{access_token}}
3.2 FreeBusy (pour calculer les 3 créneaux)
POST https://www.googleapis.com/calendar/v3/freeBusy
{ "timeMin": "2026-01-03T00:00:00+01:00", "timeMax": "2026-01-17T00:00:00+01:00", "timeZone": "Europe/Paris", "items": [ { "id": "agent1@domain.com" }, { "id": "agent2@domain.com" }, { "id": "agent3@domain.com" } ] }
Réponse : calendars[calendarId].busy[]. Vous calculez les “holes” côté n8n et vous proposez 3 slots.
3.3 Create event (Insert)
POST https://www.googleapis.com/calendar/v3/calendars/{calendarId}/events
Body (avec traçabilité) :
{ "summary": "RDV Synergia – Téléassistance", "start": { "dateTime": "2026-01-08T16:15:00+01:00", "timeZone": "Europe/Paris" }, "end": { "dateTime": "2026-01-08T16:45:00+01:00", "timeZone": "Europe/Paris" }, "location": "Téléphone", "description": "Créé via IVR. Odoo: /web#id=987&model=calendar.event&view_type=form", "extendedProperties": { "private": { "external_uid": "synergia:odoo_event:987", "odoo_event_id": "987", "partner_id": "123", "call_id": "CALL-UUID", "source": "ivr" } } }
Réponse Google contiendra notamment :
- id (→ x_gcal_event_id)
- etag (→ x_gcal_etag)
3.4 Update event (Patch)
PATCH https://www.googleapis.com/calendar/v3/calendars/{calendarId}/events/{eventId}
{ "start": { "dateTime": "2026-01-06T15:00:00+01:00", "timeZone": "Europe/Paris" }, "end": { "dateTime": "2026-01-06T15:30:00+01:00", "timeZone": "Europe/Paris" }, "summary": "RDV Synergia – Téléassistance", "location": "Téléphone", "description": "Replanifié via IVR. Odoo: /web#id=987&model=calendar.event&view_type=form", "extendedProperties": { "private": { "external_uid": "synergia:odoo_event:987", "odoo_event_id": "987", "partner_id": "123", "call_id": "CALL-UUID", "source": "ivr" } } }
Concurrency (option recommandé)
Si vous stockez etag, vous pouvez utiliser If-Match: <etag> pour éviter d’écraser une modification manuelle agent.
Si 412 Precondition Failed, vous basculez en mode agent (transfert) ou en “réconciliation”.
3.5 Cancel côté Google (recommandation)
PATCH (plutôt que delete) :
{ "status": "cancelled" }
(Alternative : DELETE .../events/{eventId} si vous ne voulez pas d’historique.)
4) Template n8n (pseudo-export) – Hybride Odoo + Google
Ci-dessous la liste exacte des nœuds (un workflow principal + 2 sub-workflows).
4.1 Workflow principal : IVR_RDV_Main
- Webhook IVR_In /ivr
- Function NormalizeEvent
- Data Store GetContext (key=call_id)
- Function MergeContext
- Switch ByEvent (call_start / stt_final / no_speech / transfer_result)
-
Branche stt_final :
- Function CheckConfidence
- Switch ByState (WELCOME_INTENT / CONFIRM_IDENTITY / SLOTS_READOUT / CANCEL_CONFIRM / REMINDER_OPTIN …)
- Execute Workflow WF_Odoo_PatientAndEvent (selon besoin)
- Execute Workflow WF_GCal_FreeBusyAndEvent (selon besoin)
- Data Store SetContext
- Respond to Webhook IVR_Out
Variables clés à conserver dans le contexte :
- context.appointment.event_id (Odoo)
- context.appointment.gcal_calendar_id
- context.appointment.gcal_event_id
- context.appointment.gcal_etag
4.2 Sub-workflow : WF_Odoo_PatientAndEvent
Nœuds :
- HTTP Request Odoo_Auth (si cookie expiré)
- HTTP Request Odoo_PatientLookup (si partner_id absent)
- HTTP Request Odoo_CreateEvent (si BOOK et pas event_id)
- HTTP Request Odoo_WriteEvent (si RESCHEDULE/CANCEL)
- HTTP Request Odoo_WriteSyncFields (écrire x_gcal_* après retour Google)
4.3 Sub-workflow : WF_GCal_FreeBusyAndEvent
Nœuds :
- HTTP Request GCal_FreeBusy (si compute slots)
- Function Compute3Slots (holes → 3 slots)
- HTTP Request GCal_InsertEvent (si pas gcal_event_id)
- HTTP Request GCal_PatchEvent (si gcal_event_id présent)
- HTTP Request GCal_CancelEvent (si cancel)
5) Séquence “BOOK” (résumé exécutable)
Copiable :
- GCal_FreeBusy → calc slots
- IVR lit SLOTS_READOUT → user choisit slot #2
- Odoo_CreateEvent (Odoo event_id=987, x_sync_state=pending)
- Odoo_WriteExternalUID (x_external_uid=synergia:odoo_event:987)
- GCal_InsertEvent(calendarId=agentX, extendedProperties.private.external_uid=...)
- Odoo_WriteSyncFields(x_gcal_event_id, x_gcal_etag, synced)
6) Séquence “RESCHEDULE”
- Odoo_WriteEvent(start/stop, pending)
- GCal_PatchEvent
- Odoo_WriteSyncFields(synced)
7) Point de vigilance (hybride) à trancher dès le MVP
Je tranche par défaut (le plus robuste) :
- Google = planning agent, donc FreeBusy est la référence pour proposer des créneaux.
- Odoo = vérité métier, donc la “décision” et le statut sont dans Odoo.
-
Modifs manuelles dans Google (agent) : tolérées mais non “source of truth” en MVP.
En cas de conflit (etag), fallback transfert agent.
Si vous voulez, je peux maintenant vous livrer la version “copiable” du workflow n8n complet sous forme :
- d’une liste de nœuds avec leurs expressions exactes ({{$json...}}) et
-
des bodies HTTP déjà paramétrés pour Odoo et Google (avec placeholders),
afin que votre équipe le reconstitue en 30–60 minutes.