bemade-addons/odoo_to_odoo_sync/models/sync_manager.py
2025-05-29 21:35:47 -04:00

204 lines
7.9 KiB
Python

# 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')