feature/5-analysis-catalog #25

Merged
luis_portillo merged 7 commits from feature/5-analysis-catalog into dev 2025-07-14 06:37:47 +00:00
17 changed files with 410 additions and 3 deletions

101
GEMINI.md
View File

@ -140,3 +140,104 @@ Busca errores en la salida. Si encuentras alguno, debes presentar un resumen del
- **Errores de sintaxis:** Problemas en archivos Python (`.py`) o XML (`.views`, `.xml`).
- **Permisos incorrectos:** Problemas de acceso a archivos o directorios.
- **Datos incorrectos:** Errores en los archivos de datos de demostración o iniciales.
### Política de Persistencia de la Instancia
Después de una instalación o actualización exitosa, la instancia de Odoo **debe permanecer activa** para permitir la validación manual por parte del usuario. **No se debe detener la instancia** (`docker-compose down -v`) hasta que el usuario confirme explícitamente que ha finalizado sus pruebas.
---
## Convenciones de Desarrollo en Odoo 18
Para evitar errores recurrentes, es **mandatorio** seguir las siguientes convenciones específicas para Odoo 18, especialmente en lo que respecta a la definición de vistas.
### Uso de Vistas de Lista (Tree Views)
En Odoo 18, la etiqueta `<tree>` ha sido **reemplazada por `<list>`**. El uso de `<tree>` provocará un error de validación (`ValueError: Wrong value for ir.ui.view.type: 'tree'`).
**Forma Incorrecta (Odoo < 18):**
```xml
<record id="view_example_tree" model="ir.ui.view">
<field name="arch" type="xml">
<tree string="Ejemplo">
<field name="name"/>
</tree>
</field>
</record>
```
**Forma Correcta (Odoo 18):**
```xml
<record id="view_example_list" model="ir.ui.view">
<field name="arch" type="xml">
<list string="Ejemplo">
<field name="name"/>
</list>
</field>
</record>
```
### Definición de Acciones de Ventana (`view_mode`)
Consecuente con el cambio anterior, al definir una acción de ventana (`ir.actions.act_window`) que deba mostrar una vista de lista, el `view_mode` debe ser `'list,form'` en lugar de `'tree,form'`.
**Forma Incorrecta:**
```xml
<record id="action_example" model="ir.actions.act_window">
<field name="name">Ejemplo</field>
<field name="res_model">example.model</field>
<field name="view_mode">tree,form</field>
</record>
```
**Forma Correcta:**
```xml
<record id="action_example" model="ir.actions.act_window">
<field name="name">Ejemplo</field>
<field name="res_model">example.model</field>
<field name="view_mode">list,form</field>
</record>
### Atributos de Visibilidad (`attrs`)
A partir de Odoo 17, el atributo `attrs` para controlar la visibilidad de los elementos ha sido **reemplazado por el uso directo de `invisible`**.
**Forma Incorrecta (Odoo < 17):**
```xml
<field name="special_field" attrs="{'invisible': [('is_special', '=', False)]}"/>
```
**Forma Correcta (Odoo 18):**
Se utiliza el atributo `invisible` con una expresión de dominio simplificada. La expresión se evalúa como verdadera para ocultar el campo.
```xml
<field name="special_field" invisible="not is_special"/>
```
O, de forma equivalente:
```xml
<field name="special_field" invisible="is_special == False"/>
```
### Uso de `ref()` en el Contexto de Acciones de Ventana
La función `ref('module.xml_id')` se utiliza para obtener el ID de base de datos de un registro a partir de su ID XML. Sin embargo, esta función **solo existe en el servidor**.
Cuando se define el `context` de una acción de ventana (`ir.actions.act_window`), este se evalúa en el cliente (navegador), donde `ref()` no está definido, causando un error `Name 'ref' is not defined`.
Para solucionar esto, el `context` debe ser evaluado en el servidor utilizando el atributo `eval`.
**Forma Incorrecta:**
```xml
<field name="context">{
'default_categ_id': ref('lims_management.product_category_analysis')
}</field>
```
Esto envía la cadena `"{'default_categ_id': ref(...)}"` al cliente, que no puede procesarla.
**Forma Correcta:**
```xml
<field name="context" eval="{
'default_categ_id': ref('lims_management.product_category_analysis')
}"/>
```
Al usar `eval`, Odoo ejecuta la expresión en el servidor, reemplaza `ref(...)` por el ID numérico correspondiente, y envía un diccionario JSON válido al cliente.
```

View File

