fix(#67): Fix selection widget showing 'Sin Opciones' by using proper Selection field

- Add new Selection field 'value_selection_field' with dynamic options
- Update views to use the new field instead of char field with selection widget
- Add migration script to copy existing data from old field to new field
- Update field synchronization and validation logic
- Increment module version to trigger migration

The issue was that Odoo doesn't support using widget='selection' on Char fields.
The solution implements a proper Selection field with dynamic options based on
the parameter configuration.
This commit is contained in:
Luis Ernesto Portillo Zaldivar 2025-07-16 19:43:43 -06:00
parent 875a90a6aa
commit 22082965d0
6 changed files with 184 additions and 12 deletions

View File

@ -15,7 +15,7 @@
'author': "Gemini",
'website': "https://gitea.grupoconsiti.com/luis_portillo/clinical_laboratory",
'category': 'Industries',
'version': '18.0.1.0.0',
'version': '18.0.1.0.1',
'depends': ['base', 'product', 'sale', 'base_setup'],
'assets': {
'web.assets_backend': [

View File

@ -0,0 +1,26 @@
# -*- coding: utf-8 -*-
from odoo import api, SUPERUSER_ID
def migrate(cr, version):
"""Migrate existing selection values from char field to selection field."""
env = api.Environment(cr, SUPERUSER_ID, {})
# Find all results with selection parameters that have values
results = env['lims.result'].search([
('parameter_value_type', '=', 'selection'),
('value_selection', '!=', False),
('value_selection', '!=', '')
])
if results:
cr.execute("""
UPDATE lims_result
SET value_selection_field = value_selection
WHERE id IN %s
AND parameter_value_type = 'selection'
AND value_selection IS NOT NULL
AND value_selection != ''
""", (tuple(results.ids),))
# Log migration
print(f"Migrated {len(results)} selection values from value_selection to value_selection_field")

View File

@ -97,6 +97,12 @@ class LimsResult(models.Model):
string='Valor de Selección'
)
# Add Selection field with dynamic options
value_selection_field = fields.Selection(
selection='_get_selection_options',
string='Valor de Selección'
)
# Campo para mostrar las opciones disponibles
selection_options_display = fields.Char(
string='Opciones disponibles',
@ -167,7 +173,7 @@ class LimsResult(models.Model):
else:
record.display_name = record.parameter_name or _('Nuevo')
@api.depends('value_numeric', 'value_text', 'value_selection', 'value_boolean', 'parameter_value_type')
@api.depends('value_numeric', 'value_text', 'value_selection', 'value_selection_field', 'value_boolean', 'parameter_value_type')
def _compute_value_display(self):
"""Calcula el valor a mostrar según el tipo de dato."""
for record in self:
@ -179,7 +185,8 @@ class LimsResult(models.Model):
elif record.parameter_value_type == 'text':
record.value_display = record.value_text or ''
elif record.parameter_value_type == 'selection':
record.value_display = record.value_selection or ''
# Use the new selection field value
record.value_display = record.value_selection_field or record.value_selection or ''
elif record.parameter_value_type == 'boolean':
record.value_display = '' if record.value_boolean else 'No'
else:
@ -278,18 +285,20 @@ class LimsResult(models.Model):
_('Para parámetros de texto solo se debe ingresar el valor de texto.')
)
elif value_type == 'selection':
has_value = bool(record.value_selection)
# Check both fields for backward compatibility
has_value = bool(record.value_selection_field or record.value_selection)
if (record.value_numeric not in [False, 0.0]) or record.value_text or record.value_boolean:
raise ValidationError(
_('Para parámetros de selección solo se debe elegir una opción.')
)
# Validar que el valor seleccionado sea válido
actual_value = record.value_selection_field or record.value_selection
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:
if valid_options and actual_value 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))
(actual_value, ', '.join(valid_options))
)
elif value_type == 'boolean':
has_value = True # Boolean siempre tiene valor (True o False)
@ -312,6 +321,7 @@ class LimsResult(models.Model):
self.value_numeric = False
self.value_text = False
self.value_selection = False
self.value_selection_field = False
self.value_boolean = False
# Si es selección, obtener las opciones
@ -319,6 +329,12 @@ class LimsResult(models.Model):
# Esto se usará en las vistas para mostrar las opciones dinámicamente
pass
@api.onchange('value_selection_field')
def _onchange_value_selection_field(self):
"""Sincroniza el campo de selección con el campo char."""
if self.parameter_value_type == 'selection':
self.value_selection = self.value_selection_field
def _get_selection_options(self):
"""Retorna las opciones de selección basadas en el parámetro."""
options = []

View File

@ -65,9 +65,8 @@
decoration-warning="is_out_of_range and not is_critical"/>
<field name="value_text"
invisible="parameter_value_type != 'text'"/>
<field name="value_selection"
invisible="parameter_value_type != 'selection'"
widget="selection"/>
<field name="value_selection_field"
invisible="parameter_value_type != 'selection'"/>
<field name="value_boolean"
invisible="parameter_value_type != 'boolean'"
widget="boolean_toggle"/>

