Merge pull request 'feat(#67): Implementar autocompletado inteligente para campos de selección' (#72) from feature/67-smart-selection-autocomplete into dev
Reviewed-on: #72
This commit is contained in:
commit
2ca64186b0
BIN
documents/logs/Screenshot_8.png
Normal file
BIN
documents/logs/Screenshot_8.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 23 KiB |
Binary file not shown.
|
@ -93,7 +93,15 @@ class LimsResult(models.Model):
|
||||||
)
|
)
|
||||||
|
|
||||||
value_selection = fields.Char(
|
value_selection = fields.Char(
|
||||||
string='Valor de Selección'
|
string='Valor de Selección',
|
||||||
|
help='Ingrese el valor o las primeras letras. Ej: P para Positivo, N para Negativo'
|
||||||
|
)
|
||||||
|
|
||||||
|
# Campo para mostrar las opciones disponibles
|
||||||
|
selection_options_display = fields.Char(
|
||||||
|
string='Opciones disponibles',
|
||||||
|
compute='_compute_selection_options_display',
|
||||||
|
help='Opciones válidas para este parámetro'
|
||||||
)
|
)
|
||||||
|
|
||||||
value_boolean = fields.Boolean(
|
value_boolean = fields.Boolean(
|
||||||
|
@ -275,6 +283,17 @@ class LimsResult(models.Model):
|
||||||
raise ValidationError(
|
raise ValidationError(
|
||||||
_('Para parámetros de selección solo se debe elegir una opción.')
|
_('Para parámetros de selección solo se debe elegir una opción.')
|
||||||
)
|
)
|
||||||
|
# Validar que el valor seleccionado sea válido
|
||||||
|
if has_value and record.parameter_id:
|
||||||
|
valid_options = record.parameter_id.get_selection_list()
|
||||||
|
if valid_options and record.value_selection not in valid_options:
|
||||||
|
# Intentar autocompletar antes de rechazar
|
||||||
|
autocompleted = record._validate_and_autocomplete_selection(record.value_selection)
|
||||||
|
if autocompleted not in valid_options:
|
||||||
|
raise ValidationError(
|
||||||
|
_('El valor "%s" no es una opción válida. Opciones disponibles: %s') %
|
||||||
|
(record.value_selection, ', '.join(valid_options))
|
||||||
|
)
|
||||||
elif value_type == 'boolean':
|
elif value_type == 'boolean':
|
||||||
has_value = True # Boolean siempre tiene valor (True o False)
|
has_value = True # Boolean siempre tiene valor (True o False)
|
||||||
if (record.value_numeric not in [False, 0.0]) or record.value_text or record.value_selection:
|
if (record.value_numeric not in [False, 0.0]) or record.value_text or record.value_selection:
|
||||||
|
@ -301,4 +320,119 @@ class LimsResult(models.Model):
|
||||||
# Si es selección, obtener las opciones
|
# Si es selección, obtener las opciones
|
||||||
if self.parameter_value_type == 'selection' and self.parameter_id.selection_values:
|
if self.parameter_value_type == 'selection' and self.parameter_id.selection_values:
|
||||||
# Esto se usará en las vistas para mostrar las opciones dinámicamente
|
# Esto se usará en las vistas para mostrar las opciones dinámicamente
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
@api.depends('parameter_id', 'parameter_id.selection_values')
|
||||||
|
def _compute_selection_options_display(self):
|
||||||
|
"""Calcula las opciones disponibles para mostrar al usuario."""
|
||||||
|
for record in self:
|
||||||
|
if record.parameter_id and record.parameter_value_type == 'selection':
|
||||||
|
options = record.parameter_id.get_selection_list()
|
||||||
|
if options:
|
||||||
|
record.selection_options_display = ' | '.join(options)
|
||||||
|
else:
|
||||||
|
record.selection_options_display = 'Sin opciones definidas'
|
||||||
|
else:
|
||||||
|
record.selection_options_display = False
|
||||||
|
|
||||||
|
@api.onchange('value_selection')
|
||||||
|
def _onchange_value_selection(self):
|
||||||
|
"""Autocompleta el valor de selección basado en coincidencia parcial."""
|
||||||
|
if self.value_selection and self.parameter_id and self.parameter_value_type == 'selection':
|
||||||
|
# Obtener las opciones disponibles
|
||||||
|
options = self.parameter_id.get_selection_list()
|
||||||
|
if options:
|
||||||
|
# Convertir el valor ingresado a mayúsculas para comparación
|
||||||
|
input_upper = self.value_selection.upper().strip()
|
||||||
|
|
||||||
|
# Buscar coincidencias
|
||||||
|
matches = []
|
||||||
|
for option in options:
|
||||||
|
option_upper = option.upper()
|
||||||
|
if option_upper.startswith(input_upper):
|
||||||
|
matches.append(option)
|
||||||
|
|
||||||
|
# Si hay exactamente una coincidencia, autocompletar
|
||||||
|
if len(matches) == 1:
|
||||||
|
self.value_selection = matches[0]
|
||||||
|
elif len(matches) == 0:
|
||||||
|
# Si no hay coincidencias directas, buscar coincidencias parciales
|
||||||
|
for option in options:
|
||||||
|
if input_upper in option.upper():
|
||||||
|
matches.append(option)
|
||||||
|
|
||||||
|
# Si hay una sola coincidencia parcial, autocompletar
|
||||||
|
if len(matches) == 1:
|
||||||
|
self.value_selection = matches[0]
|
||||||
|
|
||||||
|
def _validate_and_autocomplete_selection(self, value):
|
||||||
|
"""Valida y autocompleta el valor de selección.
|
||||||
|
|
||||||
|
Esta función es llamada antes de guardar para asegurar que el valor
|
||||||
|
sea válido y esté completo.
|
||||||
|
"""
|
||||||
|
if not value or not self.parameter_id or self.parameter_value_type != 'selection':
|
||||||
|
return value
|
||||||
|
|
||||||
|
options = self.parameter_id.get_selection_list()
|
||||||
|
if not options:
|
||||||
|
return value
|
||||||
|
|
||||||
|
# Convertir a mayúsculas para comparación
|
||||||
|
value_upper = value.upper().strip()
|
||||||
|
|
||||||
|
# Buscar coincidencias exactas primero
|
||||||
|
for option in options:
|
||||||
|
if option.upper() == value_upper:
|
||||||
|
return option
|
||||||
|
|
||||||
|
# Buscar coincidencias que empiecen con el valor
|
||||||
|
matches = []
|
||||||
|
for option in options:
|
||||||
|
if option.upper().startswith(value_upper):
|
||||||
|
matches.append(option)
|
||||||
|
|
||||||
|
if len(matches) == 1:
|
||||||
|
return matches[0]
|
||||||
|
elif len(matches) > 1:
|
||||||
|
# Si hay múltiples coincidencias, intentar ser más específico
|
||||||
|
# Preferir la coincidencia más corta
|
||||||
|
shortest = min(matches, key=len)
|
||||||
|
return shortest
|
||||||
|
|
||||||
|
# Si no hay coincidencias por inicio, buscar contenido
|
||||||
|
for option in options:
|
||||||
|
if value_upper in option.upper():
|
||||||
|
matches.append(option)
|
||||||
|
|
||||||
|
if len(matches) == 1:
|
||||||
|
return matches[0]
|
||||||
|
elif len(matches) > 1:
|
||||||
|
# Retornar la primera coincidencia
|
||||||
|
return matches[0]
|
||||||
|
|
||||||
|
# Si no hay ninguna coincidencia, retornar el valor original
|
||||||
|
# La validación en @api.constrains se encargará de rechazarlo
|
||||||
|
return value
|
||||||
|
|
||||||
|
@api.model
|
||||||
|
def create(self, vals):
|
||||||
|
"""Override create para autocompletar valores de selección."""
|
||||||
|
if 'value_selection' in vals and vals.get('value_selection'):
|
||||||
|
# Necesitamos el parameter_id para validar
|
||||||
|
if 'parameter_id' in vals:
|
||||||
|
parameter = self.env['lims.analysis.parameter'].browse(vals['parameter_id'])
|
||||||
|
if parameter.value_type == 'selection':
|
||||||
|
# Crear un registro temporal para usar el método
|
||||||
|
temp_record = self.new({'parameter_id': parameter.id, 'parameter_value_type': 'selection'})
|
||||||
|
vals['value_selection'] = temp_record._validate_and_autocomplete_selection(vals['value_selection'])
|
||||||
|
return super(LimsResult, self).create(vals)
|
||||||
|
|
||||||
|
def write(self, vals):
|
||||||
|
"""Override write para autocompletar valores de selección."""
|
||||||
|
if 'value_selection' in vals and vals.get('value_selection'):
|
||||||
|
for record in self:
|
||||||
|
if record.parameter_value_type == 'selection':
|
||||||
|
vals['value_selection'] = record._validate_and_autocomplete_selection(vals['value_selection'])
|
||||||
|
break # Solo necesitamos procesar una vez
|
||||||
|
return super(LimsResult, self).write(vals)
|
|
@ -183,11 +183,40 @@ class LimsTest(models.Model):
|
||||||
|
|
||||||
# Crear una línea de resultado por cada parámetro
|
# Crear una línea de resultado por cada parámetro
|
||||||
for param_config in template_parameters:
|
for param_config in template_parameters:
|
||||||
|
# Preparar las notas/instrucciones
|
||||||
|
notes = param_config.instructions or ''
|
||||||
|
|
||||||
|
# Si es un parámetro de tipo selection, agregar instrucciones de autocompletado
|
||||||
|
if param_config.parameter_value_type == 'selection':
|
||||||
|
selection_values = param_config.parameter_id.selection_values
|
||||||
|
if selection_values:
|
||||||
|
options = [v.strip() for v in selection_values.split(',')]
|
||||||
|
if options:
|
||||||
|
# Generar instrucciones automáticas
|
||||||
|
auto_instructions = "Opciones: " + ", ".join(options) + ". "
|
||||||
|
auto_instructions += "Puede escribir las iniciales o parte del texto. "
|
||||||
|
|
||||||
|
# Agregar ejemplos específicos
|
||||||
|
examples = []
|
||||||
|
for opt in options[:3]: # Mostrar ejemplos para las primeras 3 opciones
|
||||||
|
if opt:
|
||||||
|
initial = opt[0].upper()
|
||||||
|
examples.append(f"{initial}={opt}")
|
||||||
|
|
||||||
|
if examples:
|
||||||
|
auto_instructions += "Ej: " + ", ".join(examples)
|
||||||
|
|
||||||
|
# Combinar con instrucciones existentes
|
||||||
|
if notes:
|
||||||
|
notes = auto_instructions + "\n" + notes
|
||||||
|
else:
|
||||||
|
notes = auto_instructions
|
||||||
|
|
||||||
result_vals = {
|
result_vals = {
|
||||||
'test_id': test.id,
|
'test_id': test.id,
|
||||||
'parameter_id': param_config.parameter_id.id,
|
'parameter_id': param_config.parameter_id.id,
|
||||||
'sequence': param_config.sequence,
|
'sequence': param_config.sequence,
|
||||||
'notes': param_config.instructions or ''
|
'notes': notes
|
||||||
}
|
}
|
||||||
|
|
||||||
# Inicializar valores según el tipo
|
# Inicializar valores según el tipo
|
||||||
|
@ -255,7 +284,7 @@ class LimsTest(models.Model):
|
||||||
|
|
||||||
# Verificar que todos los resultados tengan valores ingresados
|
# Verificar que todos los resultados tengan valores ingresados
|
||||||
empty_results = self.result_ids.filtered(
|
empty_results = self.result_ids.filtered(
|
||||||
lambda r: not r.value_text and not r.value_numeric and not r.value_boolean and r.parameter_id.value_type != 'boolean'
|
lambda r: not r.value_text and not r.value_numeric and not r.value_selection and not r.value_boolean and r.parameter_id.value_type != 'boolean'
|
||||||
)
|
)
|
||||||
if empty_results:
|
if empty_results:
|
||||||
params = ', '.join(empty_results.mapped('parameter_id.name'))
|
params = ', '.join(empty_results.mapped('parameter_id.name'))
|
||||||
|
|
|
@ -90,7 +90,7 @@
|
||||||
class="oe_edit_only"/>
|
class="oe_edit_only"/>
|
||||||
<field name="value_selection"
|
<field name="value_selection"
|
||||||
invisible="parameter_value_type != 'selection'"
|
invisible="parameter_value_type != 'selection'"
|
||||||
widget="selection"
|
placeholder="Ingrese valor o iniciales"
|
||||||
class="oe_edit_only"/>
|
class="oe_edit_only"/>
|
||||||
<field name="value_boolean"
|
<field name="value_boolean"
|
||||||
invisible="parameter_value_type != 'boolean'"
|
invisible="parameter_value_type != 'boolean'"
|
||||||
|
|
68
test/debug_selection_autocomplete.py
Normal file
68
test/debug_selection_autocomplete.py
Normal file
|
@ -0,0 +1,68 @@
|
||||||
|
#!/usr/bin/env python3
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
"""
|
||||||
|
Script para debuggear el autocompletado de selection
|
||||||
|
"""
|
||||||
|
|
||||||
|
import odoo
|
||||||
|
import logging
|
||||||
|
|
||||||
|
_logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
def debug_selection_autocomplete(env):
|
||||||
|
"""Debug del autocompletado"""
|
||||||
|
|
||||||
|
print("=" * 80)
|
||||||
|
print("DEBUG DE AUTOCOMPLETADO DE SELECTION")
|
||||||
|
print("=" * 80)
|
||||||
|
|
||||||
|
# Buscar un resultado con tipo selection
|
||||||
|
result = env['lims.result'].search([
|
||||||
|
('parameter_value_type', '=', 'selection'),
|
||||||
|
('test_id.state', '=', 'in_process')
|
||||||
|
], limit=1)
|
||||||
|
|
||||||
|
if result:
|
||||||
|
print(f"\nResultado encontrado:")
|
||||||
|
print(f" - ID: {result.id}")
|
||||||
|
print(f" - Parámetro: {result.parameter_id.name}")
|
||||||
|
print(f" - Valor actual: '{result.value_selection}'")
|
||||||
|
print(f" - Valores posibles: {result.parameter_id.selection_values}")
|
||||||
|
|
||||||
|
# Probar el autocompletado
|
||||||
|
test_values = ['Negative', 'negative', 'NEG', 'neg', 'N', 'n', 'Positivo', 'P']
|
||||||
|
|
||||||
|
print("\nProbando autocompletado:")
|
||||||
|
for test_val in test_values:
|
||||||
|
autocompleted = result._validate_and_autocomplete_selection(test_val)
|
||||||
|
print(f" '{test_val}' -> '{autocompleted}'")
|
||||||
|
else:
|
||||||
|
print("No se encontraron resultados de tipo selection")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
# Configuración
|
||||||
|
db_name = 'lims_demo'
|
||||||
|
|
||||||
|
# Conectar a Odoo
|
||||||
|
odoo.tools.config.parse_config(['--database', db_name])
|
||||||
|
|
||||||
|
# Obtener el registro de la base de datos
|
||||||
|
registry = odoo.registry(db_name)
|
||||||
|
|
||||||
|
# Crear cursor y environment
|
||||||
|
with registry.cursor() as cr:
|
||||||
|
env = odoo.api.Environment(cr, odoo.SUPERUSER_ID, {})
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Debug
|
||||||
|
debug_selection_autocomplete(env)
|
||||||
|
|
||||||
|
# No guardar cambios
|
||||||
|
cr.rollback()
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
cr.rollback()
|
||||||
|
print(f"\n❌ Error: {str(e)}")
|
||||||
|
_logger.error(f"Error: {str(e)}", exc_info=True)
|
Loading…
Reference in New Issue
Block a user