@ -0,0 +1,84 @@
# Plan de Actividades: Issue #5 - Catálogo de Análisis Clínicos
## TODO
- [x] **Extender el Modelo de Productos (`product.template`):**
- [x] Crear `lims_management/models/product.py`.
- [x] Heredar de `product.template`.
- [x] Añadir campo booleano `is_analysis`.
- [x] Añadir campo de selección `analysis_type`.
- [x] Añadir campo de texto `technical_specifications`.
- [x] Crear campo `value_range` (One2many) que enlace al nuevo modelo `lims.analysis.range`.
- [x] **Crear el Modelo para Rangos de Referencia (`lims.analysis.range`):**
- [x] Crear `lims_management/models/analysis_range.py`.
- [x] Definir campos: `analysis_id` (Many2one), `gender`, `age_min`, `age_max`, `min_value`, `max_value`, `unit_of_measure`.
- [x] **Definir Permisos de Seguridad:**
- [x] Modificar `lims_management/security/ir.model.access.csv`.
- [x] Añadir permisos para el modelo `lims.analysis.range`.
- [x] **Crear las Vistas para el Catálogo de Análisis:**
- [x] Crear `lims_management/views/analysis_views.xml`.
- [x] Crear vista de lista/Kanban para análisis clínicos.
- [x] Heredar de la vista de formulario de productos para añadir la pestaña "Configuración de Análisis".
- [x] Mostrar campos condicionalmente (`is_analysis = True`).
- [x] Crear una vista de árbol independiente para los rangos de referencia (`lims.analysis.range`).
- [x] En la vista de formulario del producto, referenciar la nueva vista de árbol para el campo `value_range_ids`.
- [x] **(Nuevo)** Modificar las etiquetas en la acción de ventana para que se muestre "Análisis Clinico" en lugar de "Producto".
- [x] **Crear el Menú "Catálogo de Análisis":**
- [x] Modificar `lims_management/views/menus.xml`.
- [x] Crear una nueva acción de ventana (`ir.actions.act_window`).
- [x] **(Nuevo)** Definir valores por defecto en el `context` de la acción para: `type`, `purchase_ok`, `categ_id`.
- [x] **(Nuevo)** Crear la categoría de producto "Análisis Clínico" mediante un archivo de datos.
- [x] Crear un `menuitem` para "Catálogo de Análisis".
- [x] **Actualizar el Manifiesto (`__manifest__.py`):**
- [x] Añadir los nuevos modelos al `__init__.py` de la carpeta `models`.
- [x] Añadir el nuevo archivo de vistas a la lista `data` en `__manifest__.py`.
- [x] **Verificación Final:**
- [x] Reiniciar la instancia de Odoo (`docker-compose down -v` y `docker-compose up -d`).
- [x] Revisar logs de `odoo_init`.
- [x] Verificar la funcionalidad en la interfaz de Odoo.
- [x] **(Nuevo) Crear Datos de Demostración:**
- [x] Crear el archivo `demo/analysis_demo.xml`.
- [x] Definir registros de ejemplo para análisis clínicos (Hemograma, Perfil Lipídico, etc.).
- [x] Asegurarse de que los datos de demostración incluyan la configuración de los rangos de referencia.
- [x] Añadir el archivo `analysis_demo.xml` a la clave `demo` en `__manifest__.py`.
---
## Consideraciones Adicionales para Valores por Defecto
### 1. Análisis y Justificación
Para mejorar la experiencia de usuario y asegurar la consistencia de los datos, se realizó un análisis para determinar qué campos del modelo `product.template` deberían tener valores por defecto al crear un nuevo "Análisis Clínico". Esta decisión se basa en el estudio del código fuente de Odoo 18 (`product.template.py`) y los documentos de requerimientos y diseño del proyecto.
- **Análisis del Modelo `product.template`:** La revisión del modelo base de productos en Odoo 18 revel<65><6C> campos clave que definen el comportamiento de un producto en el sistema, tales como `type`, `sale_ok`, `purchase_ok`, y `categ_id`. Estos campos son fundamentales para que un producto se integre correctamente con los módulos de Ventas, Compras e Inventario.
- **Relación con los Requerimientos y Diseño Técnico:**
- El documento `RequerimientoInicial.md` especifica que los análisis clínicos deben ser considerados **servicios facturables**. Esto justifica la necesidad de configurar los análisis como productos de tipo "Servicio" (`type='service'`) que se puedan vender (`sale_ok=True`).
- El documento `ToBeDesing.md` enfatiza la importancia de **categorizar** la información para mantener el sistema organizado y facilitar los filtros. Esto respalda la creación de una categoría de producto específica, "Análisis Clínico", para agrupar todos estos servicios y separarlos de otros productos que la empresa pueda manejar.
- Ambos documentos dejan claro que el laboratorio presta estos servicios, pero no los compra. Por lo tanto, deshabilitar la opción de compra (`purchase_ok=False`) es coherente con el flujo de negocio, evitando que los análisis aparezcan en contextos de compra.
### 2. Propuesta de Valores por Defecto
Basado en el análisis anterior, se implementarán los siguientes valores por defecto al crear un nuevo análisis clínico desde su menú correspondiente:
| Campo en `product.template` | Valor por Defecto Recomendado | Justificación |
| :------------------------------------- | :---------------------------- | :------------------------------------------------------------------------------------------------------ |
| **`type` (Tipo de Producto)** | `'service'` (Servicio) | Un análisis es un servicio prestado, no un bien físico. Esto evita que Odoo intente gestionar su stock. |
| **`purchase_ok` (Se puede Comprar)** | `False` (Falso) | El laboratorio vende análisis, no los compra. Esto limpia la interfaz en los flujos de compra. |
| **`categ_id` (Categoría de Producto)** | `Análisis Clínico` | Permite agrupar, filtrar y aplicar reglas contables específicas a todos los análisis clínicos. |
| **`sale_ok` (Se puede Vender)** | `True` (Verdadero) | Esencial para que los análisis puedan ser añadidos a las órdenes de laboratorio (órdenes de venta). |
Estos valores se configurarán en el `context` de la acción de ventana (`ir.actions.act_window`) que gestiona la creación de nuevos análisis, asegurando que cada nuevo registro se cree con la configuración correcta de forma automática.

