odoo to odoo

This commit is contained in:
xtremxpert 2025-02-14 07:41:09 -05:00
parent 1b95a6c2ea
commit cc66512df2
18 changed files with 1823 additions and 0 deletions

View file

@ -0,0 +1,676 @@
# 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.
## Architecture
### Flux Global
```mermaid
%%{init: {'theme': 'neutral'}}%%
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
## Flux de Synchronisation
1. **Détection des Changements**
- Surveillance des modifications sur les modèles configurés
- Création d'une entrée dans la file de synchronisation
2. **Validation Initiale**
- Vérification des données à synchroniser
- Validation des dépendances
3. **Synchronisation**
- Envoi des données via XML-RPC
- Gestion des réponses et erreurs
4. **Validation Finale**
- Vérification de la synchronisation
- Confirmation de l'intégrité
5. **Journalisation**
- Enregistrement du résultat
- Mise à jour des statistiques
## Architecture Technique
```plantuml
@startuml
skinparam monochrome true
package "Configuration" {
[Instances Odoo] <<(E,LightGreen)>>
[Modèles Sync] <<(E,LightGreen)>>
[Destinations] <<(E,LightGreen)>>
}
package "Orchestration" {
[Queue] <<(Q,LightBlue)>>
[Worker] <<(Q,LightBlue)>>
[Scheduler] <<(Q,LightBlue)>>
}
package "Connectivité" {
[Adaptateur RPC] <<(C,Orange)>>
[Sérialiseur] <<(C,Orange)>>
}
[Instances Odoo] --> [Modèles Sync]
[Modèles Sync] --> [Destinations]
[Queue] --> [Worker]
[Worker] --> [Adaptateur RPC]
[Adaptateur RPC] --> [Sérialiseur]
note right of [Sérialiseur]
Transformations de données
Gestion des dépendances
Mapping de champs
end note
@enduml
```
### Configuration et Observers
#### Instances Odoo (odoo.sync.instance)
Configuration des connexions aux instances distantes :
- `name` : Nom de l'instance
- `url` : URL de l'instance
- `database` : Base de données
- `username` : Utilisateur technique
- `password` : Mot de passe (chiffré)
- `active` : Instance active/inactive
- `state` : État de la connexion
#### 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), # Si déjà synchronisé
'type': operation,
'state': 'pending',
'data_json': json.dumps(sync_data),
'create_date': record.create_date,
'write_date': record.write_date
})
@api.model
def _observe_changes(self, method):
"""Décorateur pour observer les changements sur les modèles configurés"""
def wrapper(self, *args, **kwargs):
# Capturer les champs modifiés pour write
changed_fields = list(kwargs.get('vals', {}).keys()) if method.__name__ == 'write' else None
result = method(self, *args, **kwargs)
sync_manager = self.env['odoo.sync.manager']
if isinstance(result, models.Model):
for record in result:
sync_manager._queue_sync(record, method.__name__, changed_fields)
return result
return wrapper
# Application des observers sur les méthodes standard
models.Model.create = OdooSyncManager._observe_changes(models.Model.create)
models.Model.write = OdooSyncManager._observe_changes(models.Model.write)
models.Model.unlink = OdooSyncManager._observe_changes(models.Model.unlink)
### Template de Code pour le Gestionnaire
```python
class OdooSyncManager(models.Model):
_name = 'odoo.sync.manager'
def _process_sync_queue(self):
"""Template de traitement de la queue"""
jobs = self.env['odoo.sync.job'].search([('state', '=', 'pending')])
for job in jobs:
try:
# Logique de 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):
"""Template d'exécution d'une synchronisation"""
adapter = self._get_rpc_adapter(job.instance_id)
serializer = self._get_serializer(job.model_id)
data = serializer.serialize(job.record_id)
response = adapter.execute(job.operation, data)
if not response['success']:
raise SyncException(response['error_code'])
```
## Modèles de Données
### SyncConfiguration
#### Configuration des Instances (odoo.sync.instance)
- Nom de l'instance
- URL de l'instance
- Base de données
- Identifiants de connexion sécurisés
- État de la connexion
#### Configuration des Modèles (odoo.sync.model)
- Modèle Odoo à synchroniser
- Liste des instances Odoo cibles
- Mapping des champs
- Direction de la synchronisation (uni/bidirectionnelle)
- Champs à surveiller
- Règles de synchronisation spécifiques
### 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
### Journalisation Avancée (SyncLog)
#### Niveaux de Log
- **DEBUG**: Payloads complets et traces d'exécution
- **INFO**: Diffs des modifications et métadonnées
- **WARNING**: Erreurs non critiques (ex: timeouts)
- **ERROR**: Échecs critiques de synchronisation
#### Politique de Rétention
- Stockage local: 90 jours (accès rapide)
- Archivage long terme: AWS S3 Glacier (7 ans)
- Format d'archivage: Parquet compressé
#### Masquage des Données Sensibles
Fonction de masquage automatique :
```python
def sanitize_log_entry(entry):
sensitive_fields = ['password', 'api_key', 'token']
for field in sensitive_fields:
if field in entry['data']:
entry['data'][field] = '*****'
return entry
```
### Sécurité des Données
#### Chiffrement
- TLS 1.3 obligatoire pour les communications
- Rotation automatique des certificats (Let's Encrypt)
- Chiffrement AES-256 au repos pour :
- SyncQueue.data_json
- SyncLog.payload
#### Gestion des Accès
- Authentification mutuelle OAuth2 avec JWT :
```python
# Génération de token sécurisé
def generate_jwt(secret, payload):
return jwt.encode(payload, secret, algorithm="HS256")
```
- RBAC (Role-Based Access Control) :
- Rôle 'Sync Admin' : Configuration complète
- Rôle 'Sync Auditor' : Lecture seule
#### Audit
- Logs d'accès horodatés avec IP/user-agent
- Intégration SIEM (ex: Splunk, ELK)
## Sécurité
- Authentification sécurisée entre instances
- Encryption des données sensibles
- Validation des permissions
- Audit des opérations
## Interface Utilisateur
- Configuration des synchronisations
- Monitoring en temps réel
- Gestion des erreurs
- Rapports et statistiques
## Performance
- Optimisation des requêtes
- Gestion de la charge
- Limitation des appels API
- Mise en cache intelligente
## 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 Grafana avec :
- Débit (records/min)
- Latence (P50/P90/P99)
- Taux d'utilisation des workers
## Maintenance
- Outils de diagnostic
- Nettoyage automatique des logs
- Gestion des sauvegardes
- Procédures de mise à jour
## Gestion des Conflits de Synchronisation
### Détection des Conflits
- **Conflit de Version** : Détecté lorsque la version locale et distante ont été modifiées depuis la dernière synchronisation
- **Conflit de Données** : Détecté lorsque les mêmes champs ont été modifiés différemment sur les deux instances
- **Conflit de Relations** : Détecté lorsque des enregistrements liés sont incohérents entre les instances
### Stratégies de Résolution
1. **Automatique**
- Priorité configurable par instance (master/slave)
- Règles de fusion personnalisables par champ
- Horodatage "le plus récent gagne"
2. **Manuelle**
- Interface de résolution pour l'utilisateur
- Visualisation côte à côte des différences
- Options : garder source, garder destination, fusionner, ignorer
### Configuration des Règles de Résolution
```python
class OdooSyncModelField(models.Model):
_inherit = 'odoo.sync.model.field'
conflict_strategy = fields.Selection([
('source_wins', 'Source gagne'),
('dest_wins', 'Destination gagne'),
('newest', 'Plus récent'),
('manual', 'Résolution manuelle')
], default='newest')
```
## Gestion des Suppressions
### Stratégies de Suppression
1. **Suppression Douce**
- Marquage comme inactif sur les deux instances
- Conservation de l'historique
- Possibilité de restauration
2. **Suppression Dure**
- Suppression physique sur les deux instances
- Vérification des dépendances
- Journal d'audit détaillé
### Configuration
```python
class OdooSyncModel(models.Model):
_inherit = 'odoo.sync.model'
deletion_strategy = fields.Selection([
('soft', 'Suppression douce'),
('hard', 'Suppression physique'),
('ignore', 'Ignorer'),
('manual', 'Validation manuelle')
], default='soft')
cascade_deletion = fields.Boolean('Cascade aux enregistrements liés')
```
## Sécurité et Droits d'Accès
### Niveaux de Sécurité
1. **Niveau Instance**
- Authentification par token JWT
- Chiffrement des communications
- Restriction par IP
2. **Niveau Utilisateur**
- Groupes de sécurité dédiés
- Journalisation des actions
- Validation multi-niveau
### 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>
```
### Règles de Sécurité
```xml
<record id="rule_sync_model_manager" model="ir.rule">
<field name="name">Sync Manager : Accès Total</field>
<field name="model_id" ref="model_odoo_sync_model"/>
<field name="groups" eval="[(4, ref('group_sync_manager'))]"/>
<field name="domain_force">[(1, '=', 1)]</field>
</record>
```
## 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'
}
# Fonction de mapping personnalisée
def map_cost_price(self, record):
return record.standard_price * self.currency_rate
```
### 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'
}
```
### 3. Interface de Configuration
```xml
<record id="view_sync_config_form" model="ir.ui.view">
<field name="name">odoo.sync.config.form</field>
<field name="model">odoo.sync.model</field>
<field name="arch" type="xml">
<form>
<group>
<field name="model_id"/>
<field name="active"/>
<field name="deletion_strategy"/>
</group>
<notebook>
<page string="Champs">
<field name="field_ids">
<tree editable="bottom">
<field name="field_id"/>
<field name="sync_type"/>
<field name="conflict_strategy"/>
</tree>
</field>
</page>
</notebook>
</form>
</field>
</record>
```
## 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
## Procédures de Déploiement
### Prérequis
- Odoo 15.0+
- Accès API aux instances distantes
- Bibliothèque python-requests
### 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
### Configuration
```python
# Configuration de base dans odoo.conf
[odoo_sync]
max_retries = 3
retry_delay = 300 # secondes
queue_size = 1000
# Activation du mode debug
debug = False
```
### Tests
```bash
# Lancer les tests d'intégration
$ ./odoo-bin -i odoo_to_odoo_sync --test-enable

View file

@ -0,0 +1 @@
from . import models

View file

@ -0,0 +1,28 @@
{
'name': 'Odoo to Odoo Sync',
'version': '18.0.1.0.0',
'category': 'Technical',
'summary': 'Synchronisation bidirectionnelle entre instances Odoo',
'description': """
Module de synchronisation bidirectionnelle entre instances Odoo
- Support multi-instances
- Synchronisation asynchrone
- Gestion des conflits
- Monitoring et reprise sur erreur
""",
'author': 'Bemade',
'website': 'https://bemade.org',
'depends': ['base'],
'data': [
'security/ir.model.access.csv',
'views/sync_instance_views.xml',
'views/sync_model_views.xml',
'views/sync_queue_views.xml',
'views/sync_log_views.xml',
'views/menus.xml',
'data/ir_cron_data.xml',
],
'installable': True,
'application': True,
'license': 'LGPL-3',
}

View file

@ -0,0 +1,15 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<data noupdate="1">
<record id="ir_cron_sync_queue_processor" model="ir.cron">
<field name="name">Traitement de la file de synchronisation</field>
<field name="model_id" ref="model_odoo_sync_queue"/>
<field name="state">code</field>
<field name="code">model._process_sync_queue()</field>
<field name="interval_number">5</field>
<field name="interval_type">minutes</field>
<field name="active" eval="True"/>
<field name="user_id" ref="base.user_root"/>
</record>
</data>
</odoo>

View file

@ -0,0 +1,6 @@
from . import sync_instance
from . import sync_model
from . import sync_model_field
from . import sync_log
from . import sync_queue
from . import sync_manager

View file

@ -0,0 +1,127 @@
# Copyright 2025 Codeium
# License LGPL-3.0 or later (https://www.gnu.org/licenses/lgpl-3.0.html)
"""Remote Odoo Instance Management.
This module handles the configuration and connection management for remote
Odoo instances. It provides functionality to store connection credentials,
test connections, and maintain the connection state with remote instances.
"""
import logging
import xmlrpc.client
from odoo import api, fields, models
_logger = logging.getLogger(__name__)
class OdooSyncInstance(models.Model):
"""Remote Odoo instance for synchronization.
This model stores connection information and credentials for remote Odoo instances
that will be synchronized with the current instance.
"""
_name = 'odoo.sync.instance'
_description = 'Remote Odoo Instance'
name = fields.Char(
string='Name',
required=True,
help='Name to identify this remote instance',
)
url = fields.Char(
string='URL',
required=True,
help='Base URL of the remote Odoo instance',
)
database = fields.Char(
string='Database',
required=True,
help='Database name on the remote instance',
)
username = fields.Char(
string='Technical User',
required=True,
help='Username of the technical user for synchronization',
)
password = fields.Char(
string='Password',
required=True,
help='Password of the technical user',
)
active = fields.Boolean(
string='Active',
default=True,
help='Whether this instance is active for synchronization',
)
state = fields.Selection(
selection=[
('draft', 'Draft'),
('testing', 'Testing Connection'),
('connected', 'Connected'),
('error', 'Error')
],
default='draft',
string='State',
help='Current state of the connection',
)
last_connection = fields.Datetime(
string='Last Connection',
help='Timestamp of the last successful connection',
)
error_message = fields.Text(
string='Error Message',
help='Details of the last error that occurred',
)
@api.onchange('url')
def _onchange_url(self):
"""Reset state when URL changes."""
if self.url != self._origin.url:
self.state = 'draft'
self.error_message = False
def test_connection(self):
self.ensure_one()
self.state = 'testing'
try:
common = xmlrpc.client.ServerProxy(f'{self.url}/xmlrpc/2/common')
uid = common.authenticate(self.database, self.username, self.password, {})
if uid:
self.write({
'state': 'connected',
'last_connection': fields.Datetime.now(),
'error_message': False
})
return True
else:
raise Exception('Échec d\'authentification')
except Exception as e:
self.write({
'state': 'error',
'error_message': str(e)
})
return False
def get_connection(self):
"""Retourne une connexion active à l'instance distante"""
self.ensure_one()
if self.state != 'connected':
self.test_connection()
if self.state != 'connected':
raise Exception(f'Impossible de se connecter à {self.name}: {self.error_message}')
common = xmlrpc.client.ServerProxy(f'{self.url}/xmlrpc/2/common')
uid = common.authenticate(self.database, self.username, self.password, {})
models = xmlrpc.client.ServerProxy(f'{self.url}/xmlrpc/2/object')
return models, uid

View file

@ -0,0 +1,50 @@
# Copyright 2025 Codeium
# License LGPL-3.0 or later (https://www.gnu.org/licenses/lgpl-3.0.html)
"""Synchronization Logging System.
This module implements the logging system for synchronization operations.
It tracks all synchronization attempts, their outcomes, and any errors
that occur during the process, providing a complete audit trail.
"""
import logging
from odoo import api, fields, models
_logger = logging.getLogger(__name__)
class OdooSyncLog(models.Model):
_name = 'odoo.sync.log'
_description = 'Synchronization Log'
_order = 'create_date desc'
name = fields.Char(
string='Name',
compute='_compute_name'
)
queue_id = fields.Many2one(
comodel_name='odoo.sync.queue',
string='Queue Entry',
required=True,
ondelete='cascade'
)
state = fields.Selection(
selection=[
('success', 'Success'),
('error', 'Error')
],
required=True
)
message = fields.Text(
string='Message',
)
details = fields.Text(
string='Technical Details'
)
@api.depends('queue_id', 'state')
def _compute_name(self):
for record in self:
record.name = f'{record.queue_id.name} - {record.state}'

View file

@ -0,0 +1,204 @@
# Copyright 2025 Codeium
# License LGPL-3.0 or later (https://www.gnu.org/licenses/lgpl-3.0.html)
"""Synchronization Management System.
This module implements the core synchronization logic and conflict resolution
strategies. It manages the overall synchronization process, including queuing
operations, handling retries, and ensuring data consistency across instances.
"""
import json
import logging
from datetime import datetime, timedelta
import xmlrpc.client
from odoo import fields, models
_logger = logging.getLogger(__name__)
class SyncConflictException(Exception):
"""Exception raised when a synchronization conflict is detected.
This exception is raised when the synchronization process encounters
conflicting changes between source and destination instances that
cannot be automatically resolved based on the current conflict
resolution strategy.
"""
pass
class OdooSyncManager(models.Model):
"""Manages synchronization operations between Odoo instances.
This class is responsible for orchestrating the synchronization process,
including:
- Managing synchronization queues
- Handling conflict resolution
- Coordinating data transfer between instances
- Monitoring synchronization status
- Implementing retry mechanisms
- Logging synchronization events
The synchronization process is configurable through various strategies
and can be customized based on specific business needs.
"""
_name = 'odoo.sync.manager'
_description = 'Odoo Sync Manager'
conflict_strategy = fields.Selection(
selection=[
('timestamp', 'Last Modified'),
('manual', 'Manual Resolution'),
('source_priority', 'Source Priority'),
('destination_priority', 'Destination Priority')
],
default='timestamp',
required=True
)
def _get_sync_models(self):
"""Récupère tous les modèles actifs à synchroniser"""
return self.env['odoo.sync.model'].search([('active', '=', True)])
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
# Préparation des données à synchroniser
data = record.read()[0] if operation != 'unlink' else {'id': record.id}
# Pour chaque destination configurée
for destination in sync_model.destination_ids.filtered('active'):
# Vérifier si les champs modifiés sont à synchroniser
if operation == 'write' and changed_fields:
sync_fields = destination.field_ids.mapped('name')
if not any(field in sync_fields for field in changed_fields):
continue
# Création de l'entrée dans la file
self.env['odoo.sync.queue'].create({
'model_id': sync_model.id,
'record_id': record.id,
'operation': operation,
'state': 'pending',
'priority': sync_model.priority,
'data': json.dumps(data)
})
def _process_sync_queue(self):
"""Traitement de la file d'attente"""
queue_items = self.env['odoo.sync.queue'].search([
('state', '=', 'pending'),
'|',
('next_retry', '<=', fields.Datetime.now()),
('next_retry', '=', False)
], order='priority desc, retry_count, create_date')
for item in queue_items:
try:
item.state = 'processing'
self._process_queue_item(item)
item.state = 'done'
except Exception as e:
_logger.error(f'Erreur lors du traitement de {item.name}: {str(e)}')
item.write({
'state': 'error',
'error_message': str(e),
'retry_count': item.retry_count + 1
})
if item.retry_count < item.max_retries:
delay = 2 ** item.retry_count # Délai exponentiel
item.next_retry = fields.Datetime.now() + timedelta(minutes=delay)
self.env['odoo.sync.log'].create({
'queue_id': item.id,
'state': 'error',
'message': str(e)
})
def _process_queue_item(self, item):
"""Traite un élément de la file d'attente"""
model = item.model_id
data = json.loads(item.data)
for destination in model.destination_ids.filtered('active'):
instance = destination.instance_id
models, uid = instance.get_connection()
try:
if item.operation == 'create':
result = models.execute_kw(
instance.database, uid, instance.password,
destination.target_model, 'create',
[self._prepare_sync_data(data, destination)]
)
elif item.operation == 'write':
result = models.execute_kw(
instance.database, uid, instance.password,
destination.target_model, 'write',
[[data['id']], self._prepare_sync_data(data, destination)]
)
elif item.operation == 'unlink':
result = models.execute_kw(
instance.database, uid, instance.password,
destination.target_model, 'unlink',
[[data['id']]]
)
self.env['odoo.sync.log'].create({
'queue_id': item.id,
'state': 'success',
'message': f'Synchronisation réussie vers {instance.name}'
})
except xmlrpc.client.Fault as e:
raise Exception(f'Erreur RPC vers {instance.name}: {str(e)}')
def _prepare_sync_data(self, data, destination):
"""Prépare les données pour la synchronisation"""
sync_fields = destination.field_ids
result = {}
for field in sync_fields:
if field.name in data:
value = data[field.name]
if not value and field.sync_default:
value = field.sync_default
result[field.name] = value
return result
def _handle_conflict(self, local_data, remote_data):
"""Gestion des conflits selon la stratégie configurée"""
strategy = self.conflict_strategy
_logger.info(f'Résolution de conflit avec stratégie: {strategy}')
if strategy == 'timestamp':
return self._resolve_by_timestamp(local_data, remote_data)
elif strategy == 'manual':
return self._flag_for_manual_resolution(local_data, remote_data)
elif strategy == 'source_priority':
return local_data
elif strategy == 'destination_priority':
return remote_data
def _resolve_by_timestamp(self, local, remote):
"""Résout un conflit en utilisant l'horodatage"""
local_date = fields.Datetime.from_string(local['write_date'])
remote_date = fields.Datetime.from_string(remote['write_date'])
return local if local_date > remote_date else remote
def _flag_for_manual_resolution(self, local, remote):
"""Marque un conflit pour résolution manuelle"""
self.env['odoo.sync.conflict'].create({
'local_data': json.dumps(local),
'remote_data': json.dumps(remote),
'model_name': local['model'],
'record_id': local['id'],
'state': 'pending'
})
raise SyncConflictException('Conflit nécessitant une résolution manuelle')

View file

@ -0,0 +1,94 @@
# Copyright 2025 Codeium
# License LGPL-3.0 or later (https://www.gnu.org/licenses/lgpl-3.0.html)
"""Model Synchronization Configuration.
This module defines how models are synchronized between Odoo instances.
It handles model mapping, field configuration, and synchronization rules
for each model that needs to be synchronized.
"""
import logging
from odoo import api, fields, models
_logger = logging.getLogger(__name__)
class OdooSyncModel(models.Model):
_name = 'odoo.sync.model'
_description = 'Synchronized Model'
_order = 'priority, id'
model_id = fields.Many2one(
comodel_name='ir.model',
string='Source Model',
required=True,
ondelete='cascade'
)
name = fields.Char(
related='model_id.model',
string='Technical Name',
store=True
)
instance_id = fields.Many2one(
comodel_name='odoo.sync.instance',
string='Remote Instance',
required=True
)
target_model = fields.Char(
string='Target Model',
help='Technical name of the model on remote instance'
)
active = fields.Boolean(
string='Active',
default=True
)
priority = fields.Integer(
string='Priority',
default=10,
help='Synchronization order for dependencies'
)
field_ids = fields.One2many(
comodel_name='odoo.sync.model.field',
inverse_name='model_sync_id',
string='Synchronized Fields'
)
_sql_constraints = [
('model_instance_uniq', 'unique(model_id, instance_id)',
'Un modèle ne peut être synchronisé qu\'une fois par instance!')
]
@api.model
def create(self, vals):
if not vals.get('target_model') and vals.get('model_id'):
# Par défaut, utiliser le même nom de modèle que la source
model = self.env['ir.model'].browse(vals['model_id'])
vals['target_model'] = model.model
record = super().create(vals)
# Créer automatiquement les champs de base
if record.model_id:
fields_to_sync = ['create_date', 'write_date', 'create_uid', 'write_uid']
for field_name in fields_to_sync:
field = self.env['ir.model.fields'].search([
('model_id', '=', record.model_id.id),
('name', '=', field_name)
])
if field:
self.env['odoo.sync.model.field'].create({
'model_sync_id': record.id,
'field_id': field.id,
'required': True
})
return record
def name_get(self):
return [(r.id, f'{r.name}{r.instance_id.name}') for r in self]

View file

@ -0,0 +1,41 @@
from odoo import models, fields, api
class OdooSyncModelField(models.Model):
_name = 'odoo.sync.model.field'
_description = 'Synchronized Field'
model_sync_id = fields.Many2one(
comodel_name='odoo.sync.model',
string='Synchronized Model',
required=True,
ondelete='cascade'
)
field_id = fields.Many2one(
comodel_name='ir.model.fields',
string='Field',
domain="[('model_id', '=', parent.model_id)]",
required=True,
ondelete='cascade'
)
name = fields.Char(
related='field_id.name',
string='Technical Name',
store=True
)
required = fields.Boolean(
string='Required',
default=False
)
sync_default = fields.Char(
string='Default Value',
help='Value to use if not available'
)
_sql_constraints = [
('field_uniq', 'unique(model_sync_id, field_id)',
'Un champ ne peut être synchronisé qu\'une fois par modèle!')
]

View file

@ -0,0 +1,204 @@
# Copyright 2025 Codeium
# License LGPL-3.0 or later (https://www.gnu.org/licenses/lgpl-3.0.html)
"""Synchronization Queue Management.
This module implements the queue system for managing synchronization operations
between Odoo instances. It handles the scheduling, retry logic, and status
tracking of synchronization tasks.
"""
import json
import logging
from odoo import api, fields, models
_logger = logging.getLogger(__name__)
class OdooSyncQueue(models.Model):
_name = 'odoo.sync.queue'
_description = 'Synchronization Queue'
_order = 'priority desc, retry_count, create_date'
name = fields.Char(
string='Name',
compute='_compute_name',
store=True
)
model_id = fields.Many2one(
comodel_name='odoo.sync.model',
string='Model',
required=True
)
record_id = fields.Integer(
string='Record ID',
required=True
)
operation = fields.Selection(
selection=[
('create', 'Create'),
('write', 'Update'),
('unlink', 'Delete')
],
required=True
)
state = fields.Selection(
selection=[
('draft', 'Draft'),
('pending', 'Pending'),
('processing', 'Processing'),
('done', 'Done'),
('error', 'Error'),
('cancelled', 'Cancelled')
],
default='draft',
required=True
)
priority = fields.Integer(
string='Priority',
default=1
)
retry_count = fields.Integer(
string='Retry Count',
default=0
)
max_retries = fields.Integer(
string='Max Retries',
default=3
)
next_retry = fields.Datetime(
string='Next Retry'
)
data = fields.Text(
string='Data',
help='JSON data to synchronize'
)
error_message = fields.Text(
string='Error Message'
)
log_ids = fields.One2many(
comodel_name='odoo.sync.log',
inverse_name='queue_id',
string='Logs'
)
@api.depends('model_id', 'record_id', 'operation')
def _compute_name(self):
"""Compute the display name of the queue entry.
The name is generated using the format: 'operation - model_name#record_id'
e.g., 'create - res.partner#42'
Triggered by changes to: model_id, record_id, or operation fields.
"""
for record in self:
record.name = f'{record.operation} - {record.model_id.name}#{record.record_id}'
def action_reset(self):
"""Reset a failed queue entry to pending state.
This method:
- Changes state back to 'pending'
- Resets retry count to 0
- Clears error message and next retry time
Returns:
dict: Result of the write operation
"""
return self.write({
'state': 'pending',
'retry_count': 0,
'error_message': False,
'next_retry': False
})
def action_cancel(self):
"""Cancel the queue entry.
Changes the state of the queue entry to 'cancelled',
preventing further processing attempts.
Returns:
dict: Result of the write operation
"""
return self.write({'state': 'cancelled'})
@api.model
def _process_sync_queue(self):
"""Process pending synchronization queue entries.
This method is called by the cron job to process pending queue entries.
It will:
1. Find pending entries that are ready for processing
2. Process each entry according to its operation type
3. Update the entry status and create log entries
Returns:
bool: True if processing completed successfully
"""
# Find pending entries ready for processing
domain = [
('state', '=', 'pending'),
'|',
('next_retry', '=', False),
('next_retry', '<=', fields.Datetime.now())
]
entries = self.search(domain, order='priority desc, retry_count, create_date')
for entry in entries:
try:
# Mark as processing
entry.write({'state': 'processing'})
# TODO: Implement actual synchronization logic here
# This will be implemented in a future update
# For now, just mark as done
entry.write({'state': 'done'})
# Create success log
self.env['odoo.sync.log'].create({
'queue_id': entry.id,
'state': 'success',
'message': 'Synchronization completed successfully'
})
except Exception as e:
_logger.error('Error processing queue entry %s: %s', entry.name, str(e))
# Update retry count and status
vals = {
'state': 'error',
'retry_count': entry.retry_count + 1,
'error_message': str(e)
}
# Schedule next retry if not exceeded max retries
if entry.retry_count < entry.max_retries:
vals.update({
'state': 'pending',
'next_retry': fields.Datetime.now() + timedelta(minutes=5 * (entry.retry_count + 1))
})
entry.write(vals)
# Create error log
self.env['odoo.sync.log'].create({
'queue_id': entry.id,
'state': 'error',
'message': str(e),
'details': str(e)
})
return True

View file

@ -0,0 +1,13 @@
id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
access_odoo_sync_instance_user,odoo.sync.instance.user,model_odoo_sync_instance,base.group_user,1,0,0,0
access_odoo_sync_instance_admin,odoo.sync.instance.admin,model_odoo_sync_instance,base.group_system,1,1,1,1
access_odoo_sync_model_user,odoo.sync.model.user,model_odoo_sync_model,base.group_user,1,0,0,0
access_odoo_sync_model_admin,odoo.sync.model.admin,model_odoo_sync_model,base.group_system,1,1,1,1
access_odoo_sync_model_field_user,odoo.sync.model.field.user,model_odoo_sync_model_field,base.group_user,1,0,0,0
access_odoo_sync_model_field_admin,odoo.sync.model.field.admin,model_odoo_sync_model_field,base.group_system,1,1,1,1
access_odoo_sync_queue_user,odoo.sync.queue.user,model_odoo_sync_queue,base.group_user,1,1,1,0
access_odoo_sync_queue_admin,odoo.sync.queue.admin,model_odoo_sync_queue,base.group_system,1,1,1,1
access_odoo_sync_log_user,odoo.sync.log.user,model_odoo_sync_log,base.group_user,1,1,1,0
access_odoo_sync_log_admin,odoo.sync.log.admin,model_odoo_sync_log,base.group_system,1,1,1,1
access_odoo_sync_manager_user,odoo.sync.manager.user,model_odoo_sync_manager,base.group_user,1,0,0,0
access_odoo_sync_manager_admin,odoo.sync.manager.admin,model_odoo_sync_manager,base.group_system,1,1,1,1
1 id name model_id:id group_id:id perm_read perm_write perm_create perm_unlink
2 access_odoo_sync_instance_user odoo.sync.instance.user model_odoo_sync_instance base.group_user 1 0 0 0
3 access_odoo_sync_instance_admin odoo.sync.instance.admin model_odoo_sync_instance base.group_system 1 1 1 1
4 access_odoo_sync_model_user odoo.sync.model.user model_odoo_sync_model base.group_user 1 0 0 0
5 access_odoo_sync_model_admin odoo.sync.model.admin model_odoo_sync_model base.group_system 1 1 1 1
6 access_odoo_sync_model_field_user odoo.sync.model.field.user model_odoo_sync_model_field base.group_user 1 0 0 0
7 access_odoo_sync_model_field_admin odoo.sync.model.field.admin model_odoo_sync_model_field base.group_system 1 1 1 1
8 access_odoo_sync_queue_user odoo.sync.queue.user model_odoo_sync_queue base.group_user 1 1 1 0
9 access_odoo_sync_queue_admin odoo.sync.queue.admin model_odoo_sync_queue base.group_system 1 1 1 1
10 access_odoo_sync_log_user odoo.sync.log.user model_odoo_sync_log base.group_user 1 1 1 0
11 access_odoo_sync_log_admin odoo.sync.log.admin model_odoo_sync_log base.group_system 1 1 1 1
12 access_odoo_sync_manager_user odoo.sync.manager.user model_odoo_sync_manager base.group_user 1 0 0 0
13 access_odoo_sync_manager_admin odoo.sync.manager.admin model_odoo_sync_manager base.group_system 1 1 1 1

View file

@ -0,0 +1 @@

View file

@ -0,0 +1,45 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<!-- Menu principal -->
<menuitem id="menu_sync_root"
name="Synchronisation"
sequence="100"
web_icon="odoo_to_odoo_sync,static/description/icon.png"/>
<!-- Sous-menus -->
<menuitem id="menu_sync_config"
name="Configuration"
parent="menu_sync_root"
sequence="10"/>
<menuitem id="menu_sync_monitoring"
name="Monitoring"
parent="menu_sync_root"
sequence="20"/>
<!-- Éléments de configuration -->
<menuitem id="menu_sync_instance"
name="Instances Odoo"
parent="menu_sync_config"
action="action_sync_instance"
sequence="10"/>
<menuitem id="menu_sync_model"
name="Modèles synchronisés"
parent="menu_sync_config"
action="action_sync_model"
sequence="20"/>
<!-- Éléments de monitoring -->
<menuitem id="menu_sync_queue"
name="File d'attente"
parent="menu_sync_monitoring"
action="action_sync_queue"
sequence="10"/>
<menuitem id="menu_sync_log"
name="Journaux"
parent="menu_sync_monitoring"
action="action_sync_log"
sequence="20"/>
</odoo>

View file

@ -0,0 +1,68 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<record id="view_sync_instance_list" model="ir.ui.view">
<field name="name">odoo.sync.instance.list</field>
<field name="model">odoo.sync.instance</field>
<field name="arch" type="xml">
<list>
<field name="name"/>
<field name="url"/>
<field name="database"/>
<field name="state" decoration-success="state == 'connected'"
decoration-warning="state == 'testing'"
decoration-danger="state == 'error'"/>
<field name="last_connection"/>
<field name="active" invisible="1"/>
</list>
</field>
</record>
<record id="view_sync_instance_form" model="ir.ui.view">
<field name="name">odoo.sync.instance.form</field>
<field name="model">odoo.sync.instance</field>
<field name="arch" type="xml">
<form>
<header>
<button name="test_connection" string="Tester la connexion"
type="object" class="btn-primary"/>
<field name="state" widget="statusbar"
statusbar_visible="draft,testing,connected"/>
</header>
<sheet>
<div class="oe_button_box" name="button_box">
<button name="toggle_active" type="object" class="oe_stat_button" icon="fa-archive">
<field name="active" widget="boolean_button"/>
</button>
</div>
<div class="oe_title">
<h1>
<field name="name" placeholder="Nom de l'instance"/>
</h1>
</div>
<group>
<group>
<field name="url" placeholder="https://example.odoo.com"/>
<field name="database"/>
</group>
<group>
<field name="username"/>
<field name="password" password="True"/>
<field name="last_connection" readonly="1"/>
</group>
</group>
<notebook invisible="error_message == False">
<page string="Messages d'erreur">
<field name="error_message" readonly="1"/>
</page>
</notebook>
</sheet>
</form>
</field>
</record>
<record id="action_sync_instance" model="ir.actions.act_window">
<field name="name">Instances Odoo</field>
<field name="res_model">odoo.sync.instance</field>
<field name="view_mode">list,form</field>
</record>
</odoo>

View file

@ -0,0 +1,51 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<record id="view_sync_log_list" model="ir.ui.view">
<field name="name">odoo.sync.log.list</field>
<field name="model">odoo.sync.log</field>
<field name="arch" type="xml">
<list>
<field name="create_date"/>
<field name="name"/>
<field name="queue_id"/>
<field name="state"/>
<field name="message"/>
</list>
</field>
</record>
<record id="view_sync_log_form" model="ir.ui.view">
<field name="name">odoo.sync.log.form</field>
<field name="model">odoo.sync.log</field>
<field name="arch" type="xml">
<form>
<sheet>
<group>
<group>
<field name="name"/>
<field name="queue_id"/>
<field name="state"/>
</group>
<group>
<field name="create_date"/>
</group>
</group>
<notebook>
<page string="Message">
<field name="message" nolabel="1"/>
</page>
<page string="Technical Details">
<field name="details" nolabel="1"/>
</page>
</notebook>
</sheet>
</form>
</field>
</record>
<record id="action_sync_log" model="ir.actions.act_window">
<field name="name">Journaux de synchronisation</field>
<field name="res_model">odoo.sync.log</field>
<field name="view_mode">list,form</field>
</record>
</odoo>

View file

@ -0,0 +1,69 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<record id="view_sync_model_list" model="ir.ui.view">
<field name="name">odoo.sync.model.list</field>
<field name="model">odoo.sync.model</field>
<field name="arch" type="xml">
<list>
<field name="model_id"/>
<field name="instance_id"/>
<field name="target_model"/>
<field name="priority"/>
<field name="active" invisible="1"/>
</list>
</field>
</record>
<record id="view_sync_model_form" model="ir.ui.view">
<field name="name">odoo.sync.model.form</field>
<field name="model">odoo.sync.model</field>
<field name="arch" type="xml">
<form>
<sheet>
<div class="oe_button_box" name="button_box">
<button name="toggle_active" type="object" class="oe_stat_button" icon="fa-archive">
<field name="active" widget="boolean_button"/>
</button>
</div>
<group>
<group>
<field name="model_id" options="{'no_create': True}"/>
<field name="instance_id" options="{'no_create': True}"/>
<field name="target_model" placeholder="Exemple: res.partner"/>
</group>
<group>
<field name="name" readonly="1"/>
<field name="priority"/>
</group>
</group>
<notebook>
<page string="Champs synchronisés">
<field name="field_ids" context="{'default_model_sync_id': id}">
<list editable="bottom">
<field name="field_id" options="{'no_create': True}"/>
<field name="name" readonly="1"/>
<field name="required"/>
<field name="sync_default" placeholder="Valeur par défaut si vide"/>
</list>
</field>
</page>
</notebook>
</sheet>
</form>
</field>
</record>
<record id="action_sync_model" model="ir.actions.act_window">
<field name="name">Modèles synchronisés</field>
<field name="res_model">odoo.sync.model</field>
<field name="view_mode">list,form</field>
<field name="help" type="html">
<p class="o_view_nocontent_smiling_face">
Aucun modèle synchronisé défini
</p>
<p>
Définissez les modèles à synchroniser entre vos instances Odoo.
</p>
</field>
</record>
</odoo>

View file

@ -0,0 +1,130 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<record id="view_sync_queue_list" model="ir.ui.view">
<field name="name">odoo.sync.queue.list</field>
<field name="model">odoo.sync.queue</field>
<field name="arch" type="xml">
<list>
<field name="name"/>
<field name="model_id"/>
<field name="record_id"/>
<field name="operation"/>
<field name="state" decoration-danger="state == 'error'"
decoration-info="state == 'processing'"
decoration-success="state == 'done'"/>
<field name="retry_count"/>
<field name="next_retry"/>
<field name="create_date"/>
</list>
</field>
</record>
<record id="view_sync_queue_form" model="ir.ui.view">
<field name="name">odoo.sync.queue.form</field>
<field name="model">odoo.sync.queue</field>
<field name="arch" type="xml">
<form>
<header>
<button name="action_reset" string="Réinitialiser" type="object"
invisible="state not in ('error', 'cancelled')"/>
<button name="action_cancel" string="Annuler" type="object"
invisible="state in ('done', 'cancelled')"/>
<field name="state" widget="statusbar"
statusbar_visible="draft,pending,processing,done"/>
</header>
<sheet>
<div class="oe_title">
<h1>
<field name="name"/>
</h1>
</div>
<group>
<group>
<field name="model_id" options="{'no_create': True}"/>
<field name="record_id"/>
<field name="operation"/>
</group>
<group>
<field name="priority"/>
<field name="retry_count"/>
<field name="max_retries"/>
<field name="next_retry"/>
</group>
</group>
<notebook>
<page string="Données">
<field name="data" widget="ace" options="{'mode': 'json'}"/>
</page>
<page string="Logs">
<field name="log_ids" readonly="1">
<list>
<field name="create_date"/>
<field name="state" decoration-danger="state == 'error'"
decoration-success="state == 'success'"/>
<field name="message"/>
</list>
</field>
</page>
<page string="Message d'erreur" invisible="error_message == False">
<field name="error_message" readonly="1"/>
</page>
</notebook>
</sheet>
</form>
</field>
</record>
<record id="view_sync_log_list" model="ir.ui.view">
<field name="name">odoo.sync.log.list</field>
<field name="model">odoo.sync.log</field>
<field name="arch" type="xml">
<list>
<field name="create_date"/>
<field name="queue_id"/>
<field name="state" decoration-danger="state == 'error'"
decoration-success="state == 'success'"/>
<field name="message"/>
</list>
</field>
</record>
<record id="view_sync_log_form" model="ir.ui.view">
<field name="name">odoo.sync.log.form</field>
<field name="model">odoo.sync.log</field>
<field name="arch" type="xml">
<form>
<sheet>
<group>
<group>
<field name="queue_id" options="{'no_create': True}"/>
<field name="state"/>
</group>
<group>
<field name="create_date" readonly="1"/>
</group>
</group>
<notebook>
<page string="Message">
<field name="message" readonly="1"/>
</page>
<page string="Détails techniques" invisible="details == False">
<field name="details" readonly="1"/>
</page>
</notebook>
</sheet>
</form>
</field>
</record>
<record id="action_sync_queue" model="ir.actions.act_window">
<field name="name">File d'attente de synchronisation</field>
<field name="res_model">odoo.sync.queue</field>
<field name="view_mode">list,form</field>
</record>
<record id="action_sync_log" model="ir.actions.act_window">
<field name="name">Journaux de synchronisation</field>
<field name="res_model">odoo.sync.log</field>
<field name="view_mode">list,form</field>
</record>
</odoo>