odoo to odoo
This commit is contained in:
parent
1b95a6c2ea
commit
cc66512df2
18 changed files with 1823 additions and 0 deletions
676
odoo_to_odoo_sync/Spécifications.md
Normal file
676
odoo_to_odoo_sync/Spécifications.md
Normal 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
|
||||||
1
odoo_to_odoo_sync/__init__.py
Normal file
1
odoo_to_odoo_sync/__init__.py
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
from . import models
|
||||||
28
odoo_to_odoo_sync/__manifest__.py
Normal file
28
odoo_to_odoo_sync/__manifest__.py
Normal 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',
|
||||||
|
}
|
||||||
15
odoo_to_odoo_sync/data/ir_cron_data.xml
Normal file
15
odoo_to_odoo_sync/data/ir_cron_data.xml
Normal 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>
|
||||||
6
odoo_to_odoo_sync/models/__init__.py
Normal file
6
odoo_to_odoo_sync/models/__init__.py
Normal 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
|
||||||
127
odoo_to_odoo_sync/models/sync_instance.py
Normal file
127
odoo_to_odoo_sync/models/sync_instance.py
Normal 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
|
||||||
50
odoo_to_odoo_sync/models/sync_log.py
Normal file
50
odoo_to_odoo_sync/models/sync_log.py
Normal 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}'
|
||||||
204
odoo_to_odoo_sync/models/sync_manager.py
Normal file
204
odoo_to_odoo_sync/models/sync_manager.py
Normal 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')
|
||||||
94
odoo_to_odoo_sync/models/sync_model.py
Normal file
94
odoo_to_odoo_sync/models/sync_model.py
Normal 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]
|
||||||
41
odoo_to_odoo_sync/models/sync_model_field.py
Normal file
41
odoo_to_odoo_sync/models/sync_model_field.py
Normal 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!')
|
||||||
|
]
|
||||||
204
odoo_to_odoo_sync/models/sync_queue.py
Normal file
204
odoo_to_odoo_sync/models/sync_queue.py
Normal 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
|
||||||
13
odoo_to_odoo_sync/security/ir.model.access.csv
Normal file
13
odoo_to_odoo_sync/security/ir.model.access.csv
Normal 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
odoo_to_odoo_sync/static/description/icon.png
Normal file
1
odoo_to_odoo_sync/static/description/icon.png
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
|
||||||
45
odoo_to_odoo_sync/views/menus.xml
Normal file
45
odoo_to_odoo_sync/views/menus.xml
Normal 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>
|
||||||
68
odoo_to_odoo_sync/views/sync_instance_views.xml
Normal file
68
odoo_to_odoo_sync/views/sync_instance_views.xml
Normal 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>
|
||||||
51
odoo_to_odoo_sync/views/sync_log_views.xml
Normal file
51
odoo_to_odoo_sync/views/sync_log_views.xml
Normal 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>
|
||||||
69
odoo_to_odoo_sync/views/sync_model_views.xml
Normal file
69
odoo_to_odoo_sync/views/sync_model_views.xml
Normal 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>
|
||||||
130
odoo_to_odoo_sync/views/sync_queue_views.xml
Normal file
130
odoo_to_odoo_sync/views/sync_queue_views.xml
Normal 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>
|
||||||
Loading…
Reference in a new issue