View File

@ -16,16 +16,19 @@
'website': "https://gitea.grupoconsiti.com/luis_portillo/clinical_laboratory",
'category': 'Industries',
'version': '18.0.1.0.0',
'depends': ['base'],
'depends': ['base', 'product'],
'data': [
'security/lims_security.xml',
'security/ir.model.access.csv',
'data/ir_sequence.xml',
'data/product_category.xml',
'views/partner_views.xml',
'views/analysis_views.xml',
'views/menus.xml',
],
'demo': [
'data/lims_demo.xml',
'demo/lims_demo.xml',
'demo/analysis_demo.xml',
],
'installable': True,
'application': True,

View File

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<data noupdate="1">
<record id="product_category_analysis" model="product.category">
<field name="name">Análisis Clínico</field>
</record>
</data>
</odoo>

View File

@ -0,0 +1,71 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<data>
<!-- Datos de Demostración para Análisis Clínicos -->
<!-- Análisis: Hemograma Completo -->
<record id="analysis_hemograma" model="product.template">
<field name="name">Hemograma Completo</field>
<field name="is_analysis">True</field>
<field name="analysis_type">hematology</field>
<field name="categ_id" ref="lims_management.product_category_analysis"/>
<field name="type">service</field>
<field name="purchase_ok" eval="False"/>
<field name="sale_ok" eval="True"/>
<field name="technical_specifications">
El hemograma completo es un análisis de sangre que mide los niveles de los principales componentes sanguíneos: glóbulos rojos, glóbulos blancos y plaquetas.
</field>
</record>
<!-- Rangos de Referencia para Hemograma -->
<record id="range_hemograma_globulos_rojos_m" model="lims.analysis.range">
<field name="analysis_id" ref="analysis_hemograma"/>
<field name="gender">male</field>
<field name="age_min">18</field>
<field name="age_max">99</field>
<field name="min_value">4.5</field>
<field name="max_value">5.9</field>
<field name="unit_of_measure">millones/µL</field>
</record>
<record id="range_hemograma_globulos_rojos_f" model="lims.analysis.range">
<field name="analysis_id" ref="analysis_hemograma"/>
<field name="gender">female</field>
<field name="age_min">18</field>
<field name="age_max">99</field>
<field name="min_value">4.0</field>
<field name="max_value">5.2</field>
<field name="unit_of_measure">millones/µL</field>
</record>
<!-- Análisis: Perfil Lipídico -->
<record id="analysis_perfil_lipidico" model="product.template">
<field name="name">Perfil Lipídico</field>
<field name="is_analysis">True</field>
<field name="analysis_type">chemistry</field>
<field name="categ_id" ref="lims_management.product_category_analysis"/>
<field name="type">service</field>
<field name="purchase_ok" eval="False"/>
<field name="sale_ok" eval="True"/>
<field name="technical_specifications">
Mide los niveles de colesterol y otros lípidos en la sangre. Incluye Colesterol Total, LDL, HDL y Triglicéridos.
</field>
</record>
<!-- Rangos para Colesterol Total -->
<record id="range_colesterol_total" model="lims.analysis.range">
<field name="analysis_id" ref="analysis_perfil_lipidico"/>
<field name="min_value">0</field>
<field name="max_value">200</field>
<field name="unit_of_measure">mg/dL</field>
</record>
<!-- Rangos para Colesterol LDL -->
<record id="range_colesterol_ldl" model="lims.analysis.range">
<field name="analysis_id" ref="analysis_perfil_lipidico"/>
<field name="min_value">0</field>
<field name="max_value">100</field>
<field name="unit_of_measure">mg/dL</field>
</record>
</data>
</odoo>

View File

