openwebui and product

This commit is contained in:
xtremxpert 2025-02-18 08:55:09 -05:00
parent b605f77e55
commit a3b4d3ed39
37 changed files with 1224 additions and 182 deletions

View file

@ -23,7 +23,6 @@ class OdooSyncLog(models.Model):
string='Name',
compute='_compute_name'
)
queue_id = fields.Many2one(
comodel_name='odoo.sync.queue',
string='Queue Entry',
@ -38,7 +37,7 @@ class OdooSyncLog(models.Model):
required=True
)
message = fields.Text(
string='Message',
string='Message'
)
details = fields.Text(
string='Technical Details'

View file

@ -0,0 +1,110 @@
# Spécifications du module OpenWebUI Integration
## Objectif
Ce module fournit une intégration complète entre Odoo et OpenWebUI, permettant l'utilisation de modèles d'IA avancés dans diverses fonctionnalités d'Odoo. Il sert de base pour tous les modules qui souhaitent utiliser les capacités d'IA d'OpenWebUI.
## Architecture du Module
### Structure des Répertoires
```
openwebui_integration/
├── controllers/ # Contrôleurs pour les endpoints web
├── models/ # Modèles de données
├── security/ # Fichiers de sécurité et accès
├── static/ # Ressources statiques
└── views/ # Vues XML Odoo
```
## Composants Principaux
### 1. OpenWebUI Bot Mixin (`openwebui.bot.mixin`)
Mixin permettant d'ajouter des fonctionnalités de bot à n'importe quel modèle Odoo.
#### Fonctionnalités clés:
- Gestion du contexte du bot
- Génération et traitement des messages
- Gestion des réponses
- Points de personnalisation spécifiques au modèle
#### Méthodes principales:
- `_get_bot_context()`: Récupère le contexte et les paramètres du bot
- `_apply_logic()`: Applique la logique du bot à un enregistrement
- `_generate_bot_message()`: Génère le message à envoyer (à surcharger)
- `_process_bot_response()`: Traite la réponse du bot (à surcharger)
### 2. Modèle OpenWebUI (`openwebui.model`)
Gère les modèles d'IA disponibles via OpenWebUI.
#### Caractéristiques:
- Modèles par défaut: GPT-3.5 Turbo, GPT-4, Claude 2
- Identifiant unique par entreprise
- Gestion de l'état actif/inactif
- Support des modèles temporaires pour les tests
#### Contraintes de sécurité:
- Création manuelle interdite (sauf modèles temporaires)
- Modifications limitées (activation/désactivation uniquement)
- Suppression contrôlée
### 3. Configuration par Entreprise
Extension du modèle `res.company` pour la configuration OpenWebUI.
#### Paramètres configurables:
- `openwebui_enabled`: Activation de l'intégration
- `openwebui_api_url`: URL de l'API
- `openwebui_api_key`: Clé d'API
- `openwebui_verify_ssl`: Vérification du certificat SSL
- `openwebui_timeout`: Délai d'attente des requêtes
- `openwebui_default_model_id`: Modèle d'IA par défaut à utiliser
- Sélectionnable uniquement parmi les modèles actifs
- Utilisé comme modèle par défaut pour toutes les requêtes IA
- Peut être surchargé au niveau des modules spécifiques
#### Fonctionnalités de gestion:
- Test de connexion à l'API
- Synchronisation des modèles disponibles
- Gestion des modèles par entreprise
## Sécurité et Gestion des Erreurs
### Sécurité
- Authentification API via clé
- Vérification SSL configurable
- Contrôle d'accès par entreprise
- Protection contre la création/modification non autorisée
### Gestion des Erreurs
- Validation des paramètres de connexion
- Gestion des timeouts
- Traitement des erreurs API
- Validation des réponses
## Intégration et Utilisation
### Étapes d'installation
1. Installation du module via Odoo
2. Configuration des paramètres OpenWebUI dans la configuration de l'entreprise
3. Test de la connexion API
4. Synchronisation initiale des modèles
### Développement d'Extensions
1. Hériter du mixin `openwebui.bot.mixin`
2. Implémenter les méthodes de génération et traitement
3. Configurer les paramètres spécifiques au modèle
4. Gérer les réponses selon les besoins
### Maintenance
- Nettoyage périodique des modèles temporaires
- Surveillance des timeouts et erreurs
- Mise à jour des modèles disponibles
## Dépendances
- Module `mail` d'Odoo
- Accès à une instance OpenWebUI
- Python 3.x avec support SSL
## Notes Techniques
- Utilisation de requêtes HTTP asynchrones
- Cache des réponses API pour optimisation
- Support multi-entreprises
- Extensible pour différents cas d'usage

View file

@ -2,20 +2,20 @@
# Part of Odoo. See LICENSE file for full copyright and licensing details.
{
'name': 'OpenWebUI Integration',
'version': '1.0',
'summary': 'Integration with OpenWebUI for chatbots',
'version': '18.0.1.0.1',
'summary': 'Core integration with OpenWebUI',
'sequence': 10,
'description': """
OpenWebUI Integration
=====================
This module provides integration with OpenWebUI to create and manage chatbots.
This module provides core integration with OpenWebUI.
Features:
---------
* Connect to OpenWebUI API
* Manage models and configurations
* Create chatbots with specific behaviors
* Control access to bots by channels and users
* Base bot functionality and configuration
* Authentication and access control
""",
'category': 'Discuss',
'website': 'https://www.odoo.com/app/discuss',
@ -24,10 +24,10 @@ Features:
'security/security.xml',
'security/ir.model.access.csv',
'views/res_company_views.xml',
'views/openwebui_bot_views.xml',
],
'installable': True,
'application': False,
'auto_install': False,
'license': 'LGPL-3',
'i18n': True,
}

View file

@ -0,0 +1 @@
from . import pre-migration

View file

@ -0,0 +1,17 @@
def migrate(cr, version):
"""Add products_per_request column to res_company table."""
if not version:
return
# Add products_per_request column if it doesn't exist
cr.execute("""
SELECT column_name
FROM information_schema.columns
WHERE table_name='res_company'
AND column_name='openwebui_products_per_request'
""")
if not cr.fetchone():
cr.execute("""
ALTER TABLE res_company
ADD COLUMN openwebui_products_per_request integer DEFAULT 10
""")

View file

@ -3,7 +3,4 @@
from . import openwebui_bot_mixin
from . import openwebui_model
from . import openwebui_bot
from . import discuss_channel
from . import res_company
from . import res_users

