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 :

  1. call_start → Flow_OnCallStart
  2. stt_final → Flow_OnSTTFinal
  3. no_speech → Flow_OnNoSpeech
  4. transfer_result → Flow_OnTransferResult
  5. default → Flow_FailoverAgent

3.3 Subflows (nœuds internes)

Flow_OnCallStart

  1. Fn_InitContextDefaults
    • init window J+1→J+14, step 15, duration 30, work_hours…
  2. Fn_SetState_WELCOME_INTENT
  3. Respond_Webhook (actions WELCOME_INTENT)

Flow_OnSTTFinal

  1. Fn_CheckConfidence
    • si stt_conf < threshold → Fn_SetState_LOWCONFIRM → Respond (confirmation)
  2. 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

  1. Fn_IncNoSpeech
  2. IF no_speech >= 2 → Build_TRANSFER_AGENT
  3. else → Build_REPROMPT (même état, prompt plus direct)
  4. Respond_Webhook

Flow_OnTransferResult

  1. Fn_SetState_END
  2. 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)

SujetOdoo CalendarGoogle Calendar
Patient/CRMNatif (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 eventsFreeBusy natif (pratique)
Gouvernance / audit / RGPDPlus maîtrisable en internePlus complexe (données chez Google)
“Screen-pop agent”Très direct (URL Odoo)Dépend de votre outil agent
Multi-tenant / verticalisationTrès bonMoins 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 :

  1. Modèle de synchronisation (champs Studio Odoo + règles)
  2. Appels Odoo JSON-RPC exacts (6 blocs)
  3. 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 StudioTypeExempleRôle
x_sourceSelection (ivr,agent,web)ivrOrigine métier
x_statusSelection (pending,confirmed,canceled,to_reschedule)confirmedStatut métier (évite delete)
x_patient_partner_idMany2one → res.partner123Redondant si partner_ids contient le patient, mais utile
x_call_idCharuuidTraçabilité IVR
x_gcal_calendar_idCharagent1@domain.comCalendrier Google cible (agent)
x_gcal_event_idChar7k3...abcID Google event
x_gcal_etagChar"33q9..."Concurrency Google
x_sync_stateSelection (pending,synced,error)syncedStatut sync n8n
x_sync_errorTextquota exceeded...Diagnostic
x_sync_updated_atDatetime...Dernier sync n8n
x_external_uidCharsynergia:987UID stable exposé à Google (extendedProperties)

Recommandation : mettez aussi un champ sur res.users (ou hr.employee) :

ChampTypeExempleRôle
x_gcal_calendar_idCharagent1@domain.comQuel calendrier Google correspond à cet agent

1.2 Règles d’écriture (qui écrit où)

Copiable :

Règle A — Création / modification via IVR (n8n)

  1. Créer/mettre à jour l’événement dans Odoo (métier).
  2. Déterminer l’agent (user_id) et donc x_gcal_calendar_id.
  3. Créer/patcher l’événement dans Google Calendar.
  4. É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.eventGoogle eventCommentaire
namesummaryTitre
start / stopstart.dateTime / end.dateTimeConvertir timezone (Europe/Paris)
locationlocationOptionnel
descriptiondescriptionAjoutez un résumé IVR et un lien “screen-pop” Odoo
user_idcalendarId (cible)Via res.users.x_gcal_calendar_id
partner_ids (patient)attendees[]Si email dispo ; sinon omettre
x_external_uidextendedProperties.private.external_uidClé stable
x_call_id / partner_idextendedProperties.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

  1. Webhook IVR_In /ivr
  2. Function NormalizeEvent
  3. Data Store GetContext (key=call_id)
  4. Function MergeContext
  5. Switch ByEvent (call_start / stt_final / no_speech / transfer_result)
  6. Branche stt_final :
    • Function CheckConfidence
    • Switch ByState (WELCOME_INTENT / CONFIRM_IDENTITY / SLOTS_READOUT / CANCEL_CONFIRM / REMINDER_OPTIN …)
  7. Execute Workflow WF_Odoo_PatientAndEvent (selon besoin)
  8. Execute Workflow WF_GCal_FreeBusyAndEvent (selon besoin)
  9. Data Store SetContext
  10. 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 :

  1. HTTP Request Odoo_Auth (si cookie expiré)
  2. HTTP Request Odoo_PatientLookup (si partner_id absent)
  3. HTTP Request Odoo_CreateEvent (si BOOK et pas event_id)
  4. HTTP Request Odoo_WriteEvent (si RESCHEDULE/CANCEL)
  5. HTTP Request Odoo_WriteSyncFields (écrire x_gcal_* après retour Google)

4.3 Sub-workflow : WF_GCal_FreeBusyAndEvent

Nœuds :

  1. HTTP Request GCal_FreeBusy (si compute slots)
  2. Function Compute3Slots (holes → 3 slots)
  3. HTTP Request GCal_InsertEvent (si pas gcal_event_id)
  4. HTTP Request GCal_PatchEvent (si gcal_event_id présent)
  5. HTTP Request GCal_CancelEvent (si cancel)

5) Séquence “BOOK” (résumé exécutable)

Copiable :

  1. GCal_FreeBusy → calc slots
  2. IVR lit SLOTS_READOUT → user choisit slot #2
  3. Odoo_CreateEvent (Odoo event_id=987, x_sync_state=pending)
  4. Odoo_WriteExternalUID (x_external_uid=synergia:odoo_event:987)
  5. GCal_InsertEvent(calendarId=agentX, extendedProperties.private.external_uid=...)
  6. Odoo_WriteSyncFields(x_gcal_event_id, x_gcal_etag, synced)

6) Séquence “RESCHEDULE”

  1. Odoo_WriteEvent(start/stop, pending)
  2. GCal_PatchEvent
  3. 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.