feat(#71): Implementar dashboards para administrador del laboratorio

- Dashboard de Estado de Órdenes: Vista gráfica y pivot de órdenes por estado
- Dashboard de Productividad de Técnicos: Análisis de pruebas por técnico
- Dashboard de Muestras: Estado y distribución de muestras por tipo
- Dashboard de Parámetros Fuera de Rango: Identificación de resultados críticos
- Dashboard de Análisis Más Solicitados: Top de análisis por período
- Dashboard de Distribución Demográfica: Tests por género y rango de edad
- Agregar campos computed age_range, patient_gender y patient_age_range
- Configurar menú de Dashboards solo para administradores

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Luis Ernesto Portillo Zaldivar 2025-07-17 11:17:26 -06:00
parent 34f3b0aa14
commit d51d3b5d69
7 changed files with 499 additions and 0 deletions

78
dashboard_analysis.md Normal file
View File

@ -0,0 +1,78 @@
# Análisis de Dashboards para LIMS - Issue #71
## Dashboards Implementables sin Módulos Adicionales ni Cambios Estructurales
### 1. ✅ Dashboard de Estado de Órdenes
**Factibilidad**: Alta
- Usar vistas graph y pivot nativas de Odoo
- Datos disponibles: sale.order con is_lab_request=True
- Métricas: órdenes por estado, por fecha, por paciente
### 2. ✅ Dashboard de Productividad de Técnicos
**Factibilidad**: Alta
- Datos disponibles: lims.test (technician_id, state, create_date, validation_date)
- Métricas: pruebas procesadas por técnico, tiempos promedio, estados
### 3. ✅ Dashboard de Muestras
**Factibilidad**: Alta
- Datos disponibles: stock.lot con is_lab_sample=True
- Métricas: muestras por estado, rechazos, re-muestreos
### 4. ✅ Dashboard de Parámetros Fuera de Rango
**Factibilidad**: Alta
- Datos disponibles: lims.result (is_out_of_range, is_critical)
- Métricas: resultados críticos, fuera de rango por parámetro
### 5. ✅ Dashboard de Análisis Más Solicitados
**Factibilidad**: Alta
- Datos disponibles: sale.order.line con productos is_analysis=True
- Métricas: top análisis, tendencias por período
### 6. ⚠️ Dashboard de Tiempos de Respuesta
**Factibilidad**: Media
- Requiere campos calculados (no almacenados actualmente)
- Necesitaría agregar campos store=True para métricas de tiempo
### 7. ❌ Dashboard de Facturación
**Factibilidad**: Baja
- Requiere módulo account (facturación)
- No está en las dependencias actuales
### 8. ❌ Dashboard de Inventario de Reactivos
**Factibilidad**: Baja
- Requiere configuración adicional de stock
- No hay modelo específico para reactivos
## Implementación Técnica
### Herramientas Disponibles en Odoo 18:
1. **Vistas Graph**: Gráficos de barras, líneas, pie
2. **Vistas Pivot**: Tablas dinámicas
3. **Vistas Cohort**: Análisis de cohortes
4. **Filtros y Agrupaciones**: Para segmentar datos
5. **Acciones de Servidor**: Para cálculos complejos
### Estructura Propuesta:
```xml
<!-- Menú principal de Dashboards -->
<menuitem id="menu_lims_dashboards"
name="Dashboards"
parent="lims_management.menu_lims_root"
sequence="5"
groups="group_lims_admin,group_lims_manager"/>
```
## Recomendación
Sugiero comenzar con los 5 dashboards marcados con ✅ ya que:
1. Utilizan datos existentes
2. No requieren cambios en modelos
3. Usan herramientas nativas de Odoo
4. Proveen valor inmediato al administrador
Orden de implementación sugerido:
1. Dashboard de Estado de Órdenes (más básico)
2. Dashboard de Productividad de Técnicos
3. Dashboard de Muestras
4. Dashboard de Parámetros Fuera de Rango
5. Dashboard de Análisis Más Solicitados

View File

@ -45,6 +45,7 @@
'views/analysis_parameter_views.xml',
'views/product_template_parameter_config_views.xml',
'views/parameter_dashboard_views.xml',
'views/dashboard_views.xml',
'views/menus.xml',
'views/lims_config_views.xml',
'report/sample_label_report.xml',

View File

@ -116,6 +116,21 @@ class LimsTest(models.Model):
default=lambda self: self.env.company
)
# Campos para dashboards demográficos
patient_gender = fields.Selection(
related='patient_id.gender',
string='Género del Paciente',
store=True,
readonly=True
)
patient_age_range = fields.Selection(
related='patient_id.age_range',
string='Rango de Edad',
store=True,
readonly=True
)
@api.depends('company_id')
def _compute_require_validation(self):
"""Calcula si la prueba requiere validación basado en configuración."""