View file

@ -1,7 +1,38 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo import models, fields, api, _
"""OpenWebUI Bot Mixin Module
This module provides a mixin class that can be used to add OpenWebUI bot
functionality to any Odoo model. It handles the integration between Odoo
models and OpenWebUI's AI models, providing:
- Bot context management
- Message generation and processing
- Response handling
- Model-specific customization points
Example usage:
class MyModel(models.Model, OpenWebUIBotMixin):
_name = 'my.model'
def _generate_bot_message(self, record, values, command=None):
return f"Process this: {record.name}"
def _process_bot_response(self, values, response):
values['processed_text'] = response
return values
"""
import json
import logging
import re
import time
import requests
from odoo import models, _
from odoo.exceptions import UserError
_logger = logging.getLogger(__name__)
class OpenWebUIBotMixin(models.AbstractModel):
"""Mixin to add OpenWebUI bot functionality to any model.
@ -14,51 +45,106 @@ class OpenWebUIBotMixin(models.AbstractModel):
_name = 'openwebui.bot.mixin'
_description = 'OpenWebUI Bot Mixin'
def _get_bot_context(self, bot):
"""Gets the bot context with its parameters"""
return {
'instructions': bot.instructions,
'model': bot.model_id,
'context': bot.context or '',
'max_tokens': bot.max_tokens,
'temperature': bot.temperature,
}
def _get_model(self, model):
"""Gets the model to use"""
return model
def _apply_logic(self, record, values, command=None):
"""Applies the bot logic to a record.
Args:
record: The record to apply the logic to
values: The values to use for message generation
command: Optional command for the bot
Returns:
dict: The updated values
"""
bot = values.get('bot')
if not bot or not bot.model_id:
"""Applies the model logic to a record."""
model = values.get('bot')
if not model:
return values
# Prepare the bot context
bot_context = self._get_bot_context(bot)
# Get the model
model = self._get_model(model)
# Generate the message for the bot
# Generate the message for the model
message = self._generate_bot_message(record, values, command)
# Send the message to the model
response = bot_context['model'].send_message(
message=message,
context=bot_context['context'],
max_tokens=bot_context['max_tokens'],
temperature=bot_context['temperature'],
instructions=bot_context['instructions']
)
# Paramètres de retry
max_retries = 3
base_delay = 2 # délai initial en secondes
# Obtenir le timeout de la configuration de la compagnie
company = self.env.company
timeout = company.openwebui_timeout
last_error = None
for attempt in range(max_retries):
try:
# Send the message to the model
_logger.info('Attempt %d/%d: Sending message to model (timeout=%ds)',
attempt + 1, max_retries, timeout)
response = model.send_message(message=message, timeout=timeout)
# Si la réponse contient une erreur de connexion, on la traite comme une exception
if isinstance(response, str) and "Connection error" in response:
raise ConnectionError(response)
_logger.info('Received response from model: %r', response)
if response:
# Update the values with the bot response
values = self._process_bot_response(values, response)
if not response:
raise ValueError("Empty response from model")
return values
if not isinstance(response, str):
raise ValueError(f"Invalid response type: {type(response)}")
# Clean up the response
# Supprimer les blocs de code markdown
response = re.sub(r'```json\n|\n```', '', response.strip())
_logger.debug('Response after markdown cleanup: %r', response)
try:
# Essayer de parser directement la réponse nettoyée
parsed_response = json.loads(response)
clean_response = response
except json.JSONDecodeError as e:
_logger.warning('Failed to parse response directly: %s', e)
# Si échec, chercher une liste JSON dans la réponse
start = response.find('[')
end = response.rfind(']')
if start == -1 or end == -1:
_logger.error('No JSON list markers found in response: %r', response)
raise ValueError("No JSON list found in response")
clean_response = response[start:end + 1]
_logger.debug('Extracted JSON list: %r', clean_response)
try:
parsed_response = json.loads(clean_response)
except json.JSONDecodeError as e:
_logger.error('Failed to parse extracted JSON: %s', e)
raise ValueError("Invalid JSON list in response")
if not isinstance(parsed_response, list):
_logger.error('Response is not a list: %r', parsed_response)
raise ValueError("Response must be a JSON list")
_logger.info('Successfully parsed response as list with %d items', len(parsed_response))
# Si on arrive ici, tout s'est bien passé
# Update the values with the cleaned response
values = self._process_bot_response(values, clean_response)
return values
except (ConnectionError, TimeoutError) as e:
last_error = e
if attempt < max_retries - 1: # Ne pas attendre après la dernière tentative
delay = base_delay * (2 ** attempt) # Délai exponentiel: 2s, 4s, 8s
_logger.warning('Connection error on attempt %d: %s. Retrying in %d seconds...',
attempt + 1, str(e), delay)
time.sleep(delay)
else:
_logger.error('All %d connection attempts failed. Last error: %s', max_retries, str(e))
except Exception as e:
# Pour les autres erreurs, on ne réessaie pas
_logger.error('Non-connection error occurred: %s', str(e))
raise e
# Si on arrive ici, toutes les tentatives ont échoué
raise last_error
def _generate_bot_message(self, record, values, command=None):
"""Generates the message to send to the bot.

View file

