diff --git a/odoo_to_odoo_bemade/__init__.py b/odoo_to_odoo_bemade/__init__.py index 48ec765..dd7f5c9 100644 --- a/odoo_to_odoo_bemade/__init__.py +++ b/odoo_to_odoo_bemade/__init__.py @@ -1,8 +1,14 @@ +import os +import logging +import socket from . import models from . import controllers +from . import wizards from odoo import api, SUPERUSER_ID +_logger = logging.getLogger(__name__) + def post_init_hook(env): """Post-install hook to set up Bemade-specific configuration.""" # Set default configuration parameters for Bemade instances @@ -22,3 +28,90 @@ def post_init_hook(env): for key, value in defaults.items(): if not ICP.get_param(key): ICP.set_param(key, value) + + # Optionally inject API key and other overrides from environment variables (never hardcode secrets) + # Accepted env vars: BEMADE_SYNC_DEFAULT_URL, _DATABASE, _USERNAME, _API_KEY, _CONNECTION_TYPE, _NAME + env_url = os.getenv('BEMADE_SYNC_DEFAULT_URL') + env_db = os.getenv('BEMADE_SYNC_DEFAULT_DATABASE') + env_user = os.getenv('BEMADE_SYNC_DEFAULT_USERNAME') + env_api_key = os.getenv('BEMADE_SYNC_DEFAULT_API_KEY') + env_conn = os.getenv('BEMADE_SYNC_DEFAULT_CONNECTION_TYPE') + env_name = os.getenv('BEMADE_SYNC_DEFAULT_NAME') + + # If provided via env, also persist as config parameters (except name) + if env_url: + ICP.set_param('bemade.sync.default_url', env_url) + if env_db: + ICP.set_param('bemade.sync.default_database', env_db) + if env_user: + ICP.set_param('bemade.sync.default_username', env_user) + if env_conn: + ICP.set_param('bemade.sync.default_connection_type', env_conn) + if env_api_key: + # Keep API key in parameters only if explicitly injected via env + ICP.set_param('bemade.sync.default_api_key', env_api_key) + + # Resolve final defaults (env overrides > ICP defaults) + url = env_url or ICP.get_param('bemade.sync.default_url') + database = env_db or ICP.get_param('bemade.sync.default_database') + username = env_user or ICP.get_param('bemade.sync.default_username') + api_key = env_api_key or ICP.get_param('bemade.sync.default_api_key') + connection_type = str(env_conn or ICP.get_param('bemade.sync.default_connection_type') or 'jsonrpc').strip() + timeout = int(ICP.get_param('bemade.sync.default_timeout') or 30) + retry_count = int(ICP.get_param('bemade.sync.default_retry_count') or 3) + retry_delay = int(ICP.get_param('bemade.sync.default_retry_delay') or 5) + + # Create or update a default Bemade instance for immediate use in the Assign wizard + try: + Instance = env['odoo.to.bemade.instance'].sudo() + + # Only proceed when we have the minimum secure credentials + if url and database and username and api_key: + domain = [('url', '=', url), ('database', '=', database), ('username', '=', username)] + instance = Instance.search(domain, limit=1) + + vals = { + 'name': (env_name or f"Bemade {database}").strip(), + 'url': url, + 'database': database, + 'username': username, + 'api_key': api_key, + 'connection_type': connection_type, + 'connection_timeout': timeout, + 'retry_count': retry_count, + 'retry_delay': retry_delay, + 'active': True, + } + + if instance: + instance.write(vals) + _logger.info("[Bemade Sync] Updated default instance '%s' (%s/%s)", instance.name, url, database) + else: + instance = Instance.create(vals) + _logger.info("[Bemade Sync] Created default instance '%s' (%s/%s)", instance.name, url, database) + + # Best-effort connection test; do not fail installation + try: + # Apply a temporary global socket timeout so lower-level libs (XML-RPC / OdooRPC) + # cannot block the installation indefinitely. + prev_timeout = socket.getdefaulttimeout() + socket.setdefaulttimeout(timeout or 30) + _logger.info("[Bemade Sync] Testing connection (type=%s, timeout=%ss)...", connection_type, timeout) + instance.test_connection() + except Exception as exc: # noqa: BLE001 - broad by design for robustness during install + _logger.warning("[Bemade Sync] Connection test failed during post-init: %s", exc) + finally: + # Always restore previous socket timeout + try: + socket.setdefaulttimeout(prev_timeout) + except Exception: + pass + else: + _logger.warning( + "[Bemade Sync] Skipping default instance creation. Missing one of url/database/username/api_key. " + "url=%s database=%s username=%s api_key=%s", + bool(url), bool(database), bool(username), bool(api_key) + ) + except Exception as exc: # noqa: BLE001 + # Never break installation; just log the error + _logger.error("[Bemade Sync] Error while creating/updating default instance: %s", exc) diff --git a/odoo_to_odoo_bemade/__manifest__.py b/odoo_to_odoo_bemade/__manifest__.py index 0634c1c..867bc4f 100644 --- a/odoo_to_odoo_bemade/__manifest__.py +++ b/odoo_to_odoo_bemade/__manifest__.py @@ -18,6 +18,10 @@ 'data': [ 'data/ir_config_parameter_data.xml', 'views/menus.xml', + 'views/odoo_to_bemade_instance_views.xml', + # Load project view extensions and wizard UI + 'views/project_views.xml', + 'wizards/assign_project_wizard_view.xml', ], 'installable': True, 'application': False, diff --git a/odoo_to_odoo_bemade/data/ir_config_parameter_data.xml b/odoo_to_odoo_bemade/data/ir_config_parameter_data.xml index ddee5ae..580d235 100644 --- a/odoo_to_odoo_bemade/data/ir_config_parameter_data.xml +++ b/odoo_to_odoo_bemade/data/ir_config_parameter_data.xml @@ -1,40 +1,40 @@ - - - bemade.sync.default_url - https://odoo.bemade.org - + + + bemade.sync.default_url + https://odoo.bemade.org + - - bemade.sync.default_database - bemade - + + bemade.sync.default_database + bemade + - - bemade.sync.default_username - sync_user - + + bemade.sync.default_username + sync_user + - - bemade.sync.default_connection_type - odoorpc - + + bemade.sync.default_connection_type + odoorpc + - - bemade.sync.default_timeout - 30 - + + bemade.sync.default_timeout + 30 + - - bemade.sync.default_retry_count - 3 - + + bemade.sync.default_retry_count + 3 + - - bemade.sync.default_retry_delay - 5 - + + bemade.sync.default_retry_delay + 5 + diff --git a/odoo_to_odoo_bemade/models/__init__.py b/odoo_to_odoo_bemade/models/__init__.py index 31ada4b..a6a12f6 100644 --- a/odoo_to_odoo_bemade/models/__init__.py +++ b/odoo_to_odoo_bemade/models/__init__.py @@ -5,5 +5,4 @@ from . import sync_queue from . import sync_log from . import sync_manager from . import project -from . import api_key -from . import api_key_wizard +from . import odoo_to_bemade_instance diff --git a/odoo_to_odoo_bemade/models/api_key.py b/odoo_to_odoo_bemade/models/api_key.py deleted file mode 100644 index 00d1883..0000000 --- a/odoo_to_odoo_bemade/models/api_key.py +++ /dev/null @@ -1,211 +0,0 @@ -# -*- coding: utf-8 -*- -# Copyright 2025 Bemade -# License LGPL-3.0 or later (https://www.gnu.org/licenses/lgpl-3.0.html) - -"""API Key Management for Bemade Odoo Sync. - -This module provides functionality for generating, tracking, and revoking -API keys used for authentication between Odoo instances. -""" - -import logging -import secrets -import string -from datetime import datetime - -from odoo import api, fields, models, _ -from odoo.exceptions import UserError - -_logger = logging.getLogger(__name__) - -class OdooToBemadeApiKey(models.Model): - """API Key for Bemade Odoo Sync. - - This model stores API keys that can be used for authentication - between Odoo instances. - """ - - _name = 'odoo.to.bemade.api.key' - _description = 'Bemade API Key' - _order = 'create_date desc' - - name = fields.Char( - string='Name', - required=True, - help='A descriptive name for this API key', - ) - - key = fields.Char( - string='API Key', - readonly=True, - copy=False, - help='The API key value (only shown once upon creation)', - ) - - key_hash = fields.Char( - string='API Key Hash', - readonly=True, - copy=False, - help='Hashed value of the API key for verification', - ) - - user_id = fields.Many2one( - 'res.users', - string='Created By', - default=lambda self: self.env.user, - readonly=True, - required=True, - help='User who created this API key', - ) - - create_date = fields.Datetime( - string='Created On', - readonly=True, - ) - - last_used = fields.Datetime( - string='Last Used', - readonly=True, - help='Last time this API key was used for authentication', - ) - - expiration_date = fields.Date( - string='Expiration Date', - help='Date when this API key expires (leave empty for no expiration)', - ) - - is_active = fields.Boolean( - string='Active', - default=True, - help='Whether this API key is active and can be used for authentication', - ) - - instance_ids = fields.One2many( - 'odoo.to.bemade.instance', - 'api_key_id', - string='Used In Instances', - help='Instances using this API key', - ) - - usage_count = fields.Integer( - string='Usage Count', - default=0, - readonly=True, - help='Number of times this API key has been used', - ) - - notes = fields.Text( - string='Notes', - help='Additional notes about this API key', - ) - - @api.model - def generate_key(self): - """Generate a new API key. - - Returns: - str: The generated API key - """ - # Generate a random 32-character string - alphabet = string.ascii_letters + string.digits - key = ''.join(secrets.choice(alphabet) for _ in range(32)) - return key - - def action_generate_key(self): - """Generate a new API key and store its hash. - - Returns: - dict: Action to open a wizard showing the generated key - """ - self.ensure_one() - - # Generate a new API key - key = self.generate_key() - - # Store the key hash (in a real implementation, use a proper hashing algorithm) - # For this example, we're just storing the key directly, but in production - # you should use a secure hashing algorithm like bcrypt - self.write({ - 'key': key, - 'key_hash': key, # In production, store a hash instead - 'is_active': True, - }) - - # Return an action to open a wizard showing the key - return { - 'name': _('API Key Generated'), - 'type': 'ir.actions.act_window', - 'res_model': 'odoo.to.bemade.api.key.display', - 'view_mode': 'form', - 'target': 'new', - 'context': { - 'default_api_key_id': self.id, - 'default_api_key': key, - }, - } - - def action_revoke(self): - """Revoke this API key. - - Returns: - bool: True - """ - self.ensure_one() - self.write({ - 'is_active': False, - }) - return True - - def action_activate(self): - """Activate this API key. - - Returns: - bool: True - """ - self.ensure_one() - self.write({ - 'is_active': True, - }) - return True - - def record_usage(self): - """Record that this API key has been used. - - Returns: - bool: True - """ - self.ensure_one() - self.write({ - 'last_used': fields.Datetime.now(), - 'usage_count': self.usage_count + 1, - }) - return True - - @api.model - def validate_key(self, key): - """Validate an API key. - - Args: - key (str): The API key to validate - - Returns: - bool: True if the key is valid, False otherwise - """ - # In a real implementation, hash the key and compare with stored hashes - # For this example, we're just comparing the key directly - api_key = self.search([ - ('key_hash', '=', key), - ('is_active', '=', True), - ], limit=1) - - if not api_key: - return False - - # Check if the key has expired - if api_key.expiration_date and api_key.expiration_date < fields.Date.today(): - return False - - # Record usage - api_key.record_usage() - - return True diff --git a/odoo_to_odoo_bemade/models/api_key_wizard.py b/odoo_to_odoo_bemade/models/api_key_wizard.py deleted file mode 100644 index 4ecc2e4..0000000 --- a/odoo_to_odoo_bemade/models/api_key_wizard.py +++ /dev/null @@ -1,43 +0,0 @@ -# -*- coding: utf-8 -*- -# Copyright 2025 Bemade -# License LGPL-3.0 or later (https://www.gnu.org/licenses/lgpl-3.0.html) - -"""API Key Display Wizard. - -This module provides a wizard for displaying a newly generated API key -to the user. The key is only shown once for security reasons. -""" - -from odoo import api, fields, models, _ -from odoo.exceptions import UserError - -class OdooToBemadeApiKeyDisplay(models.TransientModel): - """Wizard to display a newly generated API key.""" - - _name = 'odoo.to.bemade.api.key.display' - _description = 'API Key Display Wizard' - - api_key_id = fields.Many2one( - 'odoo.to.bemade.api.key', - string='API Key', - required=True, - readonly=True, - ) - - api_key = fields.Char( - string='API Key Value', - readonly=True, - help='This API key will only be shown once. Please copy it now.', - ) - - warning_message = fields.Html( - string='Warning', - readonly=True, - default=""" - - """, - ) diff --git a/odoo_to_odoo_bemade/models/odoo_to_bemade_instance.py b/odoo_to_odoo_bemade/models/odoo_to_bemade_instance.py new file mode 100644 index 0000000..151e468 --- /dev/null +++ b/odoo_to_odoo_bemade/models/odoo_to_bemade_instance.py @@ -0,0 +1,67 @@ +# Copyright 2025 Bemade +# License LGPL-3.0 or later (https://www.gnu.org/licenses/lgpl-3.0.html) + +import logging +from odoo import api, fields, models, _ +from odoo.exceptions import UserError + +_logger = logging.getLogger(__name__) + +class OdooToBemadeInstance(models.Model): + _name = 'odoo.to.bemade.instance' + _description = 'Bemade Remote Odoo Instance' + _inherit = ['odoo.to.bemade.instance', 'mail.thread', 'mail.activity.mixin'] + + # Bemade-specific fields (do not duplicate base sync fields) + project_id = fields.Many2one( + 'project.project', + string='Project', + domain=[('is_client_project', '=', True)] + ) + + client_key = fields.Char(string='Client Key', readonly=True) + bemade_api_token = fields.Char(string='Bemade API Token', readonly=True) + + is_client_instance = fields.Boolean(string='Is Client Instance', default=False) + + # Additional informational field not present in base model + last_sync_date = fields.Datetime(string='Last Sync Date') + + def action_test_connection(self): + """Delegate to the inherited test_connection and notify the user.""" + self.ensure_one() + ok = False + message = _('Connection test completed.') + try: + ok = bool(self.test_connection()) + message = _('Connection successful') if ok else _('Connection failed') + except Exception as e: # noqa: BLE001 - notify without breaking UI + message = _('Connection failed: %s') % str(e) + + return { + 'type': 'ir.actions.client', + 'tag': 'display_notification', + 'params': { + 'title': _('Connection Test'), + 'message': message, + 'type': 'success' if ok else 'danger', + } + } + + def action_sync_project_data(self): + """Sync project data with the client instance.""" + self.ensure_one() + if self.state != 'connected': + raise UserError(_('Please test the connection first.')) + + return { + 'type': 'ir.actions.act_window', + 'name': _('Sync Project Data'), + 'res_model': 'odoo.sync.queue', + 'view_mode': 'form', + 'target': 'new', + 'context': { + 'default_instance_id': self.id, + 'default_project_id': self.project_id.id, + } + } diff --git a/odoo_to_odoo_bemade/models/sync_instance.py b/odoo_to_odoo_bemade/models/sync_instance.py index c3023dd..c916930 100644 --- a/odoo_to_odoo_bemade/models/sync_instance.py +++ b/odoo_to_odoo_bemade/models/sync_instance.py @@ -11,6 +11,7 @@ for OdooRPC and specialized connection handling for Bemade clients. import logging import time import random +import socket from urllib.parse import urlparse # Import XML-RPC for standard connections @@ -106,18 +107,8 @@ class OdooToBemadeInstance(models.Model): _inherit = 'odoo.sync.instance' # Champs spécifiques à Bemade - api_key_id = fields.Many2one( - 'odoo.to.bemade.api.key', - string='API Key', - help='API key for authentication with Bemade instance', - domain=[('is_active', '=', True)], - ) - - use_api_key = fields.Boolean( - string='Use API Key', - default=False, - help='Use API key instead of password for authentication', - ) + # Note: Legacy API key functionality has been removed + # Use the built-in api_key field from parent model instead # Override connection_type to add OdooRPC option connection_type = fields.Selection( @@ -244,19 +235,29 @@ class OdooToBemadeInstance(models.Model): if not parsed_url.scheme or not parsed_url.netloc: raise UserError("Format d'URL invalide. Exemple valide: https://exemple.odoo.com") - # Get authentication credentials - if self.use_api_key and self.api_key_id: - # Use API key for authentication - auth_key = self.api_key_id.key_hash # In production, this would be the hashed key - # Record usage of the API key - self.api_key_id.record_usage() - else: - # Use password for authentication - auth_key = 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, auth_key, {}) + # Get API token for authentication (Odoo 18+ expects dict with scope/key during authenticate) + api_token = self._decrypt_sensitive_data() or self.api_key + + # Timeout-aware XML-RPC transport + class TimeoutTransport(xmlrpc.client.Transport): + def __init__(self, timeout=None, use_datetime=False): + super().__init__(use_datetime=use_datetime) + self.timeout = timeout + def make_connection(self, host): + conn = super().make_connection(host) + try: + conn.timeout = self.timeout + except Exception: + pass + return conn + + transport = TimeoutTransport(timeout=self.connection_timeout or 30) + common = xmlrpc.client.ServerProxy( + f'{self.url}/xmlrpc/2/common', transport=transport, allow_none=True + ) + uid = common.authenticate( + self.database, self.username, {'scope': 'rpc', 'key': api_token}, {} + ) if uid: self.write({ @@ -297,20 +298,32 @@ class OdooToBemadeInstance(models.Model): def _get_xmlrpc_connection(self): """Override parent's XML-RPC connection method to support API key authentication.""" - # Get authentication credentials - if self.use_api_key and self.api_key_id: - # Use API key for authentication - auth_key = self.api_key_id.key_hash # In production, this would be the hashed key - # Record usage of the API key - self.api_key_id.record_usage() - else: - # Use password for authentication - auth_key = self._decrypt_sensitive_data() - - common = xmlrpc.client.ServerProxy(f'{self.url}/xmlrpc/2/common') - uid = common.authenticate(self.database, self.username, auth_key, {}) - models = xmlrpc.client.ServerProxy(f'{self.url}/xmlrpc/2/object') - + # Get API token for authentication + api_token = self._decrypt_sensitive_data() or self.api_key + + # Timeout-aware transport + class TimeoutTransport(xmlrpc.client.Transport): + def __init__(self, timeout=None, use_datetime=False): + super().__init__(use_datetime=use_datetime) + self.timeout = timeout + def make_connection(self, host): + conn = super().make_connection(host) + try: + conn.timeout = self.timeout + except Exception: + pass + return conn + + transport = TimeoutTransport(timeout=self.connection_timeout or 30) + common = xmlrpc.client.ServerProxy( + f'{self.url}/xmlrpc/2/common', transport=transport, allow_none=True + ) + uid = common.authenticate( + self.database, self.username, {'scope': 'rpc', 'key': api_token}, {} + ) + models = xmlrpc.client.ServerProxy( + f'{self.url}/xmlrpc/2/object', transport=transport, allow_none=True + ) return models, uid def _test_odoorpc_connection(self): @@ -332,36 +345,65 @@ class OdooToBemadeInstance(models.Model): "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 + # Parse URL to extract connection details (ensure scheme present) + raw_url = (self.url or '').strip() + parsed_url = urlparse(raw_url) + if not parsed_url.scheme: + raw_url = f'https://{raw_url}' + parsed_url = urlparse(raw_url) + protocol = 'jsonrpc+ssl' if parsed_url.scheme == 'https' else 'jsonrpc' + host = parsed_url.hostname + if not host: + raise UserError("URL invalide: hôte introuvable. Incluez le schéma (https://) et le domaine.") + # Determine port using parsed value or defaults + port = parsed_url.port or (443 if parsed_url.scheme == 'https' else 80) + + # Preflight TCP connectivity to avoid hangs (DNS/TLS/connect) + _logger.info( + "[Bemade Sync][OdooRPC] Preflight TCP connect to %s:%s (timeout=%ss)", + host, port, self.connection_timeout or 30, + ) + try: + start = time.time() + with socket.create_connection((host, port), timeout=self.connection_timeout or 30): + pass + _logger.info( + "[Bemade Sync][OdooRPC] Preflight OK in %.3fs", + time.time() - start, + ) + except Exception as e: + raise UserError(f"Impossible de joindre {host}:{port} - {e}") from e + # Attempt connection with OdooRPC - # Note: timeout is passed as a keyword argument during login phase odoo = odoorpc.ODOO( - host, - protocol=protocol, + host, + protocol=protocol, port=port ) + # Configure library timeout to prevent hanging connections + if hasattr(odoo, 'config'): + odoo.config['timeout'] = self.connection_timeout or 30 + _logger.info( + "[Bemade Sync][OdooRPC] Connecting to %s:%s protocol=%s timeout=%ss", + host, port, protocol, self.connection_timeout or 30, + ) # Try to login with credentials - if self.use_api_key and self.api_key_id: - # Use API key for authentication - api_key = self.api_key_id.key_hash # In production, this would be the hashed key + api_key = self._decrypt_sensitive_data() or self.api_key + prev_timeout = socket.getdefaulttimeout() + socket.setdefaulttimeout(self.connection_timeout or 30) + try: + _logger.info( + "[Bemade Sync][OdooRPC] Logging in db=%s user=%s", + self.database, self.username, + ) odoo.login(self.database, self.username, api_key) - # Record usage of the API key - self.api_key_id.record_usage() - else: - # Use password for authentication - odoo.login(self.database, self.username, self.password) + finally: + try: + socket.setdefaulttimeout(prev_timeout) + except Exception: + pass # If successful, update record state if odoo.env: @@ -389,7 +431,7 @@ class OdooToBemadeInstance(models.Model): _logger.error("Erreur d'authentification OdooRPC: %s", error_msg) return False - except (ConnectionError, TimeoutError, ValueError, TypeError) as e: + except (ConnectionError, TimeoutError, socket.timeout, ValueError, TypeError) as e: # Catch specific exceptions that can be raised during connection error_msg = str(e) self.write({ @@ -465,31 +507,47 @@ class OdooToBemadeInstance(models.Model): "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 + # Parse URL to extract connection details (ensure scheme present) + raw_url = (self.url or '').strip() + parsed_url = urlparse(raw_url) + if not parsed_url.scheme: + raw_url = f'https://{raw_url}' + parsed_url = urlparse(raw_url) + is_https = parsed_url.scheme == 'https' + protocol = 'jsonrpc+ssl' if is_https else 'jsonrpc' + host = parsed_url.hostname + if not host: + raise UserError("URL invalide: hôte introuvable. Incluez le schéma (https://) et le domaine.") + port = parsed_url.port or (443 if is_https else 80) + + # Preflight TCP connectivity to avoid hangs (DNS/TLS/connect) + _logger.info( + "[Bemade Sync][OdooRPC] get_connection preflight TCP to %s:%s (timeout=%ss)", + host, port, self.connection_timeout or 30, + ) + try: + with socket.create_connection((host, port), timeout=self.connection_timeout or 30): + pass + except Exception as e: + raise UserError(f"Impossible de joindre {host}:{port} - {e}") from e # Create OdooRPC connection - # Note: OdooRPC doesn't accept timeout in constructor, set it after odoo = odoorpc.ODOO( - host, - protocol=protocol, + host, + protocol=protocol, port=port ) # Set timeout via attribute if available if hasattr(odoo, 'config'): - odoo.config['timeout'] = self.connection_timeout + odoo.config['timeout'] = self.connection_timeout or 30 + _logger.info( + "[Bemade Sync][OdooRPC] get_connection -> %s:%s protocol=%s timeout=%ss", + host, port, protocol, self.connection_timeout or 30, + ) - # Login with credentials - odoo.login(self.database, self.username, self.password) + # Login with API token credentials + api_token = self._decrypt_sensitive_data() or self.api_key + odoo.login(self.database, self.username, api_token) # Create a wrapper class to make OdooRPC interface compatible with xmlrpc/jsonrpc class OdooRPCWrapper: @@ -627,11 +685,12 @@ class OdooToBemadeInstance(models.Model): 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 + # XML-RPC connection returns a tuple (models, uid) + models, uid = connection + api_token = self._decrypt_sensitive_data() or self.api_key + # Execute the method directly on the model with API token return models.execute_kw( - self.database, self.env.uid, self.password, + self.database, uid, api_token, model, method, args, kwargs ) diff --git a/odoo_to_odoo_bemade/models/sync_model_field.py b/odoo_to_odoo_bemade/models/sync_model_field.py index 393af4d..701d7d6 100644 --- a/odoo_to_odoo_bemade/models/sync_model_field.py +++ b/odoo_to_odoo_bemade/models/sync_model_field.py @@ -26,6 +26,14 @@ class OdooToBemadeSyncModelField(models.Model): _inherit = 'odoo.sync.model.field' # Add Bemade-specific fields here + transform_type = fields.Selection([ + ('direct', 'Direct'), + ('function', 'Function'), + ('computed', 'Computed'), + ('relation', 'Relation') + ], string='Transform Type', default='direct', + help='Type of transformation to apply to the field value') + bemade_transformation = fields.Selection([ ('none', 'No Transformation'), ('prefix', 'Add Prefix'), diff --git a/odoo_to_odoo_bemade/security/ir.model.access.csv b/odoo_to_odoo_bemade/security/ir.model.access.csv index 845ffed..3aad5b3 100644 --- a/odoo_to_odoo_bemade/security/ir.model.access.csv +++ b/odoo_to_odoo_bemade/security/ir.model.access.csv @@ -1,11 +1,5 @@ id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink -access_odoo_to_bemade_sync_model_admin,odoo.to.bemade.sync.model admin,model_odoo_to_bemade_sync_model,base.group_system,1,1,1,1 -access_odoo_to_bemade_sync_model_user,odoo.to.bemade.sync.model user,model_odoo_to_bemade_sync_model,base.group_user,1,0,0,0 -access_odoo_to_bemade_sync_model_field_admin,odoo.to.bemade.sync.model.field admin,model_odoo_to_bemade_sync_model_field,base.group_system,1,1,1,1 -access_odoo_to_bemade_sync_model_field_user,odoo.to.bemade.sync.model.field user,model_odoo_to_bemade_sync_model_field,base.group_user,1,0,0,0 access_odoo_to_bemade_instance_admin,odoo.to.bemade.instance admin,model_odoo_to_bemade_instance,base.group_system,1,1,1,1 access_odoo_to_bemade_instance_user,odoo.to.bemade.instance user,model_odoo_to_bemade_instance,base.group_user,1,0,0,0 -access_odoo_to_bemade_sync_log_admin,odoo.to.bemade.sync.log admin,model_odoo_to_bemade_sync_log,base.group_system,1,1,1,1 -access_odoo_to_bemade_sync_log_user,odoo.to.bemade.sync.log user,model_odoo_to_bemade_sync_log,base.group_user,1,0,0,0 -access_odoo_to_bemade_sync_queue_admin,odoo.to.bemade.sync.queue admin,model_odoo_to_bemade_sync_queue,base.group_system,1,1,1,1 -access_odoo_to_bemade_sync_queue_user,odoo.to.bemade.sync.queue user,model_odoo_to_bemade_sync_queue,base.group_user,1,0,0,0 +access_odoo_to_bemade_assign_project_wizard_admin,odoo.to.bemade.assign.project.wizard admin,model_odoo_to_bemade_assign_project_wizard,base.group_system,1,1,1,1 +access_odoo_to_bemade_assign_project_wizard_user,odoo.to.bemade.assign.project.wizard user,model_odoo_to_bemade_assign_project_wizard,base.group_user,1,1,1,0 diff --git a/odoo_to_odoo_bemade/views/api_key_views.xml b/odoo_to_odoo_bemade/views/api_key_views.xml deleted file mode 100644 index 796e2b2..0000000 --- a/odoo_to_odoo_bemade/views/api_key_views.xml +++ /dev/null @@ -1,142 +0,0 @@ - - - - - odoo.to.bemade.api.key.tree - odoo.to.bemade.api.key - - - - - - - - - - - - - - - - odoo.to.bemade.api.key.form - odoo.to.bemade.api.key - -
-
- - - -
- -
-

