ODOO MDD Synergia 


x_health_measure (historique brut/normalisé)


Modèle de données Odoo (proposition)> x_health_measure (historique brut/normalisé) 


Vous pouvez utiliser Odoo (ERP) comme base de données « système de référence » pour stocker et exploiter, avec consentement explicite, les données issues des capteurs Withings. 

Ci-dessous une architecture recommandée en deux volets : Collecte & stockage dans Odoo ; 2) Agent conversationnel OpenAI utilisant ces données. 

  • 1) Collecte & stockage des données dans Odoo Schéma d’ensemble (texte) Objets Withings → App Withings → Cloud Withings → Webhook/ETL → Odoo (modèles personnalisés) → Dashboards/BI Composants Withings Public API + Webhooks (OAuth2 opt-in) Ingestion/ETL (micro-service FastAPI/Node) 


Rôle : recevoir les notifications, appeler l’API Withings, normaliser, contrôler le consentement, pseudonymiser si besoin, pousser dans Odoo. Odoo (Online ou On-prem) Modèles personnalisés (via Studio ou module) pour stocker mesures agrégées et historiques. Sécurité : groupes « Santé », journal d’audit, politiques de rétention. 

Dans Odoo Online, la bonne approche consiste à étendre un module standard existant via Odoo Studio 

Voici les meilleures options possibles selon les besoins fonctionnels de votre intégration Withings :

🩺 1️⃣ Option recommandée : Module « Contacts » (Contacts / res.partner)

Usage : rattacher les données Withings à la fiche patient existante.

👉 Avantages

  • Présent dans toutes les éditions Odoo Online (Community, Standard, Enterprise).
  • Compatible avec Studio et les vues personnalisées (formulaires, graphiques, pivot).
  • Permet d'utiliser le partner_id comme clé patient universelle (liaison CRM, facturation, RDV, etc.).

👉 Implémentation via Studio

  1. Ouvrir Odoo Studio → Contacts → Modifier le modèle Contact (res.partner)
  2. Ajouter un onglet « Santé / Withings »
  3. Créer les champs suivants :
    • x_withings_userid (Texte)
    • x_oauth_status (Sélection : connected, disconnected, revoked)
    • x_consent_datetime (Date/Heure)
    • x_consent_scope (Texte long)
  4. Créer une relation 1-n vers un nouveau modèle Studio x_health_measure :
    • Type : One2many
    • Modèle cible : x_health_measure
    • Champ inverse : x_patient_id
    • Libellé : « Mesures santé »

📈 2️⃣ Option complémentaire : Module « Feuilles de Temps / Timesheets » ou « Projet »

Usage : utile si vous souhaitez suivre ou agréger des données par période de suivi (journée/semaine).

Mais ce n’est pas indispensable pour un cas purement médical.

> @JHLComment > Mais important , en phase 2, pour les infirmier et autres personnels qui vont être impliqué dans la Projet pilote sur paris

📊 3️⃣ Option analytique : Module « Documents / Feuilles de calcul / Tableaux de bord »

Usage : visualiser les courbes et indicateurs (poids, TA, SPO₂, sommeil).

  • Créer des vues pivot et graph depuis Studio à partir du modèle x_health_measure.
  • Vous pouvez relier les tableaux à x_patient_id (contact) pour naviguer rapidement entre patient et mesures.

🧩 4️⃣ Option alternative (plus structurée) : Créer un « modèle Studio dédié »

Si vous voulez séparer proprement la base de santé du reste :

A. Créez dans Studio :

  • Modèle principal : x_health_patient (lié à res.partner)
  • Modèle enfant : x_health_measure
  • Modèle x_health_consent_log

B. Puis reliez :

  • x_health_patient.partner_id → res.partner
  • x_health_measure.patient_id → x_health_patient
  • x_health_consent_log.patient_id → x_health_patient

Ainsi, vous gardez la modularité du schéma que nous avions défini dans le module Python.

🔒 5️⃣ Sécurité & Partage dans Odoo Online

Dans Studio, configurez :

  • Groupes d’accès : créer un groupe « Santé » (même nom que précédemment).
  • Donner accès en lecture/écriture uniquement à ce groupe sur x_health_measure et x_health_patient.
  • Masquer ces menus aux utilisateurs standards (CRM, Comptabilité, etc.).

🔧 Schéma recommandé (en mode Odoo Studio)

[Contact / res.partner] ├── Champs Withings (userid, consent, status) └── 1-n → [x_health_measure] ├── patient_id (m2o res.partner) ├── measure_type ├── value ├── unit ├── measured_at ├── source ├── payload_json

Vues à créer dans Studio :

  • Formulaire Contact : onglet « Santé » avec la sous-vue des mesures.
  • Graphique x_health_measure : axe horizontal = measured_at, valeur = value, couleur = measure_type.
  • Pivot x_health_measure : lignes = measure_type, colonnes = mois(measured_at), valeur = avg(value).

🧠 Recommandation finale