@ -1,12 +1,19 @@
# -*- coding: utf-8 -*-
# -*-
"""
OpenWebUI Model Module
This module defines the models needed for OpenWebUI integration in Odoo.
It includes user management, roles, and customizable interfaces.
"""
# Part of Odoo. See LICENSE file for full copyright and licensing details.
import requests
import logging
import json
from requests.exceptions import RequestException
import logging
import requests
from odoo import models, fields, api, _
from odoo.exceptions import UserError
from odoo.exceptions import UserError, AccessError
_logger = logging.getLogger(__name__)
_logger = logging.getLogger(__name__)
@ -59,7 +66,11 @@ class OpenWebUIModel(models.Model):
)
_sql_constraints = [
('unique_identifier', 'unique(identifier, company_id)', 'The identifier must be unique per company!')
(
'unique_identifier',
'unique(identifier, company_id)',
'The identifier must be unique per company!'
)
]
@api.model_create_multi
@ -69,11 +80,10 @@ class OpenWebUIModel(models.Model):
# Mark model as temporary if it starts with test_ or refresh_
if vals.get('identifier', '').startswith(('test_', 'refresh_')):
vals['is_temp'] = True
# Allow creation if it's a temporary model or in installation mode
if not (vals.get('is_temp') or self.env.context.get('install_mode')):
# Allow creation if it's a temporary model, in installation mode, or during sync
if not (vals.get('is_temp') or self.env.context.get('install_mode') or self.env.context.get('sync_models')):
raise UserError(_("OpenWebUI models cannot be created manually. Use the 'Refresh List' button to synchronize models from OpenWebUI."))
return super().create(vals_list)
def write(self, vals):
@ -90,8 +100,62 @@ class OpenWebUIModel(models.Model):
return super().unlink()
raise UserError(_("OpenWebUI models cannot be deleted manually. They are managed automatically during synchronization with OpenWebUI."))
def send_message(self, message, message_history=None, context=None, instructions=None, timeout=None):
"""Sends a message to the model and returns its response
Args:
message (str): The message to send
message_history (list): Optional list of previous messages in the format
[{'role': 'user'|'assistant', 'content': 'message'}, ...]
context (dict): Optional context to pass to the model
instructions (str): Optional system instructions
timeout (int): Optional timeout in seconds for this request
Returns:
str: The model's response or error message
"""
self.ensure_one()
# Initialize messages list with history if provided
messages = []
if message_history:
messages.extend(message_history)
# Add the current message
messages.append({'role': 'user', 'content': message})
data = {
'model': self.identifier,
'messages': messages,
}
if context:
data['context'] = context
if instructions:
data['system'] = instructions
success, result = self._make_request('chat/completions', method='POST', data=data, timeout=timeout)
if not success:
return f"Error: {result}"
try:
return result['choices'][0]['message']['content']
except (KeyError, IndexError) as e:
return f"Error processing response: {str(e)}"
def cleanup_temp_models(self):
"""Cleans up temporary models"""
"""Clean up temporary models from the database.
This method automatically removes models marked as temporary,
including:
- Models with identifier starting with 'test_'
- Models with identifier starting with 'refresh_'
- Temporary models created before the current date
Note:
Deletion is performed with 'sync_models' context to bypass
standard deletion restrictions.
"""
temp_models = self.search([
('is_temp', '=', True),
'|',
@ -104,7 +168,18 @@ class OpenWebUIModel(models.Model):
temp_models.with_context(sync_models=True).unlink()
def _get_company_config(self):
"""Gets the configuration of the current company"""
"""Get the OpenWebUI configuration for the current company.
Returns:
dict: A dictionary containing the OpenWebUI configuration:
{
'enabled': bool, # Whether integration is enabled
'api_url': str, # OpenWebUI API URL
'api_key': str, # API key for authentication
'verify_ssl': bool, # SSL certificate verification
'timeout': int # Request timeout in seconds
}
"""
company = self.env.company
return {
'enabled': company.openwebui_enabled,
@ -115,7 +190,27 @@ class OpenWebUIModel(models.Model):
}
def _make_request(self, endpoint, method='GET', data=None, files=None, timeout=None):
"""Makes a request to the OpenWebUI API"""
"""Make a request to the OpenWebUI API.
Args:
endpoint (str): API endpoint (without /api/ prefix)
method (str, optional): HTTP method to use. Defaults to 'GET'.
data (dict, optional): Data to send for POST/PUT/PATCH methods.
files (dict, optional): Files to send as multipart/form-data.
timeout (int, optional): Custom request timeout in seconds.
Returns:
tuple: A tuple (success, result) where:
- success (bool): True if request succeeded, False otherwise
- result (dict|str): JSON response if success, error message if failure
Note:
The method automatically handles:
- API key authentication
- SSL verification
- Content-Type headers
- Timeouts and connection errors
"""
config = self._get_company_config()
if not config['enabled']:
@ -152,24 +247,51 @@ class OpenWebUIModel(models.Model):
except requests.exceptions.SSLError:
return False, "SSL/TLS verification failed"
except requests.exceptions.ConnectionError:
return False, "Could not connect to server"
except requests.exceptions.Timeout:
return False, "Connection timed out"
except requests.exceptions.RequestException as e:
return False, f"Connection error: {str(e)}"
except (json.JSONDecodeError, ValueError) as e:
return False, f"Invalid response from server: {str(e)}"
except Exception as e:
return False, f"Unexpected error: {str(e)}"
except (TypeError, AttributeError) as e:
return False, f"Invalid request parameters: {str(e)}"
except KeyError as e:
return False, f"Missing required configuration: {str(e)}"
def test_connection(self):
"""Tests the connection to OpenWebUI"""
"""Test the connection to the OpenWebUI API.
This method performs a simple request to the /api/models endpoint
to verify that:
1. The API URL is accessible
2. The credentials are valid
3. The SSL configuration is correct
Returns:
tuple: A tuple (success, result) where:
- success (bool): True if test succeeded
- result (dict|str): Models list if success, error message if failure
Note:
Uses a reduced timeout of 5 seconds to avoid long waits.
"""
return self._make_request('models', timeout=5)
@api.model
def get_available_models(self):
"""Gets the list of available models from the API"""
"""Get the list of available models from the OpenWebUI API.
This method attempts to retrieve the list of models from the API.
If it fails, it returns a default list of common models.
Returns:
list: A list of tuples (id, name) of available models.
Example: [('gpt-3.5-turbo', 'GPT-3.5 Turbo'), ('gpt-4', 'GPT-4')]
Note:
- Handles both possible API response formats (list or dict)
- Returns DEFAULT_MODELS in case of error or unexpected format
- Errors are logged but don't interrupt execution
"""
success, result = self._make_request('models')
if not success:
@ -184,19 +306,43 @@ class OpenWebUIModel(models.Model):
else:
_logger.warning("Unexpected response format from OpenWebUI API")
return DEFAULT_MODELS
except Exception as e:
except (KeyError, TypeError, AttributeError) as e:
_logger.error(f"Error processing models response: {str(e)}")
return DEFAULT_MODELS
def sync_models(self):
"""Synchronizes models from OpenWebUI"""
"""Synchronize models from OpenWebUI.
This method ensures that local models match those available in OpenWebUI.
It will create new models and update existing ones as needed.
Returns:
tuple: A tuple (success, result) where:
- success (bool): True if synchronization succeeded
- result (str|None): Error message if failed, None if succeeded
"""
self.ensure_one()
success, result = self._sync_models()
return success, result
@api.model
def _sync_models(self):
"""Synchronizes models from OpenWebUI"""
"""Internal method to synchronize models from OpenWebUI.
This method performs the actual synchronization work:
1. Cleans up temporary models
2. Fetches current models from the API
3. Creates new models that don't exist locally
Returns:
tuple: A tuple (success, result) where:
- success (bool): True if synchronization succeeded
- result (str|None): Error message if failed, None if succeeded
Note:
This is an internal method called by sync_models().
It should not be called directly unless you need fine-grained control.
"""
# First clean up temporary models
self.cleanup_temp_models()
@ -215,48 +361,9 @@ class OpenWebUIModel(models.Model):
'description': model_data.get('description', ''),
})
return True, None
except Exception as e:
_logger.error(f"Error synchronizing models: {str(e)}")
return False, str(e)
def send_message(self, message, message_history=None, context=None, instructions=None):
"""Sends a message to the model and returns its response
Args:
message (str): The message to send
message_history (list): Optional list of previous messages in the format
[{'role': 'user'|'assistant', 'content': 'message'}, ...]
context (dict): Optional context to pass to the model
instructions (str): Optional system instructions
Returns:
str: The model's response or error message
"""
self.ensure_one()
# Initialize messages list with history if provided
messages = []
if message_history:
messages.extend(message_history)
# Add the current message
messages.append({'role': 'user', 'content': message})
data = {
'model': self.identifier,
'messages': messages,
}
if context:
data['context'] = context
if instructions:
data['system'] = instructions
success, result = self._make_request('chat/completions', method='POST', data=data)
if not success:
return f"Error: {result}"
try:
return result['choices'][0]['message']['content']
except (KeyError, IndexError) as e:
return f"Error processing response: {str(e)}"
except (KeyError, TypeError) as e:
_logger.error(f"Error processing model data: {str(e)}")
return False, f"Invalid model data format: {str(e)}"
except odoo.exceptions.AccessError as e:
_logger.error(f"Access error while creating model: {str(e)}")
return False, f"Permission denied: {str(e)}"