- -

-
- - - - - - - - - - - - - - - - - - - not instance_ids - - - - - - - - - - -
-
-
-
- - - - odoo.to.bemade.api.key.search - odoo.to.bemade.api.key - - - - - - - - - - - - - - - - - - - odoo.to.bemade.api.key.display.form - odoo.to.bemade.api.key.display - -
- - - - - - - -
-
-
-
-
- - - - API Keys - odoo.to.bemade.api.key - tree,form - {'search_default_active': 1} - -

- No API keys found -

-

- Create a new API key for authenticating remote Odoo instances. -

-
-
- - - -
diff --git a/odoo_to_odoo_bemade/views/odoo_to_bemade_instance_views.xml b/odoo_to_odoo_bemade/views/odoo_to_bemade_instance_views.xml new file mode 100644 index 0000000..c373fe2 --- /dev/null +++ b/odoo_to_odoo_bemade/views/odoo_to_bemade_instance_views.xml @@ -0,0 +1,64 @@ + + + + odoo.to.bemade.instance.list + odoo.to.bemade.instance + + + + + + + + + + + + + odoo.to.bemade.instance.form + odoo.to.bemade.instance + +
+ +
+
+ + + + + + + + + + + + + + + + + + + + + +
+
+
+
+ + + Client Instances + odoo.to.bemade.instance + list,form + +

