- Added comprehensive dependency management with proper OdooRPC library integration - Fixed XML-RPC, JSON-RPC, and OdooRPC connection authentication issues - Implemented proper Odoo 18 API key format support with scope parameter - Added intelligent field mapping wizard with Full/Required/Balanced modes - Enhanced error handling and connection state management - Removed legacy API key system for clean reinstall capability - Added detailed debug logging for connection troubleshooting
532 lines
No EOL
17 KiB
Markdown
532 lines
No EOL
17 KiB
Markdown
# Module de Synchronisation Odoo-to-Odoo
|
|
|
|
## Objectif
|
|
Ce module permet la synchronisation bidirectionnelle de données entre deux instances Odoo via XML-RPC, avec un système de validation et de reprise robuste.
|
|
|
|
## Potential Issues and Fragile Areas
|
|
**Broad Exception Handling**: Plusieurs fichiers utilisent des clauses `except Exception` trop larges qui pourraient masquer des problèmes sous-jacents :
|
|
|
|
- `sync_instance.py` a une gestion d'exception large avec un commentaire de désactivation pylint
|
|
- `sync_manager.py` a plusieurs gestionnaires d'exception larges
|
|
- `sync_observer.py` a des gestionnaires d'exception larges
|
|
|
|
**Missing Validation**: La logique de validation précédemment manquante pour la transformation des payloads dans `sync_manager.py` a été implémentée.
|
|
|
|
## Architecture
|
|
|
|
### Flux Global
|
|
```mermaid
|
|
flowchart TD
|
|
A[Instance Source] -->|1. Détection\nModification| B(SyncManager)
|
|
B -->|2. Enqueue| C[(SyncQueue)]
|
|
C -->|3. Worker| D{Connecteur\nDestination}
|
|
D -->|4a. Succès| E[(SyncLog)]
|
|
D -->|4b. Erreur| F[Retry Policy]
|
|
F -->|Retry| C
|
|
E --> G[Dashboards]
|
|
```
|
|
|
|
### Séquence de Synchronisation
|
|
```mermaid
|
|
sequenceDiagram
|
|
participant Source as Instance A
|
|
participant Manager as SyncManager
|
|
participant Queue as SyncQueue
|
|
participant Dest as Instance B
|
|
|
|
Source->>Manager: Notify Change (webhook)
|
|
Manager->>Queue: Create SyncRecord
|
|
loop Worker Process
|
|
Queue->>Manager: Dequeue
|
|
Manager->>Dest: Prepare Payload
|
|
Dest-->>Manager: Transform/Validate
|
|
Manager->>Dest: Apply Changes
|
|
Dest-->>Manager: ACK/NACK
|
|
alt Success
|
|
Manager->>Queue: Mark Success
|
|
Manager->>Log: Write Audit
|
|
else Error
|
|
Manager->>Queue: Increment Retry
|
|
Manager->>Log: Write Error
|
|
end
|
|
end
|
|
```
|
|
|
|
## Caractéristiques Principales
|
|
|
|
### 1. Architecture Multi-Instances et Asynchrone
|
|
- Support de connexions multiples vers différentes instances Odoo
|
|
- Insertion automatique dans la queue via surcharge des méthodes create/write/unlink
|
|
- Traitement en arrière-plan des synchronisations via un worker dédié
|
|
- Possibilité de synchronisation immédiate pour les cas critiques
|
|
|
|
### 2. Configuration
|
|
- Mapping des champs configurable par modèle, par relation odoo-odoo
|
|
- Gestion automatique des dépendances entre modèles
|
|
- Paramètres de connexion sécurisés pour chaque instance
|
|
|
|
### 3. Mécanismes de Validation
|
|
- Validation des données avant synchronisation
|
|
- Vérification de l'intégrité des données
|
|
- Gestion des conflits de synchronisation
|
|
- Journalisation détaillée des opérations
|
|
|
|
### 4. Système de Reprise
|
|
- Détection automatique des échecs
|
|
- File d'attente des tentatives échouées
|
|
- Stratégie de réessai configurable
|
|
- Notification des erreurs critiques
|
|
|
|
### 5. Monitoring
|
|
- Interface de suivi des synchronisations
|
|
- Statistiques de performance
|
|
- Journal des erreurs
|
|
- Alertes configurables
|
|
|
|
## Configuration et Observers
|
|
|
|
### Instances Odoo (odoo.sync.instance)
|
|
Configuration des connexions aux instances distantes :
|
|
|
|
```python
|
|
class OdooSyncInstance(models.Model):
|
|
_name = 'odoo.sync.instance'
|
|
_description = 'Instance Odoo distante'
|
|
|
|
name = fields.Char('Nom de l\'instance', required=True)
|
|
url = fields.Char('URL de l\'instance', required=True)
|
|
database = fields.Char('Base de données', required=True)
|
|
api_token = fields.Char('API Token', required=True, help="Token API généré dans l'instance distante")
|
|
active = fields.Boolean('Instance active', default=True)
|
|
state = fields.Selection([
|
|
('connected', 'Connecté'),
|
|
('disconnected', 'Déconnecté'),
|
|
('error', 'Erreur')
|
|
], default='disconnected')
|
|
|
|
def _get_rpc_connection(self):
|
|
"""Connexion XML-RPC avec API token"""
|
|
return xmlrpc.client.ServerProxy(
|
|
f"{self.url}/xmlrpc/2/object",
|
|
context=ssl._create_unverified_context()
|
|
)
|
|
|
|
def _authenticate(self):
|
|
"""Authentification via API token"""
|
|
common = xmlrpc.client.ServerProxy(f"{self.url}/xmlrpc/2/common")
|
|
return common.authenticate_api_key(self.database, self.api_token)
|
|
|
|
def test_connection(self):
|
|
"""Test de connexion à l'instance"""
|
|
try:
|
|
uid = self._authenticate()
|
|
if uid:
|
|
self.state = 'connected'
|
|
return True
|
|
else:
|
|
self.state = 'error'
|
|
return False
|
|
except Exception as e:
|
|
self.state = 'error'
|
|
raise UserError(f"Erreur de connexion : {str(e)}")
|
|
```
|
|
|
|
### Modèles Synchronisés (odoo.sync.model)
|
|
Configuration des modèles à synchroniser :
|
|
|
|
- `model_id` : Référence vers ir.model
|
|
- `name` : Nom du modèle (computed)
|
|
- `odoo_id` : Mapping avec odoo.sync.instance
|
|
- `active` : Synchronisation active/inactive
|
|
- `priority` : Ordre de synchronisation pour les dépendances
|
|
|
|
### Champs Synchronisés (odoo.sync.model.field)
|
|
Configuration des champs par modèle :
|
|
|
|
- `field_id` : Référence vers ir.model.fields
|
|
- `name` : Nom technique du champ (computed)
|
|
- `required` : Champ obligatoire pour la synchronisation
|
|
- `sync_default` : Valeur par défaut si non disponible
|
|
- Exclusion des champs calculés (sauf si modifiables manuellement)
|
|
|
|
### Destinations (odoo.sync.model.destination)
|
|
Configuration des destinations par modèle :
|
|
|
|
- `model_sync_id` : Référence vers odoo.sync.model
|
|
- `instance_id` : Référence vers odoo.sync.instance
|
|
- `target_model` : Modèle cible sur l'instance distante
|
|
- `active` : Synchronisation active pour cette destination
|
|
- `field_ids` : Champs à synchroniser pour cette destination
|
|
|
|
## Gestionnaire de Synchronisation (odoo.sync.manager)
|
|
|
|
```python
|
|
class OdooSyncManager(models.Model):
|
|
_name = 'odoo.sync.manager'
|
|
_description = 'Gestionnaire de synchronisation Odoo'
|
|
|
|
@api.model
|
|
def _get_sync_models(self):
|
|
"""Récupère tous les modèles actifs à synchroniser"""
|
|
return self.env['odoo.sync.model'].search([('active', '=', True)])
|
|
|
|
@api.model
|
|
def _queue_sync(self, record, operation, changed_fields=None):
|
|
"""Ajoute une opération dans la queue de synchronisation"""
|
|
sync_model = self.env['odoo.sync.model'].search([
|
|
('model_id.model', '=', record._name),
|
|
('active', '=', True)
|
|
])
|
|
if not sync_model:
|
|
return
|
|
|
|
# Pour chaque destination configurée
|
|
for destination in sync_model.destination_ids.filtered('active'):
|
|
# Récupérer les champs configurés pour cette destination
|
|
sync_fields = destination.field_ids
|
|
|
|
# En cas de mise à jour, vérifier si les champs modifiés sont à synchroniser
|
|
if operation == 'write' and changed_fields:
|
|
relevant_fields = set(changed_fields) & set(sync_fields.mapped('name'))
|
|
if not relevant_fields:
|
|
continue # Aucun champ modifié n'est à synchroniser
|
|
|
|
# Préparer les données à synchroniser
|
|
sync_data = {}
|
|
for field in sync_fields:
|
|
if field.mapping_type == 'direct':
|
|
sync_data[field.name] = record[field.name]
|
|
elif field.mapping_type == 'function' and field.mapping_function:
|
|
# Appel de la fonction de transformation
|
|
sync_data[field.name] = getattr(record, field.mapping_function)()
|
|
elif field.mapping_type == 'computed':
|
|
# Gestion spéciale pour les champs computed si nécessaire
|
|
sync_data[field.name] = record[field.name]
|
|
|
|
# Créer l'entrée dans la queue
|
|
self.env['odoo.sync.queue'].create({
|
|
'model_id': sync_model.model_id.id,
|
|
'resource_id': record.id,
|
|
'other_odoo_id': destination.instance_id.id,
|
|
'other_odoo_resource_id': record.get_external_id().get(record.id),
|
|
'type': operation,
|
|
'state': 'pending',
|
|
'data_json': json.dumps(sync_data),
|
|
'create_date': record.create_date,
|
|
'write_date': record.write_date
|
|
})
|
|
|
|
def _process_sync_queue(self):
|
|
"""Traitement de la queue de synchronisation"""
|
|
jobs = self.env['odoo.sync.queue'].search([
|
|
('state', '=', 'pending'),
|
|
('retry_count', '<', 3)
|
|
], limit=100)
|
|
|
|
for job in jobs:
|
|
try:
|
|
instance = job.other_odoo_id
|
|
if not instance.active or instance.state != 'connected':
|
|
continue
|
|
|
|
# Exécuter la synchronisation
|
|
self._execute_sync(job)
|
|
job.write({'state': 'done'})
|
|
|
|
except Exception as e:
|
|
job.write({
|
|
'state': 'failed',
|
|
'error_message': str(e),
|
|
'retry_count': job.retry_count + 1
|
|
})
|
|
|
|
def _execute_sync(self, job):
|
|
"""Exécution d'une synchronisation"""
|
|
instance = job.other_odoo_id
|
|
rpc = instance._get_rpc_connection()
|
|
uid = instance._authenticate()
|
|
|
|
if not uid:
|
|
raise ValueError("Échec d'authentification")
|
|
|
|
data = json.loads(job.data_json)
|
|
|
|
if job.type == 'create':
|
|
result = rpc.execute_kw(
|
|
instance.database, uid, instance.api_token,
|
|
job.model_id.model, 'create', [data]
|
|
)
|
|
job.other_odoo_resource_id = result
|
|
|
|
elif job.type == 'write':
|
|
rpc.execute_kw(
|
|
instance.database, uid, instance.api_token,
|
|
job.model_id.model, 'write',
|
|
[[job.other_odoo_resource_id], data]
|
|
)
|
|
|
|
elif job.type == 'unlink':
|
|
rpc.execute_kw(
|
|
instance.database, uid, instance.api_token,
|
|
job.model_id.model, 'unlink', [job.other_odoo_resource_id]
|
|
)
|
|
```
|
|
|
|
## Modèles de Données
|
|
|
|
### SyncQueue
|
|
Table principale pour la gestion des synchronisations :
|
|
|
|
- `model_id` : Modèle Odoo à synchroniser
|
|
- `resource_id` : ID de la ressource locale
|
|
- `other_odoo_id` : ID de l'instance Odoo distante
|
|
- `other_odoo_resource_id` : ID de la ressource sur l'instance distante
|
|
- `type` : Type d'opération (create, update, unlink)
|
|
- `state` : État de la synchronisation
|
|
- `retry_count` : Nombre de tentatives
|
|
- `last_error` : Dernière erreur rencontrée
|
|
- `data_json` : Données à synchroniser au format JSON
|
|
- `create_date` : Date de création dans la queue
|
|
- `write_date` : Date de dernière modification
|
|
- `other_create_date` : Date de création sur l'instance distante
|
|
- `other_write_date` : Date de dernière modification sur l'instance distante
|
|
|
|
### SyncLog
|
|
- Journal détaillé des opérations
|
|
- Erreurs et avertissements
|
|
- Statistiques de performance
|
|
|
|
## Gestion des Conflits
|
|
|
|
### Détection
|
|
- Comparaison des horodatages `write_date` (source) vs `other_write_date` (cible)
|
|
- Seuil de tolérance configurable (défaut : 5 minutes)
|
|
|
|
### Stratégies de Résolution
|
|
1. **Priorité source** : Écrasement de la version cible
|
|
2. **Priorité destination** : Conservation de la version cible
|
|
3. **Fusion manuelle** :
|
|
- Notification aux administrateurs
|
|
- Interface de comparaison côte-à-côte
|
|
- Historique des versions (diff)
|
|
|
|
### Cas Particuliers
|
|
- Réconciliation des relations Many2many/One2many
|
|
- Gestion des suppressions/archivages croisés
|
|
|
|
## Sécurité des Données
|
|
|
|
### Authentification
|
|
```python
|
|
class OdooSyncInstance(models.Model):
|
|
_inherit = 'odoo.sync.instance'
|
|
|
|
def _secure_connection(self):
|
|
"""Configuration de sécurité pour les connexions"""
|
|
return {
|
|
'use_ssl': True,
|
|
'verify_ssl': True,
|
|
'timeout': 30,
|
|
'headers': {
|
|
'User-Agent': 'Odoo-Sync/1.0',
|
|
'X-API-Key': self.api_token
|
|
}
|
|
}
|
|
```
|
|
|
|
### Gestion des Accès
|
|
**API Tokens Odoo natifs** :
|
|
- Utilisation du système d'authentification intégré d'Odoo
|
|
- Révocation simple via l'interface utilisateur
|
|
- Audit automatique des accès
|
|
- Permissions granulaires via les groupes de sécurité existants
|
|
|
|
**Avantages** :
|
|
- Intégration native avec le système de sécurité Odoo
|
|
- Pas de complexité supplémentaire (JWT/OAuth2)
|
|
- Gestion centralisée des tokens
|
|
- Logs d'accès automatiques dans `res.users.log`
|
|
|
|
### Chiffrement
|
|
- TLS 1.3 obligatoire pour les communications
|
|
- Rotation automatique des certificats
|
|
- Chiffrement AES-256 au repos pour :
|
|
- `SyncQueue.data_json`
|
|
- `SyncLog.payload`
|
|
|
|
### Audit
|
|
- Logs d'accès horodatés avec IP/user-agent
|
|
- Intégration SIEM possible
|
|
- Journal des modifications sensibles
|
|
|
|
## Groupes de Sécurité
|
|
|
|
```xml
|
|
<record id="group_sync_user" model="res.groups">
|
|
<field name="name">Synchronisation : Utilisateur</field>
|
|
<field name="category_id" ref="base.module_category_usability"/>
|
|
</record>
|
|
|
|
<record id="group_sync_manager" model="res.groups">
|
|
<field name="name">Synchronisation : Manager</field>
|
|
<field name="implied_ids" eval="[(4, ref('group_sync_user'))]"/>
|
|
</record>
|
|
```
|
|
|
|
## Performance à l'Échelle
|
|
|
|
### Architecture Scalable
|
|
- File d'attente Redis pour découplage
|
|
- Scaling horizontal via Kubernetes
|
|
- Partitionnement par modèle/instance
|
|
|
|
### Optimisations
|
|
- Cache des relations fréquemment accédées
|
|
- Compression LZ4 des payloads volumineux
|
|
- Traitement batch avec isolation transactionnelle
|
|
|
|
### Monitoring
|
|
Dashboard avec métriques :
|
|
- Débit (records/min)
|
|
- Latence (P50/P90/P99)
|
|
- Taux d'utilisation des workers
|
|
|
|
## Exemples de Configuration
|
|
|
|
### 1. Synchronisation des Produits
|
|
```python
|
|
# Configuration du modèle
|
|
product_sync = {
|
|
'model': 'product.template',
|
|
'fields': {
|
|
'name': {'type': 'direct'},
|
|
'list_price': {'type': 'direct'},
|
|
'standard_price': {
|
|
'type': 'function',
|
|
'mapping': 'map_cost_price'
|
|
},
|
|
'categ_id': {
|
|
'type': 'relation',
|
|
'model': 'product.category',
|
|
'match_field': 'name'
|
|
}
|
|
},
|
|
'conflict_strategy': 'newest'
|
|
}
|
|
```
|
|
|
|
### 2. Synchronisation des Commandes
|
|
```python
|
|
# Configuration du modèle
|
|
sale_sync = {
|
|
'model': 'sale.order',
|
|
'fields': {
|
|
'name': {'type': 'direct'},
|
|
'partner_id': {
|
|
'type': 'relation',
|
|
'model': 'res.partner',
|
|
'match_field': 'ref'
|
|
},
|
|
'order_line': {
|
|
'type': 'one2many',
|
|
'fields': ['product_id', 'quantity', 'price_unit']
|
|
}
|
|
},
|
|
'deletion_strategy': 'soft',
|
|
'conflict_strategy': 'manual'
|
|
}
|
|
```
|
|
|
|
## Procédures de Déploiement
|
|
|
|
### Prérequis
|
|
- Odoo 15.0+
|
|
- Accès API aux instances distantes
|
|
- Génération d'API tokens sur les instances cibles
|
|
|
|
### Installation
|
|
1. Copier le répertoire `odoo_to_odoo_sync` dans `addons/`
|
|
2. Redémarrer le serveur Odoo
|
|
3. Installer le module via l'interface d'administration
|
|
4. Configurer les API tokens pour chaque instance
|
|
|
|
### Configuration
|
|
```ini
|
|
# Configuration de base dans odoo.conf
|
|
[odoo_sync]
|
|
max_retries = 3
|
|
retry_delay = 300 # secondes
|
|
queue_size = 1000
|
|
debug = False
|
|
```
|
|
|
|
### Tests
|
|
```bash
|
|
# Lancer les tests d'intégration
|
|
$ ./odoo-bin -i odoo_to_odoo_sync --test-enable
|
|
```
|
|
|
|
## Scénarios de Test Critiques
|
|
|
|
### TC-01 : Synchronisation bidirectionnelle
|
|
**Préconditions:**
|
|
- 2 instances interconnectées
|
|
- Modèle 'res.partner' configuré
|
|
|
|
**Étapes:**
|
|
1. Créer partenaire sur Instance A
|
|
2. Vérifier création sur Instance B
|
|
3. Modifier partenaire sur Instance B
|
|
4. Vérifier mise à jour sur Instance A
|
|
|
|
**Résultat attendu:**
|
|
- SyncLog avec code SYNC_200 sur les deux instances
|
|
- Données cohérentes après boucle complète
|
|
|
|
### TC-02 : Gestion des conflits
|
|
**Préconditions:**
|
|
- Même enregistrement modifié simultanément sur les deux instances
|
|
|
|
**Étapes:**
|
|
1. Modifier le champ 'name' sur Instance A
|
|
2. Modifier le champ 'email' sur Instance B
|
|
3. Déclencher manuellement la synchronisation
|
|
|
|
**Résultat attendu:**
|
|
- Application de la stratégie de résolution configurée
|
|
- Journalisation du conflit (SYNC_409)
|
|
|
|
### TC-03 : Tolérance aux pannes
|
|
**Préconditions:**
|
|
- Instance B hors ligne
|
|
|
|
**Étapes:**
|
|
1. Tenter une synchronisation
|
|
2. Redémarrer Instance B
|
|
3. Relancer la synchronisation
|
|
|
|
**Résultat attendu:**
|
|
- Rejeu automatique des transactions en erreur
|
|
- Conservation des données en queue pendant 24h
|
|
|
|
## Maintenance
|
|
|
|
### Outils de diagnostic
|
|
- Interface de monitoring en temps réel
|
|
- Logs détaillés par opération
|
|
- Métriques de performance
|
|
|
|
### Nettoyage automatique des logs
|
|
- Rétention configurable (défaut: 90 jours)
|
|
- Archivage automatique des anciennes données
|
|
- Compression des logs volumineux
|
|
|
|
### Gestion des sauvegardes
|
|
- Sauvegarde automatique de la configuration
|
|
- Export/import des paramètres de synchronisation
|
|
- Rollback en cas de problème
|
|
|
|
### Procédures de mise à jour
|
|
- Tests automatisés avant déploiement
|
|
- Migration des données existantes
|
|
- Documentation des changements% |