bemade-addons/odoo_to_odoo_bemade/models/sync_instance.py

518 lines
21 KiB
Python
Raw Normal View History

# Copyright 2025 Bemade
# License LGPL-3.0 or later (https://www.gnu.org/licenses/lgpl-3.0.html)
"""Bemade Instance Management for Odoo to Odoo Synchronization.
This module extends the base sync instance model with Bemade-specific
functionality for managing connections to Bemade instances, including support
for OdooRPC and specialized connection handling for Bemade clients.
"""
import logging
import time
import random
from urllib.parse import urlparse
# Import XML-RPC for standard connections
import xmlrpc.client
# Import OdooRPC conditionally to avoid hard dependency
try:
import odoorpc
except ImportError:
odoorpc = None
from odoo import api, fields, models
from odoo.exceptions import UserError, ValidationError
_logger = logging.getLogger(__name__)
# pylint: disable=pointless-string-statement
"""Note sur l'héritage et les champs/méthodes des modèles Odoo
Dans Odoo, l'héritage d'un modèle via `_inherit` permet d'hériter de tous les champs
et méthodes du modèle parent. Cependant, les outils d'analyse statique comme Pylint
et Pyright ne sont pas conscients de ce mécanisme d'héritage et signalent des erreurs
lorsque nous utilisons des attributs définis dans le modèle parent.
Fields hérités du modèle parent 'odoo.sync.instance' (qui ne sont pas redéfinis ici) :
- name: fields.Char # Nom de l'instance distante
- url: fields.Char # URL de l'instance distante
- database: fields.Char # Nom de la base de données distante
- username: fields.Char # Nom d'utilisateur pour la connexion
- password: fields.Char # Mot de passe pour la connexion
- connection_type: fields.Selection # Type de connexion (xmlrpc, jsonrpc)
- connection_timeout: fields.Integer # Timeout de connexion en secondes
- retry_count: fields.Integer # Nombre maximal de tentatives de reconnexion
- retry_delay: fields.Integer # Délai entre les tentatives de reconnexion
- auto_reconnect: fields.Boolean # Reconnexion automatique
- state: fields.Selection # État de la connexion
- error_message: fields.Text # Message d'erreur éventuel
- last_connection: fields.Datetime # Date et heure de la dernière connexion réussie
Méthodes héritées (ignorées par les linters mais disponibles) :
- test_connection()
- get_connection()
- execute_kw()
Les erreurs lint pour ces attributs et méthodes hérités peuvent être ignorées.
"""
class OdooToBemadeInstance(models.Model):
"""Bemade Remote Odoo instance for synchronization.
Extends the base sync instance model with Bemade-specific functionality.
This class adds support for OdooRPC connection type and specialized
handling for Bemade clients.
Attributes inherited from parent model (odoo.sync.instance):
name (Char): Name of the remote instance
url (Char): URL of the remote instance
database (Char): Database name of the remote instance
username (Char): Username for authentication
password (Char): Password for authentication
connection_type (Selection): Connection type (xmlrpc, jsonrpc)
connection_timeout (Integer): Timeout in seconds for connection
retry_count (Integer): Max retries for connection attempts
retry_delay (Integer): Delay in seconds between retries
auto_reconnect (Boolean): Whether to reconnect automatically
state (Selection): Connection state
error_message (Text): Error message if any
last_connection (Datetime): Date and time of last successful connection
Note to static analyzers:
The fields and methods mentioned above are inherited from the parent model
and are available at runtime through Odoo's inheritance mechanism.
Lint errors for these attributes can safely be ignored.
"""
# Fields inherited from parent class - for linters only
# pyright: ignore[reportAttributeAccessIssue]
name: str
url: str
database: str
username: str
password: str
connection_type: fields.Selection
connection_timeout: int
retry_count: int
retry_delay: int
auto_reconnect: bool
state: fields.Selection
error_message: str
last_connection: fields.Datetime
_name = 'odoo.to.bemade.instance'
_description = 'Bemade Remote Odoo Instance'
_inherit = 'odoo.sync.instance'
# Champs spécifiques à Bemade
api_key = fields.Char(
string='API Key',
help='Optional API key for authentication with Bemade instance',
)
# Override connection_type to add OdooRPC option
connection_type = fields.Selection(
selection=[
('xmlrpc', 'XML-RPC'),
('jsonrpc', 'JSON-RPC'),
('odoorpc', 'OdooRPC')
],
string='Connection Type',
default='xmlrpc',
required=True,
help='Protocol to use for connection to the remote instance',
)
model_ids = fields.One2many(
'odoo.to.bemade.sync.model',
'bemade_instance_id',
string='Modèles synchronisés',
help='Modèles configurés pour la synchronisation avec cette instance',
)
log_ids = fields.One2many(
'odoo.to.bemade.sync.log',
'bemade_instance_id',
string='Synchronization Logs',
)
connection_timeout = fields.Integer(
string='Connection Timeout',
default=60,
help='Timeout in seconds for the connection',
)
retry_count = fields.Integer(
string='Retry Count',
default=3,
help='Number of times to retry failed connections',
)
retry_delay = fields.Integer(
string='Retry Delay',
default=5,
help='Delay between retries in seconds',
)
auto_reconnect = fields.Boolean(
string='Auto Reconnect',
default=True,
help='Automatically reconnect if connection is lost',
)
# Remarque: model_ids et log_ids sont déjà définis ci-dessus, pas besoin de duplication
@api.onchange
def onchange(self, values, field_names, fields_spec):
"""Handle onchange events.
This method implements the abstract method from BaseModel by delegating
to the parent class implementation.
Args:
values: The values dict
field_names: Names of the fields that triggered the onchange
fields_spec: The onchange specification
Returns:
Dictionary with updated values
"""
return super(OdooToBemadeInstance, self).onchange(values, field_names, fields_spec)
@api.onchange('url')
def _onchange_url(self):
"""Reset state when URL changes.
Extends the parent method to add Bemade-specific behavior if needed.
Note: Although linters may report url, state, and error_message as unknown attributes,
they are inherited from the parent model and are available at runtime.
"""
# pylint: disable=attribute-defined-outside-init
# Fields inherited from parent model: url, state, error_message
if self.url != self._origin.url:
self.write({'state': 'draft', 'error_message': False})
def test_connection(self):
"""Test the connection to the remote Odoo instance.
If the connection type is 'odoorpc', use the specialized method,
otherwise fall back to the parent implementation.
Note: connection_type is inherited from the parent model, but linters won't detect this.
"""
self.ensure_one()
# Field inherited from parent model: connection_type
# pylint: disable=no-member
if self.connection_type == 'odoorpc':
return self._test_odoorpc_connection()
# Call the parent method - linters might not recognize this as valid
# pylint: disable=no-member
return super(OdooToBemadeInstance, self).test_connection()
def _test_odoorpc_connection(self):
"""Test connection using OdooRPC library.
This method is specific to the Bemade implementation and handles
the OdooRPC connection type that's not in the parent model.
"""
# Set the state to indicate testing is in progress
self.write({'state': 'testing'})
try:
# Check if OdooRPC library is installed
try:
import odoorpc
except ImportError as exc:
raise ImportError(
"La bibliothèque OdooRPC n'est pas installée. "
"Installez-la avec 'pip install odoorpc'"
) from exc
# Parse URL to extract connection details
parsed_url = urlparse(self.url)
protocol = parsed_url.scheme
host = parsed_url.netloc
# Extract port if present
if ':' in host:
host, port = host.split(':')
port = int(port)
else:
port = 443 if protocol == 'https' else 80
# Attempt connection with OdooRPC
# Note: timeout is passed as a keyword argument during login phase
odoo = odoorpc.ODOO(
host,
protocol=protocol,
port=port
)
# Try to login with credentials
odoo.login(self.database, self.username, self.password)
# If successful, update record state
if odoo.env:
self.write({
'state': 'connected',
'last_connection': fields.Datetime.now(),
'error_message': False
})
return True
else:
# This should rarely happen as login usually raises an exception
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 OdooRPC: %s", str(e))
return False
except (ConnectionError, TimeoutError, ValueError, TypeError) as e:
# Catch specific exceptions that can be raised during connection
self.write({
'state': 'error',
'error_message': str(e)
})
_logger.error("Erreur de connexion OdooRPC: %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 OdooRPC: %s", str(e))
return False
def get_connection(self):
"""Return an active connection to the remote instance.
This method extends the parent implementation to handle the
OdooRPC connection type in addition to the parent's connection types.
"""
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}')
# Handle OdooRPC connection type
if self.connection_type == 'odoorpc':
return self._get_odoorpc_connection()
# Use parent implementation for other connection types
return super(OdooToBemadeInstance, self).get_connection()
def _get_odoorpc_connection(self):
"""Get a connection to the remote Odoo instance using OdooRPC.
Returns a connection object that can be used to interact with the remote instance.
Returns:
OdooRPCWrapper: A wrapped OdooRPC connection object that provides compatible interface
Raises:
ImportError: When OdooRPC library is not installed
UserError: When connection fails due to authentication issues
Exception: For other connection failures
"""
try:
# Check if OdooRPC library is installed
try:
import odoorpc
except ImportError as exc:
raise ImportError(
"La bibliothèque OdooRPC n'est pas installée. "
"Installez-la avec 'pip install odoorpc'"
) from exc
# Parse URL to extract connection details
parsed_url = urlparse(self.url)
protocol = parsed_url.scheme
host = parsed_url.netloc
# Extract port if present
if ':' in host:
host, port = host.split(':')
port = int(port)
else:
port = 443 if protocol == 'https' else 80
# Create OdooRPC connection
# Note: OdooRPC doesn't accept timeout in constructor, set it after
odoo = odoorpc.ODOO(
host,
protocol=protocol,
port=port
)
# Set timeout via attribute if available
if hasattr(odoo, 'config'):
odoo.config['timeout'] = self.connection_timeout
# Login with credentials
odoo.login(self.database, self.username, self.password)
# Create a wrapper class to make OdooRPC interface compatible with xmlrpc/jsonrpc
class OdooRPCWrapper:
"""Wrapper class to provide a compatible interface with XML-RPC."""
def __init__(self, odoo_instance):
self.odoo = odoo_instance
def execute_kw(self, db, uid, password, model, method, args, kwargs=None):
"""Mimics XML-RPC's execute_kw but uses OdooRPC internally.
Args:
db: Database name (unused for OdooRPC, required for compatibility)
uid: User ID (unused for OdooRPC, required for compatibility)
password: Password (unused for OdooRPC, required for compatibility)
model: Model name to call
method: Method name to call on the model
args: Positional arguments for the method
kwargs: Keyword arguments for the method
Returns:
Result of the method call
Raises:
Exception: When the method call fails
"""
# These parameters are required for compatibility but not used
# with OdooRPC since we're already logged in
# pylint: disable=unused-argument
# Handle args and kwargs correctly
args = args or []
kwargs = kwargs or {}
try:
# OdooRPC has a different API for standard vs custom methods
if model in self.odoo.env and hasattr(self.odoo.env[model], method):
# Use the standard API for methods like 'search', 'read', etc.
model_obj = self.odoo.env[model]
method_obj = getattr(model_obj, method)
return method_obj(*args, **kwargs)
else:
# Use execute for custom methods
return self.odoo.execute(model, method, *args, **kwargs)
except Exception as e:
_logger.error(
"Erreur lors de l'exécution de %s.%s via OdooRPC: %s",
model, method, str(e)
)
raise
# pylint: enable=unused-argument
# Update connection state on success
self.write({
'state': 'connected',
'last_connection': fields.Datetime.now(),
'error_message': False
})
# Return wrapper and uid, similar to how XML-RPC connections work
wrapper = OdooRPCWrapper(odoo)
return wrapper, odoo.env.uid
except (ConnectionError, TimeoutError) as e:
# Specific error handling for connection and timeout issues
error_msg = f"Délai d'attente dépassé lors de la connexion à {self.name}: {e}"
self.write({
'state': 'error',
'error_message': error_msg
})
_logger.error("Erreur de connexion/timeout dans _get_odoorpc_connection: %s", str(e))
raise UserError(error_msg) from e
except (ValueError, TypeError) as e:
# Handle value or type errors
error_msg = f"Paramètres invalides pour la connexion à {self.name}: {e}"
self.write({
'state': 'error',
'error_message': error_msg
})
_logger.error("Erreur de paramètres dans _get_odoorpc_connection: %s", str(e))
raise UserError(error_msg) from e
except Exception as e: # pylint: disable=broad-except
# Fall back for any unexpected exceptions
error_msg = f"Échec lors de la connexion à {self.name}: {e}"
self.write({
'state': 'error',
'error_message': error_msg
})
_logger.error("Erreur inattendue dans _get_odoorpc_connection: %s", str(e))
raise UserError(error_msg) from e
def execute_kw(self, model, method, args=None, kwargs=None):
"""Execute a method on the remote model using the appropriate connection type.
This method provides a unified interface for executing methods on remote models,
abstracting away the differences between XML-RPC and OdooRPC.
Args:
model (str): Name of the model on the remote instance
method (str): Name of the method to execute
args (list, optional): Positional arguments to pass to the method
kwargs (dict, optional): Keyword arguments to pass to the method
Returns:
object: Result of the method execution
Raises:
UserError: When connection is not established or fails
ValidationError: When method execution fails on the remote instance
"""
self.ensure_one()
args = args or []
kwargs = kwargs or {}
# Ensure we have a connection
if self.state != 'connected':
_logger.debug("Connection to %s not established. Attempting to connect.", self.name)
self.test_connection()
if self.state != 'connected':
raise UserError(f"Cannot execute method on {self.name}: {self.error_message}")
try:
# Get connection based on connection type
connection = self.get_connection()
# For OdooRPC connection, use the wrapper's execute_kw method
if self.connection_type == 'odoorpc':
_logger.debug("Executing %s.%s via OdooRPC on %s", model, method, self.name)
# OdooRPC wrapper returns a tuple (wrapper, uid)
wrapper, uid = connection
# Use the wrapper's execute_kw method
return wrapper.execute_kw(
self.database, uid, self.password,
model, method, args, kwargs
)
else:
# For XML-RPC, delegate to parent implementation
_logger.debug("Executing %s.%s via standard RPC on %s", model, method, self.name)
# XML-RPC connection returns a tuple (common, models)
common, models = connection
# Execute the method directly on the model
return models.execute_kw(
self.database, self.env.uid, self.password,
model, method, args, kwargs
)
except Exception as e:
error_msg = f"Error executing {model}.{method} on {self.name}: {str(e)}"
_logger.error(error_msg)
self.write({
'state': 'error',
'error_message': error_msg
})
raise ValidationError(error_msg) from e