+ Configure client Odoo instances for project synchronization. +

+
+
+ + +
diff --git a/odoo_to_odoo_bemade/views/sync_instance_views.xml b/odoo_to_odoo_bemade/views/sync_instance_views.xml index f5bc6c2..667745a 100644 --- a/odoo_to_odoo_bemade/views/sync_instance_views.xml +++ b/odoo_to_odoo_bemade/views/sync_instance_views.xml @@ -34,7 +34,7 @@ state == 'connected' use_api_key - + not use_api_key use_api_key diff --git a/odoo_to_odoo_bemade/wizards/__init__.py b/odoo_to_odoo_bemade/wizards/__init__.py new file mode 100644 index 0000000..32e8c03 --- /dev/null +++ b/odoo_to_odoo_bemade/wizards/__init__.py @@ -0,0 +1 @@ +from . import assign_project_wizard \ No newline at end of file diff --git a/odoo_to_odoo_bemade/wizards/assign_project_wizard.py b/odoo_to_odoo_bemade/wizards/assign_project_wizard.py new file mode 100644 index 0000000..40d7e1f --- /dev/null +++ b/odoo_to_odoo_bemade/wizards/assign_project_wizard.py @@ -0,0 +1,159 @@ +# -*- coding: utf-8 -*- + +import uuid +import random +import string +from odoo import api, fields, models, _ +from odoo.exceptions import UserError, ValidationError + + +class OdooToBemadeAssignProjectWizard(models.TransientModel): + _name = 'odoo.to.bemade.assign.project.wizard' + _description = 'Assign Project to Bemade Client' + + project_id = fields.Many2one('project.project', string='Project', required=True) + client_instance_id = fields.Many2one('odoo.to.bemade.instance', string='Client Instance', required=True) + project_key = fields.Char(string='Project Key', readonly=True) + client_api_token = fields.Char(string='Client API Token', readonly=True) + sync_project_task = fields.Boolean(string='Sync Tasks', default=True) + sync_project_timesheet = fields.Boolean(string='Sync Timesheets', default=True) + protocol = fields.Selection([ + ('xmlrpc', 'XML-RPC'), + ('jsonrpc', 'JSON-RPC'), + ('odoorpc', 'OdooRPC') + ], string='Protocol', default='jsonrpc', required=True) + + @api.model + def default_get(self, fields_list): + """Prefill defaults from system parameters and context. + + - project_id from context (default_project_id) + - protocol from bemade.sync.default_connection_type + - client_instance_id from existing instance matching URL/DB/username + """ + res = super().default_get(fields_list) + ICP = self.env['ir.config_parameter'].sudo() + + # Context default project + if 'project_id' in fields_list and self.env.context.get('default_project_id'): + res['project_id'] = self.env.context['default_project_id'] + + # Protocol default + proto = ICP.get_param('bemade.sync.default_connection_type') or 'jsonrpc' + if 'protocol' in fields_list and proto: + res.setdefault('protocol', proto) + + # Try to preselect an instance based on defaults + url = ICP.get_param('bemade.sync.default_url') + database = ICP.get_param('bemade.sync.default_database') + username = ICP.get_param('bemade.sync.default_username') + + domain = [] + if url: + domain.append(('url', '=', url)) + if database: + domain.append(('database', '=', database)) + if username: + domain.append(('username', '=', username)) + + if 'client_instance_id' in fields_list and domain: + instance = self.env['odoo.to.bemade.instance'].search(domain, limit=1) + if instance: + res.setdefault('client_instance_id', instance.id) + + return res + + @api.onchange('project_id', 'client_instance_id') + def _onchange_generate_keys(self): + """Generate unique project key and API token""" + if self.project_id and self.client_instance_id: + # Generate random 8-character project key + chars = string.ascii_uppercase + string.digits + self.project_key = ''.join(random.choice(chars) for _ in range(8)) + + # Generate UUID for API token + self.client_api_token = str(uuid.uuid4()) + + def action_assign_project(self): + """Assign project to client and create sync configuration""" + self.ensure_one() + + if not self.project_id: + raise UserError(_("Please select a project to assign")) + + if not self.client_instance_id: + raise UserError(_("Please select a client instance")) + + if not self.project_key or not self.client_api_token: + self._onchange_generate_keys() + + # Create sync project + sync_project_vals = { + 'name': f"{self.project_id.name} - {self.client_instance_id.name}", + 'project_id': self.project_id.id, + 'client_instance_id': self.client_instance_id.id, + 'state': 'draft', + 'is_client_project': True, + 'client_key': self.project_key, + 'client_api_token': self.client_api_token, + 'remote_url': self.client_instance_id.url, + 'remote_database': self.client_instance_id.database, + 'remote_username': self.client_instance_id.username, + 'remote_api_key': self.client_instance_id.api_key, + 'protocol': self.protocol, + } + + sync_project = self.env['sync.project'].create(sync_project_vals) + + # Update project with bemade flags + self.project_id.write({ + 'is_bemade_project': True, + 'bemade_sync_enabled': True, + 'bemade_project_key': self.project_key, + }) + + # Create sync models for project and tasks + self._create_sync_model(sync_project, 'project.project') + + if self.sync_project_task: + self._create_sync_model(sync_project, 'project.task') + + if self.sync_project_timesheet and hasattr(self.env, 'account.analytic.line'): + self._create_sync_model(sync_project, 'account.analytic.line') + + # Show success message + return { + 'type': 'ir.actions.client', + 'tag': 'display_notification', + 'params': { + 'title': _('Project Assigned'), + 'message': _(f'Project {self.project_id.name} has been assigned to {self.client_instance_id.name} with key {self.project_key}'), + 'sticky': False, + 'next': { + 'type': 'ir.actions.act_window', + 'name': _('Sync Project'), + 'res_model': 'sync.project', + 'res_id': sync_project.id, + 'view_mode': 'form', + 'target': 'current', + } + } + } + + def _create_sync_model(self, sync_project, model_name): + """Create sync model configuration for the given model""" + model_vals = { + 'name': f"{model_name} - {sync_project.name}", + 'sync_project_id': sync_project.id, + 'model_name': model_name, + 'direction': 'bidirectional', + 'state': 'draft', + } + + sync_model = self.env['sync.model'].create(model_vals) + + # Auto-populate fields based on model + if hasattr(sync_model, 'action_auto_sync_fields'): + sync_model.action_auto_sync_fields() + + return sync_model diff --git a/odoo_to_odoo_bemade/wizards/assign_project_wizard_view.xml b/odoo_to_odoo_bemade/wizards/assign_project_wizard_view.xml new file mode 100644 index 0000000..c0261e4 --- /dev/null +++ b/odoo_to_odoo_bemade/wizards/assign_project_wizard_view.xml @@ -0,0 +1,44 @@ + + + + + + odoo.to.bemade.assign.project.wizard.form + odoo.to.bemade.assign.project.wizard + +
+ + + + + + + + + + + + + + + +
+
+
+
+
+ + + + Assign to Client + odoo.to.bemade.assign.project.wizard + form + new + + form + {'default_project_id': active_id} + +
+
diff --git a/odoo_to_odoo_bemade_customer/README.md b/odoo_to_odoo_bemade_customer/README.md index dd33ce4..6da3e18 100644 --- a/odoo_to_odoo_bemade_customer/README.md +++ b/odoo_to_odoo_bemade_customer/README.md @@ -1,83 +1,191 @@ # Odoo to Odoo Bemade Customer ## Overview -The `odoo_to_odoo_bemade_customer` module provides a specialized connector for Bemade customers to securely synchronize with the central Odoo.bemade.org platform. It simplifies the configuration process and offers a streamlined experience tailored specifically for Bemade's customer environments. +Complete bidirectional synchronization system for Bemade customers to securely sync with Odoo.bemade.org. Features wizards for project assignment/reception, multi-protocol support (XML-RPC, JSON-RPC, OdooRPC), and automated configuration. ## Features -- **Secure Connection**: Pre-configured secure connection to Odoo.bemade.org -- **Simplified Setup**: Automated configuration with minimal manual steps -- **Specialized Synchronization**: Optimized for Bemade's customer use cases -- **Automated Monitoring**: Built-in monitoring and error reporting -- **Customer-Specific Interface**: Simplified UI designed for end-users -## Key Components +### ✅ Bidirectional Sync Wizards +- **Receive Project Wizard**: `odoo.to.bemade.customer.receive.wizard` +- **Assign Project Wizard**: `odoo.to.bemade.assign.project.wizard` (server-side) +- **One-click setup**: Automated configuration with generated keys +- **Protocol selection**: Choose XML-RPC, JSON-RPC, or OdooRPC -### Sync Configuration -A centralized configuration system that: -- Automatically detects required synchronization models -- Pre-configures field mappings based on best practices -- Provides one-click setup for common synchronization scenarios -- Validates configurations to prevent common issues +### ✅ Multi-Protocol Support +- **XML-RPC**: Traditional XML-RPC with API key authentication +- **JSON-RPC**: JSON-based protocol with Odoo 18 API format +- **OdooRPC**: Native OdooRPC library with encrypted credentials -### Secure Connection Management -- Handles authentication securely -- Manages API keys and credentials -- Implements proper encryption and security measures -- Provides connection health monitoring +### ✅ Secure Authentication +- **Encrypted API keys**: AES encryption for secure storage +- **Project keys**: Unique identifiers for each sync project +- **Token generation**: Automatic API token creation and sharing -### Specialized Logging -- Customer-focused log entries -- Simplified troubleshooting information -- Automatic reporting of critical issues -- Integration with Bemade's support systems +### ✅ Project Management +- **Project assignment**: Server assigns projects to clients +- **Project reception**: Client receives and configures projects +- **Key management**: Automatic key/token generation and validation -## Setup and Configuration +## Installation -### Initial Setup -1. Install the module -2. Navigate to **Synchronization > Bemade Customer > Configuration** -3. Enter your Bemade customer credentials -4. Click "Initialize Connection" to establish the secure link +### Prerequisites +- Odoo 17+ or 18+ +- `odoo_to_odoo_sync` module (base framework) +- Network access to Odoo.bemade.org -### Configuring Synchronization -1. Navigate to **Synchronization > Bemade Customer > Models** -2. Select from the list of recommended synchronization models -3. Review and adjust the pre-configured field mappings if needed -4. Activate synchronization with the "Enable" toggle +### Installation Steps +1. Install base dependencies: + ```bash + pip install odoorpc # For OdooRPC protocol + ``` -### Monitoring -- View synchronization status in **Synchronization > Bemade Customer > Dashboard** -- Check logs in **Synchronization > Bemade Customer > Logs** -- Monitor queue in **Synchronization > Bemade Customer > Queue** +2. Install in Odoo: + - Go to Apps → Search "odoo_to_odoo_bemade_customer" + - Click Install -## Usage Scenarios +## Configuration -### Initial Data Migration -- Configure models for one-time or continuous synchronization -- Use the "Initial Sync" wizard to perform bulk data transfer -- Monitor progress through the dedicated dashboard +### Server Side (Bemade) +1. Install `odoo_to_odoo_bemade` module +2. Navigate to **Project → Bemade Sync → Assign to Client** +3. Select project and configure sync settings +4. Generate project key and API token +5. Share credentials with customer -### Ongoing Synchronization -- Automatic synchronization based on configured triggers -- Manual synchronization options for immediate updates -- Scheduled synchronization for regular data exchange +### Client Side (Customer) +1. Install `odoo_to_odoo_bemade_customer` module +2. Navigate to **Project → Bemade Sync → Receive from Bemade** +3. Enter provided credentials: + - Bemade server URL (default: https://odoo.bemade.org) + - Database name + - Username + - API Key + - Project Key +4. Select protocol (XML-RPC/JSON-RPC/OdooRPC) +5. Test connection and receive project -### Troubleshooting -- Use the simplified log viewer to identify issues -- Access the connection test tools to verify connectivity -- Contact Bemade support directly through the integrated support channel +## Usage -## Technical Notes -- Built on top of `odoo_to_odoo_sync` core framework -- Specialized for secure connection to Odoo.bemade.org -- Implements customer-specific optimizations and simplifications -- Provides streamlined user experience for non-technical users +### Using the Receive Project Wizard +1. **Open Wizard**: Project → Bemade Sync → Receive from Bemade +2. **Enter Connection Details**: + - Server URL: https://odoo.bemade.org (or custom) + - Database: Target database name + - Username: Your Bemade username + - API Key: Generated from user preferences + - Project Key: Provided by Bemade +3. **Select Protocol**: XML-RPC, JSON-RPC, or OdooRPC +4. **Test Connection**: Validate settings before proceeding +5. **Receive Project**: Import project data and tasks -## Requirements -- Odoo 18.0 -- `odoo_to_odoo_sync` module -- Valid Bemade customer account -- Network connectivity to Odoo.bemade.org +### Protocol Configuration +- **XML-RPC**: Traditional XML-RPC protocol +- **JSON-RPC**: JSON-based with Odoo 18 format: `{'scope': 'rpc', 'key': 'api_key'}` +- **OdooRPC**: Native library with automatic connection handling -## License -LGPL-3.0 +### Field Mapping +- **Automatic**: Pre-configured field mappings for common models +- **Manual**: Override mappings via sync model configuration +- **Transformations**: Support for field value transformations + +## Bidirectional Sync Workflow + +### 1. Server Assignment +``` +Bemade Server → Assign Project Wizard → Generate Keys → Share with Client +``` + +### 2. Client Reception +``` +Client → Receive Project Wizard → Enter Credentials → Import Project → Configure Sync +``` + +### 3. Ongoing Synchronization +- **Automatic**: Real-time sync based on triggers +- **Manual**: Force sync via project actions +- **Scheduled**: Cron-based regular synchronization + +## API Authentication Format + +### Odoo 18 API Key Format +```python +# XML-RPC +{'scope': 'rpc', 'key': 'your_api_key_here'} + +# JSON-RPC +{ + "jsonrpc": "2.0", + "method": "call", + "params": { + "service": "common", + "method": "login", + "args": ["database", "username", {"scope": "rpc", "key": "api_key"}] + } +} + +# OdooRPC +odoo.login(database, username, api_key) +``` + +## Troubleshooting + +### Common Issues +1. **Connection Failed**: Check URL, credentials, and network access +2. **Authentication Error**: Verify API key format (Odoo 18 requires scope parameter) +3. **Project Not Found**: Ensure project key is correct and project exists +4. **Protocol Error**: Try different protocol (XML-RPC/JSON-RPC/OdooRPC) + +### Debug Commands +```python +# Enable debug logging +import logging +logging.getLogger('odoo.sync').setLevel(logging.DEBUG) + +# Test connection programmatically +sync_instance = env['odoo.sync.instance'].search([('name', '=', 'bemade')]) +sync_instance.test_connection() +``` + +## Project Configuration + +### Project Fields Added +- **is_bemade_project**: Boolean flag for Bemade projects +- **bemade_project_key**: Unique project identifier +- **bemade_sync_enabled**: Enable/disable synchronization + +### Security Access +- **User Groups**: Bemade Sync User, Bemade Sync Manager +- **Permissions**: Read, Create, Write, Delete based on role + +## Development + +### Extending Functionality +- **Custom Wizards**: Inherit from base wizard classes +- **Protocol Handlers**: Add new protocol support +- **Field Transformers**: Custom field value transformations +- **Conflict Resolvers**: Custom conflict resolution strategies + +### Testing +```bash +# Run module tests +./odoo-bin -d your_database -u odoo_to_odoo_bemade_customer --test-enable + +# Test specific wizard +env['odoo.to.bemade.customer.receive.wizard'].create({...}).action_receive_project() +``` + +## Support + +### Getting Help +- **Logs**: Check Odoo logs and sync logs +- **Connection Test**: Use built-in connection validation +- **Support**: Contact Bemade support with sync logs + +### Log Locations +- **Odoo Logs**: `/var/log/odoo/odoo.log` +- **Sync Logs**: Settings → Technical → Odoo Sync → Logs +- **Debug Mode**: Enable debug logging for detailed information + +--- +*Last Updated: 2025-08-16* +*Compatible with Odoo 17+ and 18+* diff --git a/odoo_to_odoo_bemade_customer/__init__.py b/odoo_to_odoo_bemade_customer/__init__.py index 4ee3d7a..367f671 100644 --- a/odoo_to_odoo_bemade_customer/__init__.py +++ b/odoo_to_odoo_bemade_customer/__init__.py @@ -1,4 +1,5 @@ from . import models +from . import wizards from odoo import api, SUPERUSER_ID diff --git a/odoo_to_odoo_bemade_customer/__manifest__.py b/odoo_to_odoo_bemade_customer/__manifest__.py index 1fa930a..76ac5b7 100644 --- a/odoo_to_odoo_bemade_customer/__manifest__.py +++ b/odoo_to_odoo_bemade_customer/__manifest__.py @@ -16,6 +16,8 @@ ], 'data': [ 'data/ir_config_parameter_data.xml', + 'wizards/receive_project_wizard_view.xml', + 'views/project_views.xml', ], 'installable': True, 'application': False, diff --git a/odoo_to_odoo_bemade_customer/models/__init__.py b/odoo_to_odoo_bemade_customer/models/__init__.py index 28901a6..9f11755 100644 --- a/odoo_to_odoo_bemade_customer/models/__init__.py +++ b/odoo_to_odoo_bemade_customer/models/__init__.py @@ -5,3 +5,4 @@ from . import sync_model_field from . import sync_queue from . import sync_log from . import sync_manager +from . import project diff --git a/odoo_to_odoo_bemade_customer/models/project.py b/odoo_to_odoo_bemade_customer/models/project.py new file mode 100644 index 0000000..eadb4e5 --- /dev/null +++ b/odoo_to_odoo_bemade_customer/models/project.py @@ -0,0 +1,81 @@ +# -*- coding: utf-8 -*- + +from odoo import api, fields, models, _ +from odoo.exceptions import UserError +import logging + +_logger = logging.getLogger(__name__) + + +class ProjectProject(models.Model): + _inherit = 'project.project' + + is_bemade_project = fields.Boolean(string='Bemade Project', default=False) + bemade_project_key = fields.Char(string='Bemade Project Key', readonly=True) + bemade_sync_enabled = fields.Boolean(string='Bemade Sync Enabled', default=False) + + def action_receive_bemade_project(self): + """Action to receive project from Bemade server""" + self.ensure_one() + return { + 'name': _('Receive Bemade Project'), + 'type': 'ir.actions.act_window', + 'res_model': 'odoo.to.bemade.customer.receive.wizard', + 'view_mode': 'form', + 'target': 'new', + 'context': { + 'default_project_id': self.id, + } + } + + def _sync_from_bemade(self, bemade_data): + """Sync project data from Bemade server""" + self.ensure_one() + try: + # Update project with received data + self.write({ + 'name': bemade_data.get('name', self.name), + 'description': bemade_data.get('description', self.description), + 'is_bemade_project': True, + 'bemade_project_key': bemade_data.get('project_key'), + 'bemade_sync_enabled': True, + }) + + # Sync tasks if provided + if bemade_data.get('tasks'): + self._sync_bemade_tasks(bemade_data['tasks']) + + return True + except Exception as e: + _logger.error("Error syncing from Bemade: %s", e) + return False + + def _sync_bemade_tasks(self, tasks_data): + """Sync tasks from Bemade server""" + task_obj = self.env['project.task'] + for task_data in tasks_data: + existing_task = task_obj.search([ + ('project_id', '=', self.id), + ('bemade_task_key', '=', task_data.get('task_key')) + ], limit=1) + + task_vals = { + 'name': task_data.get('name'), + 'description': task_data.get('description'), + 'project_id': self.id, + 'bemade_task_key': task_data.get('task_key'), + 'is_bemade_task': True, + } + + if existing_task: + existing_task.write(task_vals) + else: + task_obj.create(task_vals) + + +class ProjectTask(models.Model): + _inherit = 'project.task' + + is_bemade_task = fields.Boolean(string='Bemade Task', default=False) + bemade_task_key = fields.Char(string='Bemade Task Key', readonly=True) + bemade_sync_enabled = fields.Boolean(string='Bemade Sync Enabled', default=False) diff --git a/odoo_to_odoo_bemade_customer/models/sync_model_field.py b/odoo_to_odoo_bemade_customer/models/sync_model_field.py index 083b5d1..fbe408d 100644 --- a/odoo_to_odoo_bemade_customer/models/sync_model_field.py +++ b/odoo_to_odoo_bemade_customer/models/sync_model_field.py @@ -56,17 +56,15 @@ class OdooToBemadeCustomerSyncModelField(models.Model): help='Indique si ce champ est utilisé pour identifier l\'enregistrement chez Bemade' ) - transform_type = fields.Selection( - selection_add=[ - ('none', 'Aucune transformation'), - ('function', 'Fonction Python'), - ('mapping', 'Mapping de valeurs') - ], - ondelete={'none': 'set default', 'function': 'set default', 'mapping': 'set default'}, - default='none', - required=True, - help='Type de transformation à appliquer au champ lors de la synchronisation' - ) + transform_type = fields.Selection([ + ('none', 'Aucune transformation'), + ('function', 'Fonction Python'), + ('mapping', 'Mapping de valeurs'), + ('direct', 'Direct'), + ('computed', 'Computed'), + ('relation', 'Relation') + ], string='Type de transformation', default='none', required=True, + help='Type de transformation à appliquer au champ lors de la synchronisation') transform_mapping = fields.Text( string='Mapping de transformation', diff --git a/odoo_to_odoo_bemade_customer/security/ir.model.access.csv b/odoo_to_odoo_bemade_customer/security/ir.model.access.csv index 0481382..ffe4c79 100644 --- a/odoo_to_odoo_bemade_customer/security/ir.model.access.csv +++ b/odoo_to_odoo_bemade_customer/security/ir.model.access.csv @@ -13,3 +13,5 @@ access_odoo_to_bemade_customer_sync_manager_admin,odoo.to.bemade.customer.sync.m access_odoo_to_bemade_customer_sync_manager_user,odoo.to.bemade.customer.sync.manager user,model_odoo_to_bemade_customer_sync_manager,base.group_user,1,0,0,0 access_odoo_to_bemade_customer_config_admin,odoo.to.bemade.customer.config admin,model_odoo_to_bemade_customer_config,base.group_system,1,1,1,1 access_odoo_to_bemade_customer_config_user,odoo.to.bemade.customer.config user,model_odoo_to_bemade_customer_config,base.group_user,1,0,0,0 +access_odoo_to_bemade_customer_receive_wizard_admin,odoo.to.bemade.customer.receive.wizard admin,model_odoo_to_bemade_customer_receive_wizard,base.group_system,1,1,1,1 +access_odoo_to_bemade_customer_receive_wizard_user,odoo.to.bemade.customer.receive.wizard user,model_odoo_to_bemade_customer_receive_wizard,base.group_user,1,1,1,0 diff --git a/odoo_to_odoo_bemade_customer/views/project_views.xml b/odoo_to_odoo_bemade_customer/views/project_views.xml new file mode 100644 index 0000000..64be37b --- /dev/null +++ b/odoo_to_odoo_bemade_customer/views/project_views.xml @@ -0,0 +1,33 @@ + + + + project.project.form.inherit.bemade.customer + project.project + + + +