feat(#67): Implementar autocompletado inteligente para campos de selección

- Agregar método _onchange_value_selection() que autocompleta al escribir
- Agregar método _validate_and_autocomplete_selection() para validación
- Override create() y write() para autocompletar antes de guardar
- Búsqueda flexible: acepta iniciales, mayúsculas/minúsculas, coincidencias parciales
- Generar instrucciones automáticas en campo notes al crear resultados
- Las instrucciones muestran opciones disponibles y ejemplos de uso
This commit is contained in:
Luis Ernesto Portillo Zaldivar 2025-07-17 02:06:04 -06:00
parent db3462184b
commit f8be847777
4 changed files with 167 additions and 4 deletions

View File

@ -93,7 +93,15 @@ class LimsResult(models.Model):
)
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(
@ -275,6 +283,17 @@ class LimsResult(models.Model):
raise ValidationError(
_('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':
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:
@ -301,4 +320,119 @@ class LimsResult(models.Model):
# Si es selección, obtener las opciones
if self.parameter_value_type == 'selection' and self.parameter_id.selection_values:
# 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)

View File

@ -183,11 +183,40 @@ class LimsTest(models.Model):
# Crear una línea de resultado por cada parámetro
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 = {
'test_id': test.id,
'parameter_id': param_config.parameter_id.id,
'sequence': param_config.sequence,
'notes': param_config.instructions or ''
'notes': notes
}
# Inicializar valores según el tipo

View File

@ -90,7 +90,7 @@
class="oe_edit_only"/>
<field name="value_selection"
invisible="parameter_value_type != 'selection'"
widget="selection"
placeholder="Ingrese valor o iniciales"
class="oe_edit_only"/>
<field name="value_boolean"
invisible="parameter_value_type != 'boolean'"