View file

@ -1,9 +1,15 @@
# -*- coding: utf-8 -*-
"""
This module extends the res.company model to add OpenWebUI configuration.
It manages OpenWebUI integration settings at the company level,
including service activation, API keys, and other configuration parameters.
"""
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo import models, fields, api, _
from odoo import models, fields, _
from odoo.exceptions import UserError
class ResCompany(models.Model):
_inherit = 'res.company'
@ -31,71 +37,135 @@ class ResCompany(models.Model):
openwebui_timeout = fields.Integer(
string='Timeout (seconds)',
default=30,
default=60,
help="Maximum wait time for API calls"
)
openwebui_products_per_request = fields.Integer(
string='Products per Request',
default=10,
help="Number of products to process in a single API request"
)
openwebui_models_ids = fields.One2many(
comodel_name='openwebui.model',
inverse_name='company_id',
string='OpenWebUI Models',
)
openwebui_default_model_id = fields.Many2one(
comodel_name='openwebui.model',
string='Default Model',
domain="[('company_id', '=', id), ('is_active', '=', True)]",
help="Default OpenWebUI model to use for AI requests"
)
def test_openwebui_connection(self):
"""Tests the connection to OpenWebUI"""
"""Test the connection to OpenWebUI API.
This method verifies the connection to OpenWebUI by:
1. Checking if OpenWebUI integration is enabled
2. Attempting to connect to the configured API endpoint
3. Validating the API credentials
Returns:
dict: A notification action with the test results:
{
'type': 'ir.actions.client',
'tag': 'display_notification',
'params': {
'title': 'Success',
'message': str,
'type': 'success',
'sticky': False
}
}
Raises:
UserError: If any of the following conditions occur:
- OpenWebUI is not enabled for the company
- API endpoint is not reachable
- Invalid API credentials
- Connection timeout
"""
self.ensure_one()
if not self.openwebui_enabled:
raise UserError(_("OpenWebUI is not enabled for this company."))
model = self.env['openwebui.model'].create({
'name': 'test_connection',
'identifier': 'test_connection',
'company_id': self.id,
'is_temp': True,
})
success, message = model.test_connection()
success, result = self.env['openwebui.model'].test_connection()
if not success:
raise UserError(message)
raise UserError(_("Connection test failed: %s") % result)
return {
'type': 'ir.actions.client',
'tag': 'display_notification',
'params': {
'title': _('Success'),
'message': _('Connection to OpenWebUI was successful.'),
'type': 'success',
'message': _('Connection to OpenWebUI successful!'),
'sticky': False,
'type': 'success',
}
}
def refresh_model_list(self):
"""Refreshes the list of models from OpenWebUI"""
"""Synchronize available AI models from OpenWebUI with Odoo.
This method performs the following operations:
1. Verifies that OpenWebUI integration is enabled for the company
2. Connects to the OpenWebUI API to fetch the latest model list
3. Updates the local database:
- Creates records for new models
- Updates existing model information
- Archives models that are no longer available
Technical Details:
- Uses the sync_models context to trigger a full synchronization
- Performs all operations in a single transaction
- Handles API connection errors gracefully
Returns:
dict: An action dictionary with the following structure:
{
'type': 'ir.actions.client',
'tag': 'display_notification',
'params': {
'title': str,
'message': str,
'sticky': bool,
'type': str,
}
}
Raises:
UserError: In the following cases:
- OpenWebUI is not enabled for the company
- API connection fails
- Model synchronization fails
Example:
>>> company = env['res.company'].browse(1)
>>> result = company.refresh_model_list()
>>> print(result['params']['message'])
'Models list has been refreshed successfully!'
"""
self.ensure_one()
if not self.openwebui_enabled:
raise UserError(_("OpenWebUI is not enabled for this company."))
# Create a temporary model to perform synchronization
model = self.env['openwebui.model'].create({
'name': 'refresh_models',
'identifier': 'refresh_models',
'company_id': self.id,
'is_temp': True,
})
Model = self.env['openwebui.model'].with_context(sync_models=True)
success, result = Model._sync_models()
success, message = model.sync_models()
if not success:
raise UserError(message)
raise UserError(_("Failed to refresh models: %s") % result)
return {
'type': 'ir.actions.client',
'tag': 'display_notification',
'params': {
'title': _('Success'),
'message': _('Model list has been successfully updated.'),
'type': 'success',
'message': _('Models list has been refreshed successfully!'),
'sticky': False,
'type': 'success',
}
}

View file

