204 lines
7.9 KiB
Python
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')
|