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
- Ouvrir Odoo Studio → Contacts → Modifier le modèle Contact (res.partner)
- Ajouter un onglet « Santé / Withings »
-
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)
-
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
| Objectif | Module base | Méthode |
|---|---|---|
| Associer les données Withings à une fiche patient | Contacts (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 / graph | Odoo 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é)
| Champ | Type | Oblig. | Utilité / remarques |
|---|---|---|---|
| patient_id | m2o x_health_patient | ✔︎ | Rattachement patient (index). |
| source_domain | selection (weight,bodycomp,activity,sleep,heart,bp,temp, …) | ✔︎ | Domaine Withings d’origine (utile pr. filtres). |
| measure_type | selection (weight,bmi,fat_pct,systolic,diastolic,hr,spo2,sleep_score,sleep_duration,steps,calories, …) | ✔︎ | Type normalisé pour graphiques & KPIs. |
| value | float | ✔︎ | Valeur principale déjà normalisée SI (kg, mmHg, bpm…). |
| unit | char | Unité humaine (ex. kg, mmHg, %, bpm). | |
| measured_at | datetime (UTC) | ✔︎ | Horodatage physiologique (index). |
| timezone_offset_min | integer | Décalage local si utile pour vues par jour local. | |
| device_id | char | Identifiant appareil, si dispo. | |
| quality_flag | selection (ok,suspect,artifact) | Marqueurs qualité éventuels. | |
| source | char (default: withings) | Traçabilité (toujours withings ici). | |
| external_uid | char (unique) | ✔︎ | Idempotence: hash(`withings_userid |
| ingested_at | datetime (UTC, default now) | Traçabilité ETL. | |
| etl_version | char | Ex: etl-fastapi@1.0.0. | |
| payload_json | Json | Payload 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.