ObjectifModule baseMéthode
Associer les données Withings à une fiche patientContacts (res.partner)✅ Étendre via Studio
Créer une base santé distincte (patients, mesures, consentements)Studio (nouveau modèle)✅ Reproduire les modèles x_health_patient et x_health_measure
Ajouter dashboards / pivot / graphOdoo Studio (vues personnalisées)✅ Créer vues graph et pivot
Automatiser via webhook (ETL)Intégration externe (FastAPI)✅ Connecter via JSON-RPC à ces modèles Studio

Modèle x_health_measure (recommandé)


ChampTypeOblig.Utilité / remarques
patient_idm2o x_health_patient✔︎Rattachement patient (index).
source_domainselection (weight,bodycomp,activity,sleep,heart,bp,temp, …)✔︎Domaine Withings d’origine (utile pr. filtres).
measure_typeselection (weight,bmi,fat_pct,systolic,diastolic,hr,spo2,sleep_score,sleep_duration,steps,calories, …)✔︎Type normalisé pour graphiques & KPIs.
valuefloat✔︎Valeur principale déjà normalisée SI (kg, mmHg, bpm…).
unitcharUnité humaine (ex. kg, mmHg, %, bpm).
measured_atdatetime (UTC)✔︎Horodatage physiologique (index).
timezone_offset_minintegerDécalage local si utile pour vues par jour local.
device_idcharIdentifiant appareil, si dispo.
quality_flagselection (ok,suspect,artifact)Marqueurs qualité éventuels.
sourcechar (default: withings)Traçabilité (toujours withings ici).
external_uidchar (unique)✔︎Idempotence: hash(`withings_userid
ingested_atdatetime (UTC, default now)Traçabilité ETL.
etl_versioncharEx: etl-fastapi@1.0.0.
payload_jsonJsonPayload brut minimal (cf. ci-dessous).

JSON ou texte pour payload_json ?

  • Préférez fields.Json (Odoo 16/17) → sérialisation propre + possible filtrage basique + meilleure lisibilité.
  • Évitez Text sauf si vous stockez le blob brut non parsé (peu d’intérêt ici).

Ce qu’on met dans payload_json

Gardez-le minimal (principe de minimisation RGPD) :

{ "withings": { "type": 1, "raw_value": 70000, "raw_unit_pow": -3, "grp_id": "mgrp_abc123" } }out le reste (valeur normalisée, type, unité, timestamp) est déjà dans les colonnes requêtables.

Index et contraintes

  • Index : (patient_id, measured_at), (patient_id, measure_type, measured_at).
  • Contrainte unique : external_uid.
  • Option : index partiel par measure_type si séries volumineuses (poids/TA).

Rétention & purge

  • Conservez payload_json 13 mois (configurable) puis purgez-le (en gardant value & measured_at).
  • Ajoutez un cron Odoo ir.cron pour la purge et un log d’audit.

Exemple Odoo (champ JSON inclus)

from odoo import models, fields class HealthMeasure(models.Model): _name = "x_health_measure" _description = "Health Measure" _order = "measured_at desc, id desc" patient_id = fields.Many2one("x_health_patient", required=True, index=True) source_domain = fields.Selection([ ("weight","weight"), ("bodycomp","bodycomp"), ("activity","activity"), ("sleep","sleep"), ("heart","heart"), ("bp","bp"), ("temp","temp"), ], required=True, index=True) measure_type = fields.Selection([ ("weight","weight"), ("bmi","bmi"), ("fat_pct","fat_pct"), ("systolic","systolic"), ("diastolic","diastolic"), ("hr","hr"), ("spo2","spo2"), ("sleep_score","sleep_score"), ("sleep_duration","sleep_duration"), ("steps","steps"), ("calories","calories"), ], required=True, index=True) value = fields.Float(required=True, digits=(16, 3)) unit = fields.Char() measured_at = fields.Datetime(required=True, index=True) timezone_offset_min = fields.Integer() device_id = fields.Char() quality_flag = fields.Selection([("ok","ok"),("suspect","suspect"),("artifact","artifact")]) source = fields.Char(default="withings") external_uid = fields.Char(index=True) ingested_at = fields.Datetime(default=lambda self: fields.Datetime.now()) etl_version = fields.Char() payload_json = fields.Json(string="Payload brut") _sql_constraints = [ ("external_uid_uniq", "unique(external_uid)", "Mesure déjà importée (external_uid).") ]

Exemple d’enregistrement (normalisé)

{ "patient_id": 42, "source_domain": "bp", "measure_type": "systolic", "value": 122.0, "unit": "mmHg", "measured_at": "2025-11-12 06:50:00", "external_uid": "sha256(usr|1731394200|systolic)", "payload_json": { "withings": {"type":53,"raw_value":122,"raw_unit_pow":0,"grp_id":"mgrp_abc"} } }

Pourquoi séparer measure_type + value du payload_json ?

  • Dashboards/BI Odoo → vues graph/pivot instantanées sans parser du JSON.
  • Interop → votre agent et vos exports CSV restent simples.
  • Évolutivité → vous pouvez ajouter d’autres tables spécialisées (ex. x_health_sleep) sans casser l’historique générique.