@ -1,5 +1,3 @@
id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
access_openwebui_model_user,access_openwebui_model_user,model_openwebui_model,base.group_user,1,0,0,0
access_openwebui_model_admin,access_openwebui_model_admin,model_openwebui_model,base.group_system,1,1,1,1
access_openwebui_bot_user,access_openwebui_bot_user,model_openwebui_bot,base.group_user,1,0,0,0
access_openwebui_bot_admin,access_openwebui_bot_admin,model_openwebui_bot,base.group_system,1,1,1,1
access_openwebui_model_user,access_openwebui_model_user,model_openwebui_model,openwebui_integration.group_openwebui_user,1,0,0,0
access_openwebui_model_admin,access_openwebui_model_admin,model_openwebui_model,openwebui_integration.group_openwebui_admin,1,1,1,1

1 id name model_id:id group_id:id perm_read perm_write perm_create perm_unlink
2 access_openwebui_model_user access_openwebui_model_user model_openwebui_model base.group_user openwebui_integration.group_openwebui_user 1 0 0 0
3 access_openwebui_model_admin access_openwebui_model_admin model_openwebui_model base.group_system openwebui_integration.group_openwebui_admin 1 1 1 1
access_openwebui_bot_user access_openwebui_bot_user model_openwebui_bot base.group_user 1 0 0 0
access_openwebui_bot_admin access_openwebui_bot_admin model_openwebui_bot base.group_system 1 1 1 1

View file

@ -31,11 +31,6 @@
<field name="groups" eval="[(4, ref('base.group_user'))]"/>
</record>
<record id="openwebui_bot_company_rule" model="ir.rule">
<field name="name">Bots OpenWebUI: règle multi-société</field>
<field name="model_id" ref="model_openwebui_bot"/>
<field name="domain_force">[('company_id', 'in', company_ids)]</field>
<field name="groups" eval="[(4, ref('base.group_user'))]"/>
</record>
</data>
</odoo>

View file

@ -1,27 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<record id="view_openwebui_model_form" model="ir.ui.view">
<field name="name">openwebui.model.form</field>
<field name="model">openwebui.model</field>
<field name="arch" type="xml">
<form create="false" edit="false" delete="false">
<sheet>
<div class="oe_title">
<label for="name" class="oe_edit_only"/>
<h1><field name="name"/></h1>
</div>
<group>
<group>
<field name="identifier"/>
<field name="company_id" groups="base.group_multi_company"/>
<field name="is_active" widget="boolean_toggle"/>
</group>
<group>
<field name="description"/>
</group>
</group>
</sheet>
</form>
</field>
</record>
</odoo>

View file

@ -24,6 +24,9 @@
invisible="not openwebui_enabled"/>
<field name="openwebui_timeout"
invisible="not openwebui_enabled"/>
<field name="openwebui_products_per_request"
invisible="not openwebui_enabled"
help="Number of products to process in a single API request"/>
<button name="test_openwebui_connection"
string="Test Connection"
type="object"
@ -31,6 +34,11 @@
invisible="not openwebui_enabled"/>
</group>
</group>
<group string="Models Configuration" invisible="not openwebui_enabled">
<field name="openwebui_default_model_id"
options="{'no_create': True, 'no_open': True}"
domain="[('is_active', '=', True)]"/>
</group>
<group string="Available Models" invisible="not openwebui_enabled">
<field name="openwebui_models_ids" nolabel="1">
<list create="false" delete="false" edit="true">

View file

@ -0,0 +1,5 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from . import models
from . import controllers

View file

@ -0,0 +1,35 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
{
'name': 'OpenWebUI Integration Chat',
'version': '1.0',
'summary': 'Chat integration with OpenWebUI for Discuss',
'sequence': 11,
'description': """
OpenWebUI Integration Chat
=========================
This module extends OpenWebUI Integration to provide chatbot features in Discuss.
Features:
---------
* Integrate chatbots with Discuss channels
* Control access to bots by channels and users
* Chat-specific bot configurations and behaviors
""",
'category': 'Discuss',
'website': 'https://www.odoo.com/app/discuss',
'depends': [
'mail',
'openwebui_integration'
],
'data': [
'security/security.xml',
'security/ir.model.access.csv',
'views/discuss_channel_views.xml',
'views/openwebui_bot_views.xml',
],
'installable': True,
'application': False,
'auto_install': False,
'license': 'LGPL-3',
}

View file

@ -0,0 +1,6 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from . import discuss_channel
from . import res_users
from . import openwebui_bot

View file

@ -0,0 +1,3 @@
id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
access_openwebui_bot_user,access_openwebui_bot_user,model_openwebui_bot,openwebui_integration.group_openwebui_user,1,0,0,0
access_openwebui_bot_admin,access_openwebui_bot_admin,model_openwebui_bot,openwebui_integration.group_openwebui_admin,1,1,1,1
1 id name model_id:id group_id:id perm_read perm_write perm_create perm_unlink
2 access_openwebui_bot_user access_openwebui_bot_user model_openwebui_bot openwebui_integration.group_openwebui_user 1 0 0 0
3 access_openwebui_bot_admin access_openwebui_bot_admin model_openwebui_bot openwebui_integration.group_openwebui_admin 1 1 1 1

View file

@ -0,0 +1,12 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<data noupdate="0">
<!-- Règles de sécurité par société -->
<record id="openwebui_bot_company_rule" model="ir.rule">
<field name="name">Bots OpenWebUI: règle multi-société</field>
<field name="model_id" ref="model_openwebui_bot"/>
<field name="domain_force">[('company_id', 'in', company_ids)]</field>
<field name="groups" eval="[(4, ref('openwebui_integration.group_openwebui_user'))]"/>
</record>
</data>
</odoo>

View file

