From f8be847777ea5a4eb1904cbf9eb222b11bd60800 Mon Sep 17 00:00:00 2001 From: Luis Ernesto Portillo Zaldivar Date: Thu, 17 Jul 2025 02:06:04 -0600 Subject: [PATCH] =?UTF-8?q?feat(#67):=20Implementar=20autocompletado=20int?= =?UTF-8?q?eligente=20para=20campos=20de=20selecci=C3=B3n?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 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 --- .../__pycache__/stock_lot.cpython-312.pyc | Bin 24845 -> 24845 bytes lims_management/models/lims_result.py | 138 +++++++++++++++++- lims_management/models/lims_test.py | 31 +++- lims_management/views/lims_test_views.xml | 2 +- 4 files changed, 167 insertions(+), 4 deletions(-) diff --git a/lims_management/models/__pycache__/stock_lot.cpython-312.pyc b/lims_management/models/__pycache__/stock_lot.cpython-312.pyc index 49bcfe06999c89c72a2bcbd441bf3e5669768d70..f3a7a032698e4c5a15327a4d827a28861da937fe 100644 GIT binary patch delta 21 bcmeA@#Mpa?k?S-sFBbz4IIP;p#heHLM>_?h delta 21 bcmeA@#Mpa?k?S-sFBbz4a3yWzVon4AM2`g} diff --git a/lims_management/models/lims_result.py b/lims_management/models/lims_result.py index 19d840d..dfaca77 100644 --- a/lims_management/models/lims_result.py +++ b/lims_management/models/lims_result.py @@ -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 \ No newline at end of file + 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) \ No newline at end of file diff --git a/lims_management/models/lims_test.py b/lims_management/models/lims_test.py index cc8dc39..32b503d 100644 --- a/lims_management/models/lims_test.py +++ b/lims_management/models/lims_test.py @@ -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 diff --git a/lims_management/views/lims_test_views.xml b/lims_management/views/lims_test_views.xml index f038c09..1ce6b1c 100644 --- a/lims_management/views/lims_test_views.xml +++ b/lims_management/views/lims_test_views.xml @@ -90,7 +90,7 @@ class="oe_edit_only"/>