View File

@ -34,9 +34,8 @@
decoration-warning="is_critical"/>
<field name="value_text"
invisible="parameter_value_type != 'text'"/>
<field name="value_selection"
invisible="parameter_value_type != 'selection'"
widget="selection"/>
<field name="value_selection_field"
invisible="parameter_value_type != 'selection'"/>
<field name="value_boolean"
invisible="parameter_value_type != 'boolean'"
widget="boolean_toggle"/>

View File

@ -0,0 +1,132 @@
import odoo
import json
def diagnose_selection_issue(cr):
"""Diagnose why selection widget shows 'Sin Opciones'"""
env = odoo.api.Environment(cr, odoo.SUPERUSER_ID, {})
print("="*80)
print("DIAGNOSING SELECTION WIDGET ISSUE")
print("="*80)
# 1. Check the specific test and parameter
test_id = 33
result_id = 43
# Get the test
test = env['lims.test'].browse(test_id)
if not test:
print(f"\nERROR: Test with ID {test_id} not found!")
return
print(f"\nTest: {test.name} (ID: {test.id})")
print(f"Category: {test.category_id.name}")
# Check parameters with selection type
selection_params = test.parameter_ids.filtered(lambda p: p.value_type == 'selection')
print(f"\nParameters with selection type: {len(selection_params)}")
for param in selection_params:
print(f"\n--- Parameter: {param.name} (ID: {param.id}) ---")
print(f"Value Type: {param.value_type}")
print(f"Selection Values (raw field): '{param.selection_values}'")
print(f"Selection Values (type): {type(param.selection_values)}")
print(f"Selection Values (bool): {bool(param.selection_values)}")
# Test get_selection_list method
try:
selection_list = param.get_selection_list()
print(f"\nget_selection_list() result:")
print(f" Type: {type(selection_list)}")
print(f" Content: {selection_list}")
print(f" Length: {len(selection_list) if isinstance(selection_list, list) else 'N/A'}")
except Exception as e:
print(f"\nERROR calling get_selection_list(): {str(e)}")
# 2. Check the specific result
result = env['lims.result'].browse(result_id)
if not result:
print(f"\n\nERROR: Result with ID {result_id} not found!")
return
print(f"\n\n--- Result Analysis (ID: {result_id}) ---")
print(f"Test: {result.test_id.name}")
print(f"Parameter: {result.parameter_id.name}")
print(f"Parameter Value Type: {result.parameter_id.value_type}")
# Check computed field
print(f"\nComputed field 'selection_options': {result.selection_options}")
# Manually test _get_selection_options
try:
# Clear cache to force recomputation
result.invalidate_recordset(['selection_options'])
# Access the field to trigger computation
options = result.selection_options
print(f"\nAfter invalidating cache:")
print(f" selection_options: {options}")
print(f" Type: {type(options)}")
except Exception as e:
print(f"\nERROR accessing selection_options: {str(e)}")
# 3. Direct method test
print(f"\n\n--- Direct Method Tests ---")
# Test parameter's get_selection_list directly
if result.parameter_id.value_type == 'selection':
param = result.parameter_id
print(f"\nParameter: {param.name}")
print(f"Selection Values: '{param.selection_values}'")
# Test parsing
if param.selection_values:
lines = param.selection_values.strip().split('\n')
print(f"\nParsing test:")
print(f" Lines after split: {lines}")
print(f" Number of lines: {len(lines)}")
parsed_options = []
for line in lines:
line = line.strip()
if line:
if '|' in line:
parts = line.split('|', 1)
if len(parts) == 2:
parsed_options.append((parts[0].strip(), parts[1].strip()))
else:
parsed_options.append((line, line))
print(f"\n Parsed options: {parsed_options}")
# 4. Check other results for the same test
print(f"\n\n--- Other Results for Test {test_id} ---")
other_results = env['lims.result'].search([('test_id', '=', test_id)], limit=5)
for res in other_results:
if res.parameter_id.value_type == 'selection':
print(f"\nResult ID: {res.id}")
print(f" Parameter: {res.parameter_id.name}")
print(f" Selection Options: {res.selection_options}")
print(f" Value: {res.value_text}")
# 5. Database check
print(f"\n\n--- Direct Database Check ---")
cr.execute("""
SELECT id, name, value_type, selection_values
FROM lims_parameter
WHERE value_type = 'selection'
AND id IN (SELECT parameter_id FROM lims_result WHERE id = %s)
""", (result_id,))
for row in cr.fetchall():
print(f"\nParameter ID: {row[0]}")
print(f" Name: {row[1]}")
print(f" Value Type: {row[2]}")
print(f" Selection Values: '{row[3]}'")
print(f" Selection Values (repr): {repr(row[3])}")
if __name__ == '__main__':
db_name = 'odoo'
registry = odoo.registry(db_name)
with registry.cursor() as cr:
diagnose_selection_issue(cr)