@ -0,0 +1,123 @@
# Spécifications du module OpenWebUI Integration Product
## Objectif
Ce module étend les fonctionnalités d'OpenWebUI Integration pour ajouter des capacités d'IA spécifiques à la gestion des produits dans Odoo. Il permet d'automatiser et d'optimiser différents aspects de la gestion des produits grâce à l'intelligence artificielle.
## Architecture du Module
### Structure des Répertoires
```
openwebui_integration_product/
├── controllers/ # Contrôleurs pour les endpoints web spécifiques aux produits
├── models/ # Modèles de données pour les produits et l'historique
├── security/ # Fichiers de sécurité et accès
├── static/ # Ressources statiques (JS, CSS)
└── views/ # Vues XML Odoo pour les produits
```
## Composants Principaux
### 1. Intégration Produit (`product.integration`)
Extension du modèle `product.template` pour ajouter les fonctionnalités d'IA.
#### Fonctionnalités clés:
- Héritage du mixin `openwebui.bot.mixin`
- Gestion des suggestions de catégories
- Historique des suggestions
- Interface utilisateur intégrée
#### Méthodes principales:
- `_get_product_context()`: Prépare le contexte produit pour l'IA
- `suggest_category()`: Déclenche l'analyse IA pour les suggestions
- `apply_suggestion()`: Applique une suggestion validée
- `_process_category_suggestion()`: Traite la réponse de l'IA
### 2. Historique des Suggestions (`category.suggestion.history`)
Gère l'historique des suggestions de catégories.
#### Caractéristiques:
- Traçabilité des suggestions
- Scores de confiance
- Statut des suggestions (appliquée/rejetée)
- Métadonnées de contexte
#### Champs principaux:
- `product_id`: Produit concerné
- `suggested_category_id`: Catégorie suggérée
- `confidence_score`: Score de confiance
- `status`: État de la suggestion
- `applied_date`: Date d'application
- `user_id`: Utilisateur ayant traité la suggestion
### 3. Configuration Spécifique
Extension des paramètres de configuration d'OpenWebUI.
#### Paramètres configurables:
- `product_suggestion_threshold`: Seuil minimum de confiance
- `max_suggestions_per_product`: Nombre maximum de suggestions
- `suggestion_retention_days`: Durée de conservation de l'historique
- `auto_apply_threshold`: Seuil pour application automatique
## Processus de Suggestion de Catégorie
### Flux de Données
1. Collecte des données produit
- Informations de base (nom, description)
- Caractéristiques techniques
- Historique des achats
- Catégories existantes
2. Préparation du Contexte
- Formatage des données
- Ajout du contexte entreprise
- Inclusion des paramètres de configuration
3. Analyse IA
- Envoi à OpenWebUI
- Traitement de la réponse
- Calcul des scores de confiance
4. Validation et Application
- Vérification des seuils
- Présentation à l'utilisateur
- Enregistrement dans l'historique
## Sécurité et Gestion des Erreurs
### Sécurité
- Droits d'accès spécifiques aux produits
- Validation des suggestions
- Protection contre les suggestions malveillantes
- Traçabilité des modifications
### Gestion des Erreurs
- Validation des données produit
- Gestion des timeouts
- Traitement des réponses invalides
- Rollback en cas d'échec
## Intégration et Utilisation
### Installation
1. Installation des dépendances
2. Configuration des paramètres produit
3. Attribution des droits d'accès
4. Test initial des suggestions
### Développement d'Extensions
1. Héritage des modèles existants
2. Ajout de nouveaux critères d'analyse
3. Personnalisation des suggestions
4. Extension des fonctionnalités
## Dépendances
- Module `openwebui_integration`
- Module `product`
- Python 3.x
- Bibliothèques de traitement de texte
## Notes Techniques
- Optimisation des requêtes IA
- Cache des suggestions fréquentes
- Support multi-langue
- Extensible pour d'autres types d'analyses

View file

@ -0,0 +1,2 @@
from . import models
from . import wizard

View file

@ -0,0 +1,25 @@
# -*- coding: utf-8 -*-
{
'name': 'OpenWebUI Integration Product',
'version': '18.0.1.0.1',
'summary': "AI-powered actions integration for Odoo products",
'description': """
OpenWebUI Integration Product Module
====================================
This module extends OpenWebUI Integration to add AI-powered features to product management.
""",
'author': 'Benoît Vézina',
'license': 'LGPL-3',
'website': 'https://www.bemade.org',
'depends': ['product', 'openwebui_integration'],
'data': [
'security/ir.model.access.csv',
'views/product_template_view.xml',
'views/product_template_action.xml',
'wizard/category_suggestion_wizard_view.xml',
],
'installable': True,
'application': False,
'auto_install': False,
}

View file

@ -0,0 +1 @@
from . import pre-migration

View file

@ -0,0 +1,17 @@
def migrate(cr, version):
"""Add explanation column to product_category_suggestion_history table."""
if not version:
return
# Add explanation column if it doesn't exist
cr.execute("""
SELECT column_name
FROM information_schema.columns
WHERE table_name='product_category_suggestion_history'
AND column_name='explanation'
""")
if not cr.fetchone():
cr.execute("""
ALTER TABLE product_category_suggestion_history
ADD COLUMN explanation text
""")

View file

@ -0,0 +1,2 @@
from . import product_template
from . import category_suggestion_history

View file

@ -0,0 +1,56 @@
# -*- coding: utf-8 -*-
from odoo import models, fields
class CategorySuggestionHistory(models.Model):
_name = 'product.category.suggestion.history'
_description = 'Category Suggestion History'
_order = 'create_date desc'
product_id = fields.Many2one(
comodel_name='product.template',
string='Product',
required=True,
ondelete='cascade',
help='Product for which the category was suggested'
)
suggested_category_id = fields.Many2one(
comodel_name='product.category',
string='Suggested Category',
required=True,
help='Category suggested by AI analysis'
)
suggestion_confidence = fields.Float(
string='Confidence Score',
help='Confidence score of the suggestion (0-100)'
)
applied = fields.Boolean(
string='Applied',
help='Indicates if the suggestion was applied to the product'
)
suggestion_date = fields.Datetime(
string='Suggestion Date',
readonly=True,
help='Date and time of the suggestion'
)
suggestion_uid = fields.Many2one(
comodel_name='res.users',
string='Created By',
readonly=True,
help='User who triggered the suggestion'
)
input_data = fields.Text(
string='Analyzed Data',
help='Data used by AI to generate the suggestion'
)
explanation = fields.Text(
string='Explanation',
help='Detailed explanation of category choice'
)

View file