View File

@ -29,6 +29,17 @@ class ResPartner(models.Model):
help="Edad calculada en años basada en la fecha de nacimiento"
)
age_range = fields.Selection([
('0-10', '0-10 años'),
('11-20', '11-20 años'),
('21-30', '21-30 años'),
('31-40', '31-40 años'),
('41-50', '41-50 años'),
('51-60', '51-60 años'),
('61-70', '61-70 años'),
('71+', 'Más de 70 años')
], string="Rango de Edad", compute='_compute_age_range', store=True)
is_pregnant = fields.Boolean(
string="Embarazada",
help="Marcar si la paciente está embarazada (solo aplica para género femenino)"
@ -54,6 +65,34 @@ class ResPartner(models.Model):
else:
partner.age = 0
@api.depends('birthdate_date')
def _compute_age_range(self):
"""Calcula el rango de edad basado en la edad"""
for partner in self:
if partner.birthdate_date:
today = date.today()
delta = relativedelta(today, partner.birthdate_date)
age = delta.years
if age <= 10:
partner.age_range = '0-10'
elif age <= 20:
partner.age_range = '11-20'
elif age <= 30:
partner.age_range = '21-30'
elif age <= 40:
partner.age_range = '31-40'
elif age <= 50:
partner.age_range = '41-50'
elif age <= 60:
partner.age_range = '51-60'
elif age <= 70:
partner.age_range = '61-70'
else:
partner.age_range = '71+'
else:
partner.age_range = False
@api.constrains('is_pregnant', 'gender')
def _check_pregnant_gender(self):
"""Valida que solo pacientes de género femenino puedan estar embarazadas"""

View File

@ -0,0 +1,321 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<!-- ================================================================
DASHBOARD 1: Estado de Órdenes de Laboratorio
================================================================ -->
<!-- Vista Graph para Estado de Órdenes -->
<record id="view_lab_order_dashboard_graph" model="ir.ui.view">
<field name="name">sale.order.lab.dashboard.graph</field>
<field name="model">sale.order</field>
<field name="arch" type="xml">
<graph string="Estado de &#211;rdenes" type="pie">
<field name="state"/>
</graph>
</field>
</record>
<!-- Vista Pivot para Estado de Órdenes -->
<record id="view_lab_order_dashboard_pivot" model="ir.ui.view">
<field name="name">sale.order.lab.dashboard.pivot</field>
<field name="model">sale.order</field>
<field name="arch" type="xml">
<pivot string="An&#225;lisis de &#211;rdenes">
<field name="date_order" interval="month" type="col"/>
<field name="state" type="row"/>
</pivot>
</field>
</record>
<!-- Acción para Dashboard de Estado de Órdenes -->
<record id="action_lab_order_dashboard" model="ir.actions.act_window">
<field name="name">Estado de &#211;rdenes</field>
<field name="res_model">sale.order</field>
<field name="view_mode">graph,pivot,tree,form</field>
<field name="domain">[('is_lab_request', '=', True)]</field>
<field name="context">{'search_default_group_by_state': 1}</field>
<field name="help" type="html">
<p class="o_view_nocontent_smiling_face">
No hay &#243;rdenes de laboratorio registradas
</p>
<p>
Este dashboard muestra el estado actual de todas las &#243;rdenes de laboratorio.
</p>
</field>
</record>
<!-- ================================================================
DASHBOARD 2: Productividad de Técnicos
================================================================ -->
<!-- Vista Graph para Productividad de Técnicos -->
<record id="view_test_technician_productivity_graph" model="ir.ui.view">
<field name="name">lims.test.technician.productivity.graph</field>
<field name="model">lims.test</field>
<field name="arch" type="xml">
<graph string="Productividad de T&#233;cnicos" type="bar">
<field name="technician_id"/>
<field name="state"/>
</graph>
</field>
</record>
<!-- Vista Pivot para Productividad de Técnicos -->
<record id="view_test_technician_productivity_pivot" model="ir.ui.view">
<field name="name">lims.test.technician.productivity.pivot</field>
<field name="model">lims.test</field>
<field name="arch" type="xml">
<pivot string="An&#225;lisis por T&#233;cnico">
<field name="technician_id" type="row"/>
<field name="state" type="col"/>
</pivot>
</field>
</record>
<!-- Acción para Dashboard de Productividad de Técnicos -->
<record id="action_technician_productivity_dashboard" model="ir.actions.act_window">
<field name="name">Productividad de T&#233;cnicos</field>
<field name="res_model">lims.test</field>
<field name="view_mode">graph,pivot,tree,form</field>
<field name="context">{'search_default_group_by_technician': 1, 'search_default_this_month': 1}</field>
<field name="help" type="html">
<p class="o_view_nocontent_smiling_face">
No hay pruebas registradas
</p>
<p>
Este dashboard muestra la productividad de cada t&#233;cnico del laboratorio.
</p>
</field>
</record>
<!-- ================================================================
DASHBOARD 3: Estado de Muestras
================================================================ -->
<!-- Vista Graph para Estado de Muestras -->
<record id="view_sample_status_graph" model="ir.ui.view">
<field name="name">stock.lot.sample.status.graph</field>
<field name="model">stock.lot</field>
<field name="arch" type="xml">
<graph string="Estado de Muestras" type="pie">
<field name="state"/>
</graph>
</field>
</record>
<!-- Vista Pivot para Muestras por Tipo -->
<record id="view_sample_type_pivot" model="ir.ui.view">
<field name="name">stock.lot.sample.type.pivot</field>
<field name="model">stock.lot</field>
<field name="arch" type="xml">
<pivot string="Muestras por Tipo">
<field name="sample_type_product_id" type="row"/>
<field name="state" type="col"/>
</pivot>
</field>
</record>
<!-- Acción para Dashboard de Muestras -->
<record id="action_sample_dashboard" model="ir.actions.act_window">
<field name="name">Dashboard de Muestras</field>
<field name="res_model">stock.lot</field>
<field name="view_mode">graph,pivot,tree,form</field>
<field name="domain">[('is_lab_sample', '=', True)]</field>
<field name="context">{'search_default_group_by_state': 1}</field>
<field name="help" type="html">
<p class="o_view_nocontent_smiling_face">
No hay muestras registradas
</p>
<p>
Este dashboard muestra el estado de todas las muestras del laboratorio.
</p>
</field>
</record>
<!-- ================================================================
DASHBOARD 4: Parámetros Fuera de Rango
================================================================ -->
<!-- Vista Graph para Parámetros Fuera de Rango -->
<record id="view_result_out_of_range_graph" model="ir.ui.view">
<field name="name">lims.result.out.of.range.graph</field>
<field name="model">lims.result</field>
<field name="arch" type="xml">
<graph string="Par&#225;metros Fuera de Rango" type="bar">
<field name="parameter_id"/>
<field name="is_out_of_range" type="measure"/>
</graph>
</field>
</record>
<!-- Vista Pivot para Resultados Críticos -->
<record id="view_result_critical_pivot" model="ir.ui.view">
<field name="name">lims.result.critical.pivot</field>
<field name="model">lims.result</field>
<field name="arch" type="xml">
<pivot string="Resultados Cr&#237;ticos">
<field name="parameter_id" type="row"/>
<field name="is_critical" type="col"/>
<field name="is_out_of_range" type="col"/>
</pivot>
</field>
</record>
<!-- Acción para Dashboard de Parámetros Fuera de Rango -->
<record id="action_out_of_range_dashboard" model="ir.actions.act_window">
<field name="name">Par&#225;metros Fuera de Rango</field>
<field name="res_model">lims.result</field>
<field name="view_mode">graph,pivot,tree,form</field>
<field name="domain">[('test_id.state', '=', 'validated')]</field>
<field name="context">{'search_default_out_of_range': 1}</field>
<field name="help" type="html">
<p class="o_view_nocontent_smiling_face">
No hay resultados fuera de rango
</p>
<p>
Este dashboard muestra los par&#225;metros que est&#225;n fuera de los rangos normales.
</p>
</field>
</record>
<!-- ================================================================
DASHBOARD 5: Análisis Más Solicitados
================================================================ -->
<!-- Vista Graph para Top Análisis -->
<record id="view_top_analysis_graph" model="ir.ui.view">
<field name="name">sale.order.line.top.analysis.graph</field>
<field name="model">sale.order.line</field>
<field name="arch" type="xml">
<graph string="An&#225;lisis M&#225;s Solicitados" type="bar">
<field name="product_id"/>
<field name="product_uom_qty" type="measure"/>
</graph>
</field>
</record>
<!-- Vista Pivot para Análisis por Período -->
<record id="view_analysis_period_pivot" model="ir.ui.view">
<field name="name">sale.order.line.analysis.period.pivot</field>
<field name="model">sale.order.line</field>
<field name="arch" type="xml">
<pivot string="An&#225;lisis por Per&#237;odo">
<field name="create_date" interval="month" type="col"/>
<field name="product_id" type="row"/>
<field name="product_uom_qty" type="measure"/>
</pivot>
</field>
</record>
<!-- Acción para Dashboard de Análisis Más Solicitados -->
<record id="action_top_analysis_dashboard" model="ir.actions.act_window">
<field name="name">An&#225;lisis M&#225;s Solicitados</field>
<field name="res_model">sale.order.line</field>
<field name="view_mode">graph,pivot,tree</field>
<field name="domain">[('order_id.is_lab_request', '=', True), ('product_id.is_analysis', '=', True)]</field>
<field name="context">{'search_default_group_by_product': 1}</field>
<field name="help" type="html">
<p class="o_view_nocontent_smiling_face">
No hay an&#225;lisis registrados
</p>
<p>
Este dashboard muestra los an&#225;lisis m&#225;s solicitados en el laboratorio.
</p>
</field>
</record>
<!-- ================================================================
DASHBOARD 6: Distribución de Tests por Demografía
================================================================ -->
<!-- Vista Graph para Distribución por Sexo -->
<record id="view_test_gender_distribution_graph" model="ir.ui.view">
<field name="name">lims.test.gender.distribution.graph</field>
<field name="model">lims.test</field>
<field name="arch" type="xml">
<graph string="Distribuci&#243;n por G&#233;nero" type="pie">
<field name="patient_gender"/>
</graph>
</field>
</record>
<!-- Vista Pivot para Tests por Edad y Sexo -->
<record id="view_test_demographics_pivot" model="ir.ui.view">
<field name="name">lims.test.demographics.pivot</field>
<field name="model">lims.test</field>
<field name="arch" type="xml">
<pivot string="Tests por Demograf&#237;a">
<field name="patient_age_range" type="row"/>
<field name="patient_gender" type="col"/>
</pivot>
</field>
</record>
<!-- Acción para Dashboard de Distribución Demográfica -->
<record id="action_test_demographics_dashboard" model="ir.actions.act_window">
<field name="name">Distribuci&#243;n Demogr&#225;fica de Tests</field>
<field name="res_model">lims.test</field>
<field name="view_mode">graph,pivot,tree</field>
<field name="domain">[('state', '=', 'validated')]</field>
<field name="context">{'search_default_this_year': 1}</field>
<field name="help" type="html">
<p class="o_view_nocontent_smiling_face">
No hay tests validados
</p>
<p>
Este dashboard muestra la distribuci&#243;n de tests por caracter&#237;sticas demogr&#225;ficas de los pacientes.
</p>
</field>
</record>
<!-- ================================================================
FILTROS DE BÚSQUEDA PARA DASHBOARDS
================================================================ -->
<!-- Filtros para Tests -->
<record id="view_lims_test_dashboard_search" model="ir.ui.view">
<field name="name">lims.test.dashboard.search</field>
<field name="model">lims.test</field>
<field name="arch" type="xml">
<search>
<!-- Filtros de Estado -->
<filter string="En Proceso" name="in_process" domain="[('state', '=', 'in_process')]"/>
<filter string="Validados" name="validated" domain="[('state', '=', 'validated')]"/>
<!-- Filtros de Tiempo -->
<filter string="Hoy" name="today" domain="[('create_date', '&gt;=', datetime.datetime.now().replace(hour=0, minute=0, second=0))]"/>
<filter string="Esta Semana" name="this_week" domain="[('create_date', '&gt;=', (datetime.datetime.now() - datetime.timedelta(days=7)).strftime('%Y-%m-%d'))]"/>
<filter string="Este Mes" name="this_month" domain="[('create_date', '&gt;=', datetime.datetime.now().replace(day=1).strftime('%Y-%m-%d'))]"/>
<filter string="Este A&#241;o" name="this_year" domain="[('create_date', '&gt;=', datetime.datetime.now().replace(month=1, day=1).strftime('%Y-%m-%d'))]"/>
<!-- Agrupaciones -->
<group expand="0" string="Agrupar Por">
<filter string="T&#233;cnico" name="group_by_technician" context="{'group_by': 'technician_id'}"/>
<filter string="Estado" name="group_by_state" context="{'group_by': 'state'}"/>
<filter string="Paciente" name="group_by_patient" context="{'group_by': 'patient_id'}"/>
<filter string="An&#225;lisis" name="group_by_product" context="{'group_by': 'product_id'}"/>
<filter string="Fecha" name="group_by_date" context="{'group_by': 'create_date:month'}"/>
</group>
</search>
</field>
</record>
<!-- Filtros para Resultados -->
<record id="view_lims_result_dashboard_search" model="ir.ui.view">
<field name="name">lims.result.dashboard.search</field>
<field name="model">lims.result</field>
<field name="arch" type="xml">
<search>
<!-- Filtros de Rango -->
<filter string="Fuera de Rango" name="out_of_range" domain="[('is_out_of_range', '=', True)]"/>
<filter string="Cr&#237;ticos" name="critical" domain="[('is_critical', '=', True)]"/>
<!-- Agrupaciones -->
<group expand="0" string="Agrupar Por">
<filter string="Par&#225;metro" name="group_by_parameter" context="{'group_by': 'parameter_id'}"/>
</group>
</search>
</field>
</record>
</odoo>

View File

@ -155,6 +155,51 @@
action="action_lims_result"
sequence="30"/>
<!-- Submenú de Dashboards -->
<menuitem
id="menu_lims_dashboards"
name="Dashboards"
parent="lims_menu_root"
sequence="85"
groups="lims_management.group_lims_admin"/>
<!-- Dashboards individuales -->
<menuitem id="menu_lab_order_dashboard"
name="Estado de &#211;rdenes"
parent="menu_lims_dashboards"
action="action_lab_order_dashboard"
sequence="10"/>
<menuitem id="menu_technician_productivity_dashboard"
name="Productividad de T&#233;cnicos"
parent="menu_lims_dashboards"
action="action_technician_productivity_dashboard"
sequence="20"/>
<menuitem id="menu_sample_dashboard"
name="Dashboard de Muestras"
parent="menu_lims_dashboards"
action="action_sample_dashboard"
sequence="30"/>
<menuitem id="menu_out_of_range_dashboard"
name="Par&#225;metros Fuera de Rango"
parent="menu_lims_dashboards"
action="action_out_of_range_dashboard"
sequence="40"/>
<menuitem id="menu_top_analysis_dashboard"
name="An&#225;lisis M&#225;s Solicitados"
parent="menu_lims_dashboards"
action="action_top_analysis_dashboard"
sequence="50"/>
<menuitem id="menu_test_demographics_dashboard"
name="Distribuci&#243;n Demogr&#225;fica"
parent="menu_lims_dashboards"
action="action_test_demographics_dashboard"
sequence="60"/>
<!-- Submenú de Reportes -->
<menuitem
id="lims_menu_reports"