bemade-addons/odoo_to_odoo_sync/models/sync_instance.py
mathis b792e43873 [IMP] Odoo Sync Modules: Major Enhancements and Fixes
- Added comprehensive README files for all three sync modules
- Implemented advanced logging system with multiple log levels (DEBUG, INFO, WARNING, ERROR)
- Added automatic sensitive data masking for security in log payloads
- Implemented log retention policy (90 days local, 7 years in AWS S3 Glacier)
- Added AWS S3 integration for long-term log archiving
- Fixed validation error with bemade_instance_id in sync model creation
- Refactored model inheritance structure from _inherit to _inherits for proper delegation
- Fixed duplicate sync model prevention logic
- Restored Test Connection button in instance view
- Fixed connection test logging to properly record success/failure
- Fixed queue creation for connection test logs
- Removed unnecessary Synchronized Module tab from odoo_to_odoo_bemade
- Added database indexing on create_date for performance optimization
- Fixed foreign key constraint violations in field mappings creation
- Enhanced error handling throughout the synchronization process
2025-08-15 08:40:17 -04:00

342 lines
12 KiB
Python

# Copyright 2025 Bemade
# 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 urllib.parse import urlparse
from odoo import api, fields, models
from odoo.exceptions import UserError
from ..utils.encryption import encrypt_value, decrypt_value
_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',
copy=False,
)
# Store encrypted password separately
encrypted_password = fields.Char(
string='Encrypted Password',
copy=False,
help='Encrypted password for secure storage',
)
connection_type = fields.Selection(
selection=[
('xmlrpc', 'XML-RPC'),
('jsonrpc', 'JSON-RPC')
],
string='Connection Type',
default='xmlrpc',
required=True,
help='Protocol to use for connection to the remote instance',
)
connection_timeout = fields.Integer(
string='Connection Timeout',
default=30,
help='Timeout in seconds for connection to the remote instance',
)
retry_count = fields.Integer(
string='Max Retries',
default=3,
help='Maximum number of connection retry attempts',
)
conflict_resolution_strategy = fields.Selection([
('manual', 'Manual Resolution'),
('timestamp', 'Newest Wins'),
('source_priority', 'Source Instance Wins'),
('destination_priority', 'Destination Instance Wins')
], string='Conflict Resolution Strategy', default='manual',
help='Strategy for resolving synchronization conflicts with this instance')
retry_delay = fields.Integer(
string='Retry Delay',
default=5,
help='Delay in seconds between retry attempts',
)
auto_reconnect = fields.Boolean(
string='Auto Reconnect',
default=True,
help='Automatically attempt to reconnect when connection is lost',
)
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."""
for record in self:
if record.url != record._origin.url:
record.state = 'draft'
record.error_message = False
@api.onchange('password')
def _onchange_password(self):
"""Mark password for encryption when it changes."""
for record in self:
if record.password != record._origin.password:
# Password changed, will be encrypted on save
record.encrypted_password = False
def test_connection(self):
"""Test the connection to the remote Odoo instance.
This method attempts to authenticate with the remote instance
and updates the connection state accordingly.
Returns:
bool: True if connection successful, False otherwise
"""
for record in self:
record.state = 'testing'
if record.connection_type == 'xmlrpc':
return record._test_xmlrpc_connection()
elif record.connection_type == 'jsonrpc':
# À implémenter si nécessaire
raise UserError("La connexion JSON-RPC n'est pas encore implémentée")
else:
raise UserError(f"Type de connexion non supporté: {record.connection_type}")
def _encrypt_sensitive_data(self):
"""Encrypt sensitive data before saving to database."""
for record in self:
if record.password and not record.encrypted_password:
# Only encrypt if we have a password and it's not already encrypted
record.encrypted_password = encrypt_value(self.env, record.password)
def _decrypt_sensitive_data(self):
"""Decrypt sensitive data for use in connections."""
self.ensure_one()
if self.encrypted_password and not self.password:
# Only decrypt if we have an encrypted password and need the cleartext
return decrypt_value(self.env, self.encrypted_password)
return self.password
@api.model_create_multi
def create(self, vals_list):
"""Override create to encrypt sensitive data."""
records = super().create(vals_list)
records._encrypt_sensitive_data()
return records
def write(self, vals):
"""Override write to encrypt sensitive data."""
result = super().write(vals)
self._encrypt_sensitive_data()
return result
def _test_xmlrpc_connection(self):
"""Test XML-RPC connection to the remote instance."""
self.ensure_one()
try:
# Validate URL format
if not self.url:
raise UserError("L'URL est requise pour tester la connexion")
# Parse URL to extract connection details
parsed_url = urlparse(self.url)
if not parsed_url.scheme or not parsed_url.netloc:
raise UserError("Format d'URL invalide. Exemple valide: https://exemple.odoo.com")
# Get decrypted password for authentication
password = self._decrypt_sensitive_data()
# Attempt to connect and authenticate
common = xmlrpc.client.ServerProxy(f'{self.url}/xmlrpc/2/common')
uid = common.authenticate(self.database, self.username, password, {})
if uid:
self.write({
'state': 'connected',
'last_connection': fields.Datetime.now(),
'error_message': False
})
return True
else:
raise UserError('Échec d\'authentification')
except UserError as e:
# Pass through our UserError without modification
self.write({
'state': 'error',
'error_message': str(e)
})
_logger.error("Erreur d'authentification XML-RPC: %s", str(e))
return False
except (ConnectionError, TimeoutError, xmlrpc.client.Fault, xmlrpc.client.ProtocolError) as e:
# Catch specific exceptions that can be raised during XML-RPC connection
self.write({
'state': 'error',
'error_message': str(e)
})
_logger.error("Erreur de connexion XML-RPC: %s", str(e))
return False
except Exception as e: # pylint: disable=broad-except
# Fall back for any unexpected exceptions
self.write({
'state': 'error',
'error_message': f"Erreur inattendue: {str(e)}"
})
_logger.error("Erreur inattendue XML-RPC: %s", str(e))
return False
def get_connection(self):
"""Return an active connection to the remote instance.
Returns:
tuple: (models_proxy, user_id) for XML-RPC connection
Raises:
UserError: If connection fails
"""
self.ensure_one()
# Ensure we have an active connection
if self.state != 'connected':
self.test_connection()
if self.state != 'connected':
# Use UserError for better user experience
raise UserError(f'Impossible de se connecter à {self.name}: {self.error_message}')
# Return appropriate connection based on connection type
if self.connection_type == 'xmlrpc':
return self._get_xmlrpc_connection()
elif self.connection_type == 'jsonrpc':
# À implémenter si nécessaire
raise UserError("La connexion JSON-RPC n'est pas encore implémentée")
else:
raise UserError(f"Type de connexion non supporté: {self.connection_type}")
def _get_xmlrpc_connection(self):
"""Get XML-RPC connection to the remote instance."""
# Get decrypted password for authentication
password = self._decrypt_sensitive_data()
common = xmlrpc.client.ServerProxy(f'{self.url}/xmlrpc/2/common')
uid = common.authenticate(self.database, self.username, password, {})
models = xmlrpc.client.ServerProxy(f'{self.url}/xmlrpc/2/object')
return models, uid
def execute_kw(self, model, method, args=None, kwargs=None):
"""Execute a method on the remote Odoo instance.
This method provides a unified interface for executing methods on remote models
regardless of the connection type.
Args:
model (str): The model name
method (str): The method to call
args (list, optional): Positional arguments
kwargs (dict, optional): Keyword arguments
Returns:
The result of the method call
Raises:
UserError: If execution fails
"""
self.ensure_one()
if args is None:
args = []
if kwargs is None:
kwargs = {}
try:
models, uid = self.get_connection()
# Get decrypted password for authentication
password = self._decrypt_sensitive_data()
result = models.execute_kw(
self.database, uid, password,
model, method, args, kwargs
)
return result
except UserError:
# Re-raise UserError as it already has appropriate message
raise
except Exception as e: # pylint: disable=broad-except
# Convert other exceptions to UserError for better user experience
raise UserError(f"Erreur d'exécution de {model}.{method}: {str(e)}") from e