@ -0,0 +1,203 @@
# -*- coding: utf-8 -*-
"""OpenWebUI Product Integration Module
This module extends product.template functionality to integrate
OpenWebUI artificial intelligence into product management.
It enables automatic product category suggestions
based on description and characteristics analysis.
Main features:
- AI-powered category suggestions
- Suggestion history tracking
- Integrated user interface
"""
import json
import logging
from datetime import datetime
from odoo import models, fields, _
from odoo.exceptions import UserError
from odoo.tools import float_round
from odoo.addons.openwebui_integration.models.openwebui_bot_mixin import OpenWebUIBotMixin
_logger = logging.getLogger(__name__)
class ProductTemplate(models.Model, OpenWebUIBotMixin):
_inherit = "product.template"
suggested_category_id = fields.Many2one(
comodel_name='product.category',
string="Suggested Category",
readonly=True,
help="Category suggested by AI based on product information analysis"
)
suggestion_confidence = fields.Float(
string="Confidence",
readonly=True,
help="Confidence score (0-100) indicating how sure the AI is about the suggested category"
)
suggestion_date = fields.Datetime(
string="Suggestion Date",
readonly=True,
help="Date and time when the category was suggested by the AI"
)
suggestion_history_ids = fields.One2many(
comodel_name='product.category.suggestion.history',
inverse_name='product_id',
string="Suggestion History",
help="History of all category suggestions made by AI for this product"
)
def _generate_bot_message(self, records, values, command=None):
"""Generates the message to send to the bot to get category suggestions."""
# Préparer les données des produits
products_data = [{
'odoo_id': record.id, # ID interne Odoo
'name': record.name,
'description': record.description or '',
'description_sale': record.description_sale or '',
'default_code': record.default_code or '',
'current_category': record.categ_id.display_name,
'sellers': [
{
'name': seller.partner_id.display_name,
'product_code': seller.product_code or '',
'product_name': seller.product_name or ''
} for seller in record.seller_ids
]
} for record in records]
# Récupérer toutes les catégories disponibles
Category = self.env['product.category']
categories = Category.search([('parent_id', '!=', False)], order='complete_name')
available_categories = [{
'id': cat.id,
'name': cat.name,
'complete_name': cat.complete_name or cat.name,
'level': len(cat.parent_path.split('/')) - 1 if cat.parent_path else 0
} for cat in categories]
# Construire le message pour l'IA
message = {
'task': 'product_categorization',
'products': products_data,
'available_categories': available_categories,
'instructions': """For each product in the products list, analyze the product information and suggest the most appropriate product category from the available list.
Consider each product's name, description, and supplier information to make the best match.
Return a list of JSON objects, one for each product, with:
- 'odoo_id' (integer): The internal Odoo ID of the product
- 'category_id' (integer): The ID of the most appropriate category
- 'confidence' (float between 0 and 100): How confident you are about this suggestion
- 'explanation' (string): A detailed explanation of why this category was chosen, including analysis of the product name, description, and other relevant information.""",
'format': 'json'
}
return json.dumps(message)
def _process_bot_response(self, values, response):
"""Process the bot response to extract the suggested category."""
try:
results = json.loads(response)
if not isinstance(results, list):
raise ValueError("La réponse n'est pas une liste JSON valide")
for result in results:
category_id = result.get('category_id')
confidence = result.get('confidence', 0.0)
product_id = result.get('odoo_id')
if not category_id:
raise ValueError("No category ID in response")
if not product_id:
raise ValueError("No product ID in response")
# Vérifier que la catégorie existe
category = self.env['product.category'].browse(category_id).exists()
if not category:
raise ValueError(f"Category {category_id} not found")
# Trouver le produit concerné
product = self.filtered(lambda p: p.id == product_id)
if not product:
raise ValueError(f"Product {product_id} not found in selection")
# Mettre à jour les valeurs pour ce produit
product.write({
'suggested_category_id': category_id,
'suggestion_confidence': confidence,
'suggestion_date': fields.Datetime.now(),
})
# Créer l'historique
self.env['product.category.suggestion.history'].create({
'product_id': product_id,
'suggested_category_id': category_id,
'suggestion_confidence': confidence,
'input_data': json.dumps(result),
'explanation': result.get('explanation', ''),
'applied': False
})
except Exception as e:
raise UserError(_("Erreur lors du traitement de la réponse de l'IA: %s") % str(e))
return values
def action_suggest_category(self):
"""Request category suggestions from AI."""
if len(self) > 800:
raise UserError(_("For performance reasons, you cannot analyze more than 800 products at once."))
# Get company settings
company = self.env.company
if not company.openwebui_enabled:
raise UserError(_("OpenWebUI is not enabled for your company. Please enable it in company settings."))
model = company.openwebui_default_model_id
if not model:
raise UserError(_("No default OpenWebUI model configured. Please configure it in company settings."))
# Process products in batches
batch_size = company.openwebui_products_per_request
successful_products = self.env['product.template']
for i in range(0, len(self), batch_size):
batch = self[i:i + batch_size]
# Créer une nouvelle transaction pour ce batch
with self.env.cr.savepoint():
try:
values = {'bot': model}
self._apply_logic(batch, values)
# Si on arrive ici, le batch a réussi
successful_products |= batch
_logger.info('Successfully processed batch of %d products: %s',
len(batch), batch.mapped('default_code'))
except Exception as e:
_logger.error('Batch processing failed for products %s: %s',
batch.mapped('default_code'), str(e))
# Le savepoint sera rollback automatiquement
continue
# Si aucun produit n'a été traité avec succès
if not successful_products:
raise UserError(_('No suggestions could be generated by AI.'))
# Ouvrir l'assistant si des suggestions ont été générées
if any(product.suggested_category_id for product in successful_products):
wizard = self.env['product.category.suggestion.wizard'].create({})
return {
'name': _('Category Suggestions'),
'type': 'ir.actions.act_window',
'res_model': 'product.category.suggestion.wizard',
'res_id': wizard.id,
'view_mode': 'form',
'target': 'new',
'context': self.env.context,
}
else:
raise UserError(_('No valid suggestions could be generated by AI.'))

View file

@ -0,0 +1,4 @@
id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
access_product_category_suggestion_history_user,product.category.suggestion.history user,model_product_category_suggestion_history,base.group_user,1,1,1,1
access_product_category_suggestion_wizard_user,product.category.suggestion.wizard user,model_product_category_suggestion_wizard,base.group_user,1,1,1,1
access_product_category_suggestion_wizard_line_user,product.category.suggestion.wizard.line user,model_product_category_suggestion_wizard_line,base.group_user,1,1,1,1
1 id name model_id:id group_id:id perm_read perm_write perm_create perm_unlink
2 access_product_category_suggestion_history_user product.category.suggestion.history user model_product_category_suggestion_history base.group_user 1 1 1 1
3 access_product_category_suggestion_wizard_user product.category.suggestion.wizard user model_product_category_suggestion_wizard base.group_user 1 1 1 1
4 access_product_category_suggestion_wizard_line_user product.category.suggestion.wizard.line user model_product_category_suggestion_wizard_line base.group_user 1 1 1 1

View file