@ -1,2 +1,4 @@
# -*- coding: utf-8 -*-
from . import partner
from . import partner
from . import product
from . import analysis_range

View File

@ -0,0 +1,26 @@
# -*- coding: utf-8 -*-
from odoo import models, fields
class LimsAnalysisRange(models.Model):
_name = 'lims.analysis.range'
_description = 'Rangos de Referencia para Análisis Clínicos'
analysis_id = fields.Many2one(
'product.template',
string="Análisis",
required=True,
ondelete='cascade'
)
gender = fields.Selection([
('male', 'Masculino'),
('female', 'Femenino'),
('both', 'Ambos')
], string="Género", default='both')
age_min = fields.Integer(string="Edad Mínima", default=0)
age_max = fields.Integer(string="Edad Máxima", default=99)
min_value = fields.Float(string="Valor Mínimo")
max_value = fields.Float(string="Valor Máximo")
unit_of_measure = fields.Char(string="Unidad de Medida")

View File

@ -0,0 +1,28 @@
# -*- coding: utf-8 -*-
from odoo import models, fields
class ProductTemplate(models.Model):
_inherit = 'product.template'
is_analysis = fields.Boolean(
string="Es un Análisis Clínico",
help="Marcar si este producto es un análisis clínico."
)
analysis_type = fields.Selection([
('hematology', 'Hematología'),
('chemistry', 'Química Clínica'),
('microbiology', 'Microbiología'),
('immunology', 'Inmunología'),
('endocrinology', 'Endocrinología'),
('other', 'Otro')
], string="Tipo de Análisis")
technical_specifications = fields.Text(
string="Especificaciones Técnicas"
)
value_range_ids = fields.One2many(
'lims.analysis.range',
'analysis_id',
string="Rangos de Referencia"
)

View File

@ -1 +1,2 @@
id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
access_lims_analysis_range_user,lims.analysis.range.user,model_lims_analysis_range,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_lims_analysis_range_user lims.analysis.range.user model_lims_analysis_range base.group_user 1 1 1 1

View File

@ -0,0 +1,49 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<data>
<!-- Vista de Lista para Rangos de Referencia -->
<record id="view_lims_analysis_range_tree" model="ir.ui.view">
<field name="name">lims.analysis.range.tree</field>
<field name="model">lims.analysis.range</field>
<field name="arch" type="xml">
<list string="Rangos de Referencia" editable="bottom">
<field name="gender"/>
<field name="age_min"/>
<field name="age_max"/>
<field name="min_value"/>
<field name="max_value"/>
<field name="unit_of_measure"/>
</list>
</field>
</record>
<!-- Hereda la vista de formulario de producto para añadir la pestaña de Análisis -->
<record id="view_product_template_form_lims" model="ir.ui.view">
<field name="name">product.template.form.lims</field>
<field name="model">product.template</field>
<field name="inherit_id" ref="product.product_template_form_view"/>
<field name="arch" type="xml">
<xpath expr="//notebook" position="inside">
<page string="Configuración de Análisis" name="analysis_config"
invisible="not is_analysis">
<group>
<group>
<field name="analysis_type"/>
</group>
<group>
<field name="technical_specifications"/>
</group>
</group>
<separator string="Rangos de Referencia"/>
<field name="value_range_ids"
view_id="lims_management.view_lims_analysis_range_tree"/>
</page>
</xpath>
<!-- Añade el campo is_analysis cerca del nombre del producto para fácil acceso -->
<xpath expr="//field[@name='name']" position="after">
<field name="is_analysis"/>
</xpath>
</field>
</record>
</data>
</odoo>

View File

@ -52,5 +52,39 @@
parent="lims_menu_root"
action="action_lims_doctor"
sequence="30"/>
<!-- Submenú de Configuración -->
<menuitem
id="lims_menu_config"
name="Configuración"
parent="lims_menu_root"
sequence="100"/>
<!-- Acción de Ventana para Catálogo de Análisis -->
<record id="action_lims_analysis_catalog" model="ir.actions.act_window">
<field name="name">Análisis Clínicos</field>
<field name="res_model">product.template</field>
<field name="view_mode">kanban,form</field>
<field name="domain">[('is_analysis', '=', True)]</field>
<field name="context" eval="{
'default_is_analysis': True,
'default_type': 'service',
'default_purchase_ok': False,
'default_categ_id': ref('lims_management.product_category_analysis')
}"/>
<field name="help" type="html">
<p class="o_view_nocontent_smiling_face">
Crea un nuevo análisis clínico
</p>
</field>
</record>
<!-- Menú para Catálogo de Análisis -->
<menuitem
id="lims_menu_analysis_catalog"
name="Análisis Clínicos"
parent="lims_menu_config"
action="action_lims_analysis_catalog"
sequence="10"/>
</data>
</odoo>