@ -0,0 +1,14 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<!-- Action serveur pour suggérer des catégories -->
<record id="action_suggest_category_multi" model="ir.actions.server">
<field name="name">AI Suggested Category</field>
<field name="model_id" ref="product.model_product_template"/>
<field name="binding_model_id" ref="product.model_product_template"/>
<field name="binding_view_types">list</field>
<field name="state">code</field>
<field name="code">
action = model.browse(env.context.get('active_ids', [])).action_suggest_category()
</field>
</record>
</odoo>

View file

@ -0,0 +1,62 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<!-- Vue tree pour l'historique des suggestions -->
<record id="view_category_suggestion_history_tree" model="ir.ui.view">
<field name="name">product.category.suggestion.history.tree</field>
<field name="model">product.category.suggestion.history</field>
<field name="type">list</field>
<field name="arch" type="xml">
<list string="Historique des suggestions" editable="bottom">
<field name="create_date"/>
<field name="suggested_category_id"/>
<field name="suggestion_confidence" widget="percentage"/>
<field name="applied"/>
<field name="create_uid"/>
</list>
</field>
</record>
<!-- Vue héritée du formulaire de produit -->
<record id="product_template_form_view_inherit_openwebui" model="ir.ui.view">
<field name="name">product.template.form.inherit.openwebui</field>
<field name="model">product.template</field>
<field name="inherit_id" ref="product.product_template_form_view"/>
<field name="arch" type="xml">
<xpath expr="//div[@name='button_box']" position="inside">
<button name="action_suggest_category"
type="object"
class="oe_stat_button"
icon="fa-magic"
help="Suggérer une catégorie basée sur l'IA">
<div class="o_field_widget o_stat_info">
<span class="o_stat_text">Suggérer</span>
<span class="o_stat_text">Catégorie</span>
</div>
</button>
</xpath>
<xpath expr="//notebook" position="inside">
<page string="Suggestions IA" name="ai_suggestions">
<group>
<field name="suggested_category_id"/>
<field name="suggestion_confidence" widget="percentage"/>
<field name="suggestion_date"/>
</group>
<field name="suggestion_history_ids" readonly="1"/>
</page>
</xpath>
</field>
</record>
<!-- Vue héritée de la liste des produits -->
<record id="product_template_tree_view_inherit_openwebui" model="ir.ui.view">
<field name="name">product.template.tree.inherit.openwebui</field>
<field name="model">product.template</field>
<field name="inherit_id" ref="product.product_template_tree_view"/>
<field name="arch" type="xml">
<xpath expr="//list" position="attributes">
<attribute name="action">action_suggest_category_multi</attribute>
</xpath>
</field>
</record>
</odoo>

View file

@ -0,0 +1 @@
from . import category_suggestion_wizard

View file

@ -0,0 +1,74 @@
from odoo import models, fields, api, _
from odoo.exceptions import UserError
import json
class CategorySuggestionWizard(models.TransientModel):
_name = 'product.category.suggestion.wizard'
_description = 'Assistant de suggestions de catégories'
product_suggestion_ids = fields.One2many('product.category.suggestion.wizard.line', 'wizard_id',
string='Suggestions de produits')
@api.model
def default_get(self, fields):
res = super().default_get(fields)
context = self.env.context
if context.get('active_model') == 'product.template' and context.get('active_ids'):
products = self.env['product.template'].browse(context['active_ids'])
suggestion_lines = []
for product in products:
if product.suggested_category_id:
suggestion_lines.append((0, 0, {
'product_id': product.id,
'current_category_id': product.categ_id.id,
'suggested_category_id': product.suggested_category_id.id,
'confidence': product.suggestion_confidence,
'apply_suggestion': False
}))
res['product_suggestion_ids'] = suggestion_lines
return res
def action_apply_selected_suggestions(self):
"""Applique les suggestions sélectionnées aux produits."""
selected_lines = self.product_suggestion_ids.filtered(lambda l: l.apply_suggestion)
if not selected_lines:
raise UserError(_('Veuillez sélectionner au moins une suggestion à appliquer.'))
for line in selected_lines:
# Mise à jour du produit
line.product_id.write({
'categ_id': line.suggested_category_id.id
})
# Mise à jour de l'historique
history = self.env['product.category.suggestion.history'].search([
('product_id', '=', line.product_id.id),
('suggested_category_id', '=', line.suggested_category_id.id)
], limit=1)
if history:
history.write({'applied': True})
# Message de confirmation
return {
'type': 'ir.actions.client',
'tag': 'display_notification',
'params': {
'title': _('Succès'),
'message': _('%d catégories ont été mises à jour.') % len(selected_lines),
'type': 'success',
'sticky': False,
}
}
class CategorySuggestionWizardLine(models.TransientModel):
_name = 'product.category.suggestion.wizard.line'
_description = 'Ligne de suggestion de catégorie'
_order = 'confidence desc, product_id'
wizard_id = fields.Many2one('product.category.suggestion.wizard', string='Assistant')
product_id = fields.Many2one('product.template', string='Produit', required=True, readonly=True)
current_category_id = fields.Many2one('product.category', string='Catégorie actuelle', readonly=True)
suggested_category_id = fields.Many2one('product.category', string='Catégorie suggérée', readonly=True)
confidence = fields.Float(string='Confiance (%)', readonly=True)
apply_suggestion = fields.Boolean(string='Appliquer', default=False)

View file

@ -0,0 +1,36 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<record id="view_category_suggestion_wizard_form" model="ir.ui.view">
<field name="name">product.category.suggestion.wizard.form</field>
<field name="model">product.category.suggestion.wizard</field>
<field name="arch" type="xml">
<form string="Suggestions de Catégories">
<sheet>
<div class="alert alert-info" role="alert">
Sélectionnez les suggestions à appliquer en cochant la case dans la colonne 'Appliquer'.
</div>
<field name="product_suggestion_ids">
<list editable="bottom" create="0" delete="0">
<field name="product_id" readonly="1"/>
<field name="current_category_id" readonly="1"/>
<field name="suggested_category_id" readonly="1"/>
<field name="confidence" widget="percentage" readonly="1"/>
<field name="apply_suggestion" widget="boolean_toggle"/>
</list>
</field>
</sheet>
<footer>
<button string="Appliquer les suggestions sélectionnées"
name="action_apply_selected_suggestions"
type="object"
class="btn-primary"
data-hotkey="q"/>
<button string="Fermer"
class="btn-secondary"
special="cancel"
data-hotkey="z"/>
</footer>
</form>
</field>